Add SDK 29 sources.

Test: N/A
Change-Id: Iedb7a31029e003928eb16f7e69ed147e72bb6235
diff --git a/android/net/CaptivePortal.java b/android/net/CaptivePortal.java
new file mode 100644
index 0000000..a66fcae
--- /dev/null
+++ b/android/net/CaptivePortal.java
@@ -0,0 +1,152 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed urnder 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.NonNull;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+
+/**
+ * A class allowing apps handling the {@link ConnectivityManager#ACTION_CAPTIVE_PORTAL_SIGN_IN}
+ * activity to indicate to the system different outcomes of captive portal sign in.  This class is
+ * passed as an extra named {@link ConnectivityManager#EXTRA_CAPTIVE_PORTAL} with the
+ * {@code ACTION_CAPTIVE_PORTAL_SIGN_IN} activity.
+ */
+public class CaptivePortal implements Parcelable {
+    /**
+     * Response code from the captive portal application, indicating that the portal was dismissed
+     * and the network should be re-validated.
+     * @see ICaptivePortal#appResponse(int)
+     * @see android.net.INetworkMonitor#notifyCaptivePortalAppFinished(int)
+     * @hide
+     */
+    @SystemApi
+    @TestApi
+    public static final int APP_RETURN_DISMISSED    = 0;
+    /**
+     * Response code from the captive portal application, indicating that the user did not login and
+     * does not want to use the captive portal network.
+     * @see ICaptivePortal#appResponse(int)
+     * @see android.net.INetworkMonitor#notifyCaptivePortalAppFinished(int)
+     * @hide
+     */
+    @SystemApi
+    @TestApi
+    public static final int APP_RETURN_UNWANTED     = 1;
+    /**
+     * Response code from the captive portal application, indicating that the user does not wish to
+     * login but wants to use the captive portal network as-is.
+     * @see ICaptivePortal#appResponse(int)
+     * @see android.net.INetworkMonitor#notifyCaptivePortalAppFinished(int)
+     * @hide
+     */
+    @SystemApi
+    @TestApi
+    public static final int APP_RETURN_WANTED_AS_IS = 2;
+
+    private final IBinder mBinder;
+
+    /** @hide */
+    public CaptivePortal(@NonNull IBinder binder) {
+        mBinder = binder;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeStrongBinder(mBinder);
+    }
+
+    public static final @android.annotation.NonNull Parcelable.Creator<CaptivePortal> CREATOR
+            = new Parcelable.Creator<CaptivePortal>() {
+        @Override
+        public CaptivePortal createFromParcel(Parcel in) {
+            return new CaptivePortal(in.readStrongBinder());
+        }
+
+        @Override
+        public CaptivePortal[] newArray(int size) {
+            return new CaptivePortal[size];
+        }
+    };
+
+    /**
+     * Indicate to the system that the captive portal has been
+     * dismissed.  In response the framework will re-evaluate the network's
+     * connectivity and might take further action thereafter.
+     */
+    public void reportCaptivePortalDismissed() {
+        try {
+            ICaptivePortal.Stub.asInterface(mBinder).appResponse(APP_RETURN_DISMISSED);
+        } catch (RemoteException e) {
+        }
+    }
+
+    /**
+     * Indicate to the system that the user does not want to pursue signing in to the
+     * captive portal and the system should continue to prefer other networks
+     * without captive portals for use as the default active data network.  The
+     * system will not retest the network for a captive portal so as to avoid
+     * disturbing the user with further sign in to network notifications.
+     */
+    public void ignoreNetwork() {
+        try {
+            ICaptivePortal.Stub.asInterface(mBinder).appResponse(APP_RETURN_UNWANTED);
+        } catch (RemoteException e) {
+        }
+    }
+
+    /**
+     * Indicate to the system the user wants to use this network as is, even though
+     * the captive portal is still in place.  The system will treat the network
+     * as if it did not have a captive portal when selecting the network to use
+     * as the default active data network. This may result in this network
+     * becoming the default active data network, which could disrupt network
+     * connectivity for apps because the captive portal is still in place.
+     * @hide
+     */
+    @SystemApi
+    @TestApi
+    public void useNetwork() {
+        try {
+            ICaptivePortal.Stub.asInterface(mBinder).appResponse(APP_RETURN_WANTED_AS_IS);
+        } catch (RemoteException e) {
+        }
+    }
+
+    /**
+     * Log a captive portal login event.
+     * @param eventId one of the CAPTIVE_PORTAL_LOGIN_* constants in metrics_constants.proto.
+     * @param packageName captive portal application package name.
+     * @hide
+     */
+    @SystemApi
+    @TestApi
+    public void logEvent(int eventId, @NonNull String packageName) {
+        try {
+            ICaptivePortal.Stub.asInterface(mBinder).logEvent(eventId, packageName);
+        } catch (RemoteException e) {
+        }
+    }
+}
diff --git a/android/net/ConnectionInfo.java b/android/net/ConnectionInfo.java
new file mode 100644
index 0000000..4514a84
--- /dev/null
+++ b/android/net/ConnectionInfo.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2018 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.os.Parcel;
+import android.os.Parcelable;
+
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.UnknownHostException;
+
+/**
+ * Describe a network connection including local and remote address/port of a connection and the
+ * transport protocol.
+ *
+ * @hide
+ */
+public final class ConnectionInfo implements Parcelable {
+    public final int protocol;
+    public final InetSocketAddress local;
+    public final InetSocketAddress remote;
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public ConnectionInfo(int protocol, InetSocketAddress local, InetSocketAddress remote) {
+        this.protocol = protocol;
+        this.local = local;
+        this.remote = remote;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeInt(protocol);
+        out.writeByteArray(local.getAddress().getAddress());
+        out.writeInt(local.getPort());
+        out.writeByteArray(remote.getAddress().getAddress());
+        out.writeInt(remote.getPort());
+    }
+
+    public static final @android.annotation.NonNull Creator<ConnectionInfo> CREATOR = new Creator<ConnectionInfo>() {
+        public ConnectionInfo createFromParcel(Parcel in) {
+            int protocol = in.readInt();
+            InetAddress localAddress;
+            try {
+                localAddress = InetAddress.getByAddress(in.createByteArray());
+            } catch (UnknownHostException e) {
+                throw new IllegalArgumentException("Invalid InetAddress");
+            }
+            int localPort = in.readInt();
+            InetAddress remoteAddress;
+            try {
+                remoteAddress = InetAddress.getByAddress(in.createByteArray());
+            } catch (UnknownHostException e) {
+                throw new IllegalArgumentException("Invalid InetAddress");
+            }
+            int remotePort = in.readInt();
+            InetSocketAddress local = new InetSocketAddress(localAddress, localPort);
+            InetSocketAddress remote = new InetSocketAddress(remoteAddress, remotePort);
+            return new ConnectionInfo(protocol, local, remote);
+        }
+
+        public ConnectionInfo[] newArray(int size) {
+            return new ConnectionInfo[size];
+        }
+    };
+}
diff --git a/android/net/ConnectivityManager.java b/android/net/ConnectivityManager.java
new file mode 100644
index 0000000..111a8c4
--- /dev/null
+++ b/android/net/ConnectivityManager.java
@@ -0,0 +1,4519 @@
+/*
+ * Copyright (C) 2008 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 static android.net.IpSecManager.INVALID_RESOURCE_ID;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.SystemApi;
+import android.annotation.SystemService;
+import android.annotation.TestApi;
+import android.annotation.UnsupportedAppUsage;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.net.IpSecManager.UdpEncapsulationSocket;
+import android.net.SocketKeepalive.Callback;
+import android.os.Binder;
+import android.os.Build;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.INetworkActivityListener;
+import android.os.INetworkManagementService;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.ParcelFileDescriptor;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.ResultReceiver;
+import android.os.ServiceManager;
+import android.os.ServiceSpecificException;
+import android.provider.Settings;
+import android.telephony.SubscriptionManager;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.SparseIntArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.telephony.ITelephony;
+import com.android.internal.telephony.PhoneConstants;
+import com.android.internal.util.Preconditions;
+import com.android.internal.util.Protocol;
+
+import libcore.net.event.NetworkEventDispatcher;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.Socket;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.RejectedExecutionException;
+
+/**
+ * Class that answers queries about the state of network connectivity. It also
+ * notifies applications when network connectivity changes.
+ * <p>
+ * The primary responsibilities of this class are to:
+ * <ol>
+ * <li>Monitor network connections (Wi-Fi, GPRS, UMTS, etc.)</li>
+ * <li>Send broadcast intents when network connectivity changes</li>
+ * <li>Attempt to "fail over" to another network when connectivity to a network
+ * is lost</li>
+ * <li>Provide an API that allows applications to query the coarse-grained or fine-grained
+ * state of the available networks</li>
+ * <li>Provide an API that allows applications to request and select networks for their data
+ * traffic</li>
+ * </ol>
+ */
+@SystemService(Context.CONNECTIVITY_SERVICE)
+public class ConnectivityManager {
+    private static final String TAG = "ConnectivityManager";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    /**
+     * A change in network connectivity has occurred. A default connection has either
+     * been established or lost. The NetworkInfo for the affected network is
+     * sent as an extra; it should be consulted to see what kind of
+     * connectivity event occurred.
+     * <p/>
+     * Apps targeting Android 7.0 (API level 24) and higher do not receive this
+     * broadcast if they declare the broadcast receiver in their manifest. Apps
+     * will still receive broadcasts if they register their
+     * {@link android.content.BroadcastReceiver} with
+     * {@link android.content.Context#registerReceiver Context.registerReceiver()}
+     * and that context is still valid.
+     * <p/>
+     * If this is a connection that was the result of failing over from a
+     * disconnected network, then the FAILOVER_CONNECTION boolean extra is
+     * set to true.
+     * <p/>
+     * For a loss of connectivity, if the connectivity manager is attempting
+     * to connect (or has already connected) to another network, the
+     * NetworkInfo for the new network is also passed as an extra. This lets
+     * any receivers of the broadcast know that they should not necessarily
+     * tell the user that no data traffic will be possible. Instead, the
+     * receiver should expect another broadcast soon, indicating either that
+     * the failover attempt succeeded (and so there is still overall data
+     * connectivity), or that the failover attempt failed, meaning that all
+     * connectivity has been lost.
+     * <p/>
+     * For a disconnect event, the boolean extra EXTRA_NO_CONNECTIVITY
+     * is set to {@code true} if there are no connected networks at all.
+     *
+     * @deprecated apps should use the more versatile {@link #requestNetwork},
+     *             {@link #registerNetworkCallback} or {@link #registerDefaultNetworkCallback}
+     *             functions instead for faster and more detailed updates about the network
+     *             changes they care about.
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    @Deprecated
+    public static final String CONNECTIVITY_ACTION = "android.net.conn.CONNECTIVITY_CHANGE";
+
+    /**
+     * A temporary hack until SUPL system can get off the legacy APIS.
+     * They do too many network requests and the long list of apps listening
+     * and waking due to the CONNECTIVITY_ACTION broadcast makes it expensive.
+     * Use this broadcast intent instead for SUPL requests.
+     * @hide
+     */
+    public static final String CONNECTIVITY_ACTION_SUPL =
+            "android.net.conn.CONNECTIVITY_CHANGE_SUPL";
+
+    /**
+     * The device has connected to a network that has presented a captive
+     * portal, which is blocking Internet connectivity. The user was presented
+     * with a notification that network sign in is required,
+     * and the user invoked the notification's action indicating they
+     * desire to sign in to the network. Apps handling this activity should
+     * facilitate signing in to the network. This action includes a
+     * {@link Network} typed extra called {@link #EXTRA_NETWORK} that represents
+     * the network presenting the captive portal; all communication with the
+     * captive portal must be done using this {@code Network} object.
+     * <p/>
+     * This activity includes a {@link CaptivePortal} extra named
+     * {@link #EXTRA_CAPTIVE_PORTAL} that can be used to indicate different
+     * outcomes of the captive portal sign in to the system:
+     * <ul>
+     * <li> When the app handling this action believes the user has signed in to
+     * the network and the captive portal has been dismissed, the app should
+     * call {@link CaptivePortal#reportCaptivePortalDismissed} so the system can
+     * reevaluate the network. If reevaluation finds the network no longer
+     * subject to a captive portal, the network may become the default active
+     * data network.</li>
+     * <li> When the app handling this action believes the user explicitly wants
+     * to ignore the captive portal and the network, the app should call
+     * {@link CaptivePortal#ignoreNetwork}. </li>
+     * </ul>
+     */
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_CAPTIVE_PORTAL_SIGN_IN = "android.net.conn.CAPTIVE_PORTAL";
+
+    /**
+     * The lookup key for a {@link NetworkInfo} object. Retrieve with
+     * {@link android.content.Intent#getParcelableExtra(String)}.
+     *
+     * @deprecated The {@link NetworkInfo} object is deprecated, as many of its properties
+     *             can't accurately represent modern network characteristics.
+     *             Please obtain information about networks from the {@link NetworkCapabilities}
+     *             or {@link LinkProperties} objects instead.
+     */
+    @Deprecated
+    public static final String EXTRA_NETWORK_INFO = "networkInfo";
+
+    /**
+     * Network type which triggered a {@link #CONNECTIVITY_ACTION} broadcast.
+     *
+     * @see android.content.Intent#getIntExtra(String, int)
+     * @deprecated The network type is not rich enough to represent the characteristics
+     *             of modern networks. Please use {@link NetworkCapabilities} instead,
+     *             in particular the transports.
+     */
+    @Deprecated
+    public static final String EXTRA_NETWORK_TYPE = "networkType";
+
+    /**
+     * The lookup key for a boolean that indicates whether a connect event
+     * is for a network to which the connectivity manager was failing over
+     * following a disconnect on another network.
+     * Retrieve it with {@link android.content.Intent#getBooleanExtra(String,boolean)}.
+     *
+     * @deprecated See {@link NetworkInfo}.
+     */
+    @Deprecated
+    public static final String EXTRA_IS_FAILOVER = "isFailover";
+    /**
+     * The lookup key for a {@link NetworkInfo} object. This is supplied when
+     * there is another network that it may be possible to connect to. Retrieve with
+     * {@link android.content.Intent#getParcelableExtra(String)}.
+     *
+     * @deprecated See {@link NetworkInfo}.
+     */
+    @Deprecated
+    public static final String EXTRA_OTHER_NETWORK_INFO = "otherNetwork";
+    /**
+     * The lookup key for a boolean that indicates whether there is a
+     * complete lack of connectivity, i.e., no network is available.
+     * Retrieve it with {@link android.content.Intent#getBooleanExtra(String,boolean)}.
+     */
+    public static final String EXTRA_NO_CONNECTIVITY = "noConnectivity";
+    /**
+     * The lookup key for a string that indicates why an attempt to connect
+     * to a network failed. The string has no particular structure. It is
+     * intended to be used in notifications presented to users. Retrieve
+     * it with {@link android.content.Intent#getStringExtra(String)}.
+     */
+    public static final String EXTRA_REASON = "reason";
+    /**
+     * The lookup key for a string that provides optionally supplied
+     * extra information about the network state. The information
+     * may be passed up from the lower networking layers, and its
+     * meaning may be specific to a particular network type. Retrieve
+     * it with {@link android.content.Intent#getStringExtra(String)}.
+     *
+     * @deprecated See {@link NetworkInfo#getExtraInfo()}.
+     */
+    @Deprecated
+    public static final String EXTRA_EXTRA_INFO = "extraInfo";
+    /**
+     * The lookup key for an int that provides information about
+     * our connection to the internet at large.  0 indicates no connection,
+     * 100 indicates a great connection.  Retrieve it with
+     * {@link android.content.Intent#getIntExtra(String, int)}.
+     * {@hide}
+     */
+    public static final String EXTRA_INET_CONDITION = "inetCondition";
+    /**
+     * The lookup key for a {@link CaptivePortal} object included with the
+     * {@link #ACTION_CAPTIVE_PORTAL_SIGN_IN} intent.  The {@code CaptivePortal}
+     * object can be used to either indicate to the system that the captive
+     * portal has been dismissed or that the user does not want to pursue
+     * signing in to captive portal.  Retrieve it with
+     * {@link android.content.Intent#getParcelableExtra(String)}.
+     */
+    public static final String EXTRA_CAPTIVE_PORTAL = "android.net.extra.CAPTIVE_PORTAL";
+
+    /**
+     * Key for passing a URL to the captive portal login activity.
+     */
+    public static final String EXTRA_CAPTIVE_PORTAL_URL = "android.net.extra.CAPTIVE_PORTAL_URL";
+
+    /**
+     * Key for passing a {@link android.net.captiveportal.CaptivePortalProbeSpec} to the captive
+     * portal login activity.
+     * {@hide}
+     */
+    @SystemApi
+    @TestApi
+    public static final String EXTRA_CAPTIVE_PORTAL_PROBE_SPEC =
+            "android.net.extra.CAPTIVE_PORTAL_PROBE_SPEC";
+
+    /**
+     * Key for passing a user agent string to the captive portal login activity.
+     * {@hide}
+     */
+    @SystemApi
+    @TestApi
+    public static final String EXTRA_CAPTIVE_PORTAL_USER_AGENT =
+            "android.net.extra.CAPTIVE_PORTAL_USER_AGENT";
+
+    /**
+     * Broadcast action to indicate the change of data activity status
+     * (idle or active) on a network in a recent period.
+     * The network becomes active when data transmission is started, or
+     * idle if there is no data transmission for a period of time.
+     * {@hide}
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String ACTION_DATA_ACTIVITY_CHANGE =
+            "android.net.conn.DATA_ACTIVITY_CHANGE";
+    /**
+     * The lookup key for an enum that indicates the network device type on which this data activity
+     * change happens.
+     * {@hide}
+     */
+    public static final String EXTRA_DEVICE_TYPE = "deviceType";
+    /**
+     * The lookup key for a boolean that indicates the device is active or not. {@code true} means
+     * it is actively sending or receiving data and {@code false} means it is idle.
+     * {@hide}
+     */
+    public static final String EXTRA_IS_ACTIVE = "isActive";
+    /**
+     * The lookup key for a long that contains the timestamp (nanos) of the radio state change.
+     * {@hide}
+     */
+    public static final String EXTRA_REALTIME_NS = "tsNanos";
+
+    /**
+     * Broadcast Action: The setting for background data usage has changed
+     * values. Use {@link #getBackgroundDataSetting()} to get the current value.
+     * <p>
+     * If an application uses the network in the background, it should listen
+     * for this broadcast and stop using the background data if the value is
+     * {@code false}.
+     * <p>
+     *
+     * @deprecated As of {@link VERSION_CODES#ICE_CREAM_SANDWICH}, availability
+     *             of background data depends on several combined factors, and
+     *             this broadcast is no longer sent. Instead, when background
+     *             data is unavailable, {@link #getActiveNetworkInfo()} will now
+     *             appear disconnected. During first boot after a platform
+     *             upgrade, this broadcast will be sent once if
+     *             {@link #getBackgroundDataSetting()} was {@code false} before
+     *             the upgrade.
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    @Deprecated
+    public static final String ACTION_BACKGROUND_DATA_SETTING_CHANGED =
+            "android.net.conn.BACKGROUND_DATA_SETTING_CHANGED";
+
+    /**
+     * Broadcast Action: The network connection may not be good
+     * uses {@code ConnectivityManager.EXTRA_INET_CONDITION} and
+     * {@code ConnectivityManager.EXTRA_NETWORK_INFO} to specify
+     * the network and it's condition.
+     * @hide
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    @UnsupportedAppUsage
+    public static final String INET_CONDITION_ACTION =
+            "android.net.conn.INET_CONDITION_ACTION";
+
+    /**
+     * Broadcast Action: A tetherable connection has come or gone.
+     * Uses {@code ConnectivityManager.EXTRA_AVAILABLE_TETHER},
+     * {@code ConnectivityManager.EXTRA_ACTIVE_LOCAL_ONLY},
+     * {@code ConnectivityManager.EXTRA_ACTIVE_TETHER}, and
+     * {@code ConnectivityManager.EXTRA_ERRORED_TETHER} to indicate
+     * the current state of tethering.  Each include a list of
+     * interface names in that state (may be empty).
+     * @hide
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    @UnsupportedAppUsage
+    public static final String ACTION_TETHER_STATE_CHANGED =
+            "android.net.conn.TETHER_STATE_CHANGED";
+
+    /**
+     * @hide
+     * gives a String[] listing all the interfaces configured for
+     * tethering and currently available for tethering.
+     */
+    @UnsupportedAppUsage
+    public static final String EXTRA_AVAILABLE_TETHER = "availableArray";
+
+    /**
+     * @hide
+     * gives a String[] listing all the interfaces currently in local-only
+     * mode (ie, has DHCPv4+IPv6-ULA support and no packet forwarding)
+     */
+    public static final String EXTRA_ACTIVE_LOCAL_ONLY = "localOnlyArray";
+
+    /**
+     * @hide
+     * gives a String[] listing all the interfaces currently tethered
+     * (ie, has DHCPv4 support and packets potentially forwarded/NATed)
+     */
+    @UnsupportedAppUsage
+    public static final String EXTRA_ACTIVE_TETHER = "tetherArray";
+
+    /**
+     * @hide
+     * gives a String[] listing all the interfaces we tried to tether and
+     * failed.  Use {@link #getLastTetherError} to find the error code
+     * for any interfaces listed here.
+     */
+    @UnsupportedAppUsage
+    public static final String EXTRA_ERRORED_TETHER = "erroredArray";
+
+    /**
+     * Broadcast Action: The captive portal tracker has finished its test.
+     * Sent only while running Setup Wizard, in lieu of showing a user
+     * notification.
+     * @hide
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String ACTION_CAPTIVE_PORTAL_TEST_COMPLETED =
+            "android.net.conn.CAPTIVE_PORTAL_TEST_COMPLETED";
+    /**
+     * The lookup key for a boolean that indicates whether a captive portal was detected.
+     * Retrieve it with {@link android.content.Intent#getBooleanExtra(String,boolean)}.
+     * @hide
+     */
+    public static final String EXTRA_IS_CAPTIVE_PORTAL = "captivePortal";
+
+    /**
+     * Action used to display a dialog that asks the user whether to connect to a network that is
+     * not validated. This intent is used to start the dialog in settings via startActivity.
+     *
+     * @hide
+     */
+    public static final String ACTION_PROMPT_UNVALIDATED = "android.net.conn.PROMPT_UNVALIDATED";
+
+    /**
+     * Action used to display a dialog that asks the user whether to avoid a network that is no
+     * longer validated. This intent is used to start the dialog in settings via startActivity.
+     *
+     * @hide
+     */
+    public static final String ACTION_PROMPT_LOST_VALIDATION =
+            "android.net.conn.PROMPT_LOST_VALIDATION";
+
+    /**
+     * Action used to display a dialog that asks the user whether to stay connected to a network
+     * that has not validated. This intent is used to start the dialog in settings via
+     * startActivity.
+     *
+     * @hide
+     */
+    public static final String ACTION_PROMPT_PARTIAL_CONNECTIVITY =
+            "android.net.conn.PROMPT_PARTIAL_CONNECTIVITY";
+
+    /**
+     * Invalid tethering type.
+     * @see #startTethering(int, boolean, OnStartTetheringCallback)
+     * @hide
+     */
+    public static final int TETHERING_INVALID   = -1;
+
+    /**
+     * Wifi tethering type.
+     * @see #startTethering(int, boolean, OnStartTetheringCallback)
+     * @hide
+     */
+    @SystemApi
+    public static final int TETHERING_WIFI      = 0;
+
+    /**
+     * USB tethering type.
+     * @see #startTethering(int, boolean, OnStartTetheringCallback)
+     * @hide
+     */
+    @SystemApi
+    public static final int TETHERING_USB       = 1;
+
+    /**
+     * Bluetooth tethering type.
+     * @see #startTethering(int, boolean, OnStartTetheringCallback)
+     * @hide
+     */
+    @SystemApi
+    public static final int TETHERING_BLUETOOTH = 2;
+
+    /**
+     * Extra used for communicating with the TetherService. Includes the type of tethering to
+     * enable if any.
+     * @hide
+     */
+    public static final String EXTRA_ADD_TETHER_TYPE = "extraAddTetherType";
+
+    /**
+     * Extra used for communicating with the TetherService. Includes the type of tethering for
+     * which to cancel provisioning.
+     * @hide
+     */
+    public static final String EXTRA_REM_TETHER_TYPE = "extraRemTetherType";
+
+    /**
+     * Extra used for communicating with the TetherService. True to schedule a recheck of tether
+     * provisioning.
+     * @hide
+     */
+    public static final String EXTRA_SET_ALARM = "extraSetAlarm";
+
+    /**
+     * Tells the TetherService to run a provision check now.
+     * @hide
+     */
+    public static final String EXTRA_RUN_PROVISION = "extraRunProvision";
+
+    /**
+     * Extra used for communicating with the TetherService. Contains the {@link ResultReceiver}
+     * which will receive provisioning results. Can be left empty.
+     * @hide
+     */
+    public static final String EXTRA_PROVISION_CALLBACK = "extraProvisionCallback";
+
+    /**
+     * The absence of a connection type.
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562)
+    public static final int TYPE_NONE        = -1;
+
+    /**
+     * A Mobile data connection. Devices may support more than one.
+     *
+     * @deprecated Applications should instead use {@link NetworkCapabilities#hasTransport} or
+     *         {@link #requestNetwork(NetworkRequest, NetworkCallback)} to request an
+     *         appropriate network. {@see NetworkCapabilities} for supported transports.
+     */
+    @Deprecated
+    public static final int TYPE_MOBILE      = 0;
+
+    /**
+     * A WIFI data connection. Devices may support more than one.
+     *
+     * @deprecated Applications should instead use {@link NetworkCapabilities#hasTransport} or
+     *         {@link #requestNetwork(NetworkRequest, NetworkCallback)} to request an
+     *         appropriate network. {@see NetworkCapabilities} for supported transports.
+     */
+    @Deprecated
+    public static final int TYPE_WIFI        = 1;
+
+    /**
+     * An MMS-specific Mobile data connection.  This network type may use the
+     * same network interface as {@link #TYPE_MOBILE} or it may use a different
+     * one.  This is used by applications needing to talk to the carrier's
+     * Multimedia Messaging Service servers.
+     *
+     * @deprecated Applications should instead use {@link NetworkCapabilities#hasCapability} or
+     *         {@link #requestNetwork(NetworkRequest, NetworkCallback)} to request a network that
+     *         provides the {@link NetworkCapabilities#NET_CAPABILITY_MMS} capability.
+     */
+    @Deprecated
+    public static final int TYPE_MOBILE_MMS  = 2;
+
+    /**
+     * A SUPL-specific Mobile data connection.  This network type may use the
+     * same network interface as {@link #TYPE_MOBILE} or it may use a different
+     * one.  This is used by applications needing to talk to the carrier's
+     * Secure User Plane Location servers for help locating the device.
+     *
+     * @deprecated Applications should instead use {@link NetworkCapabilities#hasCapability} or
+     *         {@link #requestNetwork(NetworkRequest, NetworkCallback)} to request a network that
+     *         provides the {@link NetworkCapabilities#NET_CAPABILITY_SUPL} capability.
+     */
+    @Deprecated
+    public static final int TYPE_MOBILE_SUPL = 3;
+
+    /**
+     * A DUN-specific Mobile data connection.  This network type may use the
+     * same network interface as {@link #TYPE_MOBILE} or it may use a different
+     * one.  This is sometimes by the system when setting up an upstream connection
+     * for tethering so that the carrier is aware of DUN traffic.
+     *
+     * @deprecated Applications should instead use {@link NetworkCapabilities#hasCapability} or
+     *         {@link #requestNetwork(NetworkRequest, NetworkCallback)} to request a network that
+     *         provides the {@link NetworkCapabilities#NET_CAPABILITY_DUN} capability.
+     */
+    @Deprecated
+    public static final int TYPE_MOBILE_DUN  = 4;
+
+    /**
+     * A High Priority Mobile data connection.  This network type uses the
+     * same network interface as {@link #TYPE_MOBILE} but the routing setup
+     * is different.
+     *
+     * @deprecated Applications should instead use {@link NetworkCapabilities#hasTransport} or
+     *         {@link #requestNetwork(NetworkRequest, NetworkCallback)} to request an
+     *         appropriate network. {@see NetworkCapabilities} for supported transports.
+     */
+    @Deprecated
+    public static final int TYPE_MOBILE_HIPRI = 5;
+
+    /**
+     * A WiMAX data connection.
+     *
+     * @deprecated Applications should instead use {@link NetworkCapabilities#hasTransport} or
+     *         {@link #requestNetwork(NetworkRequest, NetworkCallback)} to request an
+     *         appropriate network. {@see NetworkCapabilities} for supported transports.
+     */
+    @Deprecated
+    public static final int TYPE_WIMAX       = 6;
+
+    /**
+     * A Bluetooth data connection.
+     *
+     * @deprecated Applications should instead use {@link NetworkCapabilities#hasTransport} or
+     *         {@link #requestNetwork(NetworkRequest, NetworkCallback)} to request an
+     *         appropriate network. {@see NetworkCapabilities} for supported transports.
+     */
+    @Deprecated
+    public static final int TYPE_BLUETOOTH   = 7;
+
+    /**
+     * Dummy data connection.  This should not be used on shipping devices.
+     * @deprecated This is not used any more.
+     */
+    @Deprecated
+    public static final int TYPE_DUMMY       = 8;
+
+    /**
+     * An Ethernet data connection.
+     *
+     * @deprecated Applications should instead use {@link NetworkCapabilities#hasTransport} or
+     *         {@link #requestNetwork(NetworkRequest, NetworkCallback)} to request an
+     *         appropriate network. {@see NetworkCapabilities} for supported transports.
+     */
+    @Deprecated
+    public static final int TYPE_ETHERNET    = 9;
+
+    /**
+     * Over the air Administration.
+     * @deprecated Use {@link NetworkCapabilities} instead.
+     * {@hide}
+     */
+    @Deprecated
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562)
+    public static final int TYPE_MOBILE_FOTA = 10;
+
+    /**
+     * IP Multimedia Subsystem.
+     * @deprecated Use {@link NetworkCapabilities#NET_CAPABILITY_IMS} instead.
+     * {@hide}
+     */
+    @Deprecated
+    @UnsupportedAppUsage
+    public static final int TYPE_MOBILE_IMS  = 11;
+
+    /**
+     * Carrier Branded Services.
+     * @deprecated Use {@link NetworkCapabilities#NET_CAPABILITY_CBS} instead.
+     * {@hide}
+     */
+    @Deprecated
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562)
+    public static final int TYPE_MOBILE_CBS  = 12;
+
+    /**
+     * A Wi-Fi p2p connection. Only requesting processes will have access to
+     * the peers connected.
+     * @deprecated Use {@link NetworkCapabilities#NET_CAPABILITY_WIFI_P2P} instead.
+     * {@hide}
+     */
+    @Deprecated
+    @UnsupportedAppUsage
+    public static final int TYPE_WIFI_P2P    = 13;
+
+    /**
+     * The network to use for initially attaching to the network
+     * @deprecated Use {@link NetworkCapabilities#NET_CAPABILITY_IA} instead.
+     * {@hide}
+     */
+    @Deprecated
+    @UnsupportedAppUsage
+    public static final int TYPE_MOBILE_IA = 14;
+
+    /**
+     * Emergency PDN connection for emergency services.  This
+     * may include IMS and MMS in emergency situations.
+     * @deprecated Use {@link NetworkCapabilities#NET_CAPABILITY_EIMS} instead.
+     * {@hide}
+     */
+    @Deprecated
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562)
+    public static final int TYPE_MOBILE_EMERGENCY = 15;
+
+    /**
+     * The network that uses proxy to achieve connectivity.
+     * @deprecated Use {@link NetworkCapabilities} instead.
+     * {@hide}
+     */
+    @Deprecated
+    @UnsupportedAppUsage
+    public static final int TYPE_PROXY = 16;
+
+    /**
+     * A virtual network using one or more native bearers.
+     * It may or may not be providing security services.
+     * @deprecated Applications should use {@link NetworkCapabilities#TRANSPORT_VPN} instead.
+     */
+    @Deprecated
+    public static final int TYPE_VPN = 17;
+
+    /**
+     * A network that is exclusively meant to be used for testing
+     *
+     * @deprecated Use {@link NetworkCapabilities} instead.
+     * @hide
+     */
+    @Deprecated
+    public static final int TYPE_TEST = 18; // TODO: Remove this once NetworkTypes are unused.
+
+    /** {@hide} */
+    public static final int MAX_RADIO_TYPE = TYPE_TEST;
+
+    /** {@hide} */
+    public static final int MAX_NETWORK_TYPE = TYPE_TEST;
+
+    private static final int MIN_NETWORK_TYPE = TYPE_MOBILE;
+
+    /**
+     * If you want to set the default network preference,you can directly
+     * change the networkAttributes array in framework's config.xml.
+     *
+     * @deprecated Since we support so many more networks now, the single
+     *             network default network preference can't really express
+     *             the hierarchy.  Instead, the default is defined by the
+     *             networkAttributes in config.xml.  You can determine
+     *             the current value by calling {@link #getNetworkPreference()}
+     *             from an App.
+     */
+    @Deprecated
+    public static final int DEFAULT_NETWORK_PREFERENCE = TYPE_WIFI;
+
+    /**
+     * @hide
+     */
+    public static final int REQUEST_ID_UNSET = 0;
+
+    /**
+     * Static unique request used as a tombstone for NetworkCallbacks that have been unregistered.
+     * This allows to distinguish when unregistering NetworkCallbacks those that were never
+     * registered from those that were already unregistered.
+     * @hide
+     */
+    private static final NetworkRequest ALREADY_UNREGISTERED =
+            new NetworkRequest.Builder().clearCapabilities().build();
+
+    /**
+     * A NetID indicating no Network is selected.
+     * Keep in sync with bionic/libc/dns/include/resolv_netid.h
+     * @hide
+     */
+    public static final int NETID_UNSET = 0;
+
+    /**
+     * Private DNS Mode values.
+     *
+     * The "private_dns_mode" global setting stores a String value which is
+     * expected to be one of the following.
+     */
+
+    /**
+     * @hide
+     */
+    public static final String PRIVATE_DNS_MODE_OFF = "off";
+    /**
+     * @hide
+     */
+    public static final String PRIVATE_DNS_MODE_OPPORTUNISTIC = "opportunistic";
+    /**
+     * @hide
+     */
+    public static final String PRIVATE_DNS_MODE_PROVIDER_HOSTNAME = "hostname";
+    /**
+     * The default Private DNS mode.
+     *
+     * This may change from release to release or may become dependent upon
+     * the capabilities of the underlying platform.
+     *
+     * @hide
+     */
+    public static final String PRIVATE_DNS_DEFAULT_MODE_FALLBACK = PRIVATE_DNS_MODE_OPPORTUNISTIC;
+
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562)
+    private final IConnectivityManager mService;
+    /**
+     * A kludge to facilitate static access where a Context pointer isn't available, like in the
+     * case of the static set/getProcessDefaultNetwork methods and from the Network class.
+     * TODO: Remove this after deprecating the static methods in favor of non-static methods or
+     * methods that take a Context argument.
+     */
+    private static ConnectivityManager sInstance;
+
+    private final Context mContext;
+
+    private INetworkManagementService mNMService;
+    private INetworkPolicyManager mNPManager;
+
+    /**
+     * Tests if a given integer represents a valid network type.
+     * @param networkType the type to be tested
+     * @return a boolean.  {@code true} if the type is valid, else {@code false}
+     * @deprecated All APIs accepting a network type are deprecated. There should be no need to
+     *             validate a network type.
+     */
+    @Deprecated
+    public static boolean isNetworkTypeValid(int networkType) {
+        return MIN_NETWORK_TYPE <= networkType && networkType <= MAX_NETWORK_TYPE;
+    }
+
+    /**
+     * Returns a non-localized string representing a given network type.
+     * ONLY used for debugging output.
+     * @param type the type needing naming
+     * @return a String for the given type, or a string version of the type ("87")
+     * if no name is known.
+     * @deprecated Types are deprecated. Use {@link NetworkCapabilities} instead.
+     * {@hide}
+     */
+    @Deprecated
+    @UnsupportedAppUsage
+    public static String getNetworkTypeName(int type) {
+        switch (type) {
+          case TYPE_NONE:
+                return "NONE";
+            case TYPE_MOBILE:
+                return "MOBILE";
+            case TYPE_WIFI:
+                return "WIFI";
+            case TYPE_MOBILE_MMS:
+                return "MOBILE_MMS";
+            case TYPE_MOBILE_SUPL:
+                return "MOBILE_SUPL";
+            case TYPE_MOBILE_DUN:
+                return "MOBILE_DUN";
+            case TYPE_MOBILE_HIPRI:
+                return "MOBILE_HIPRI";
+            case TYPE_WIMAX:
+                return "WIMAX";
+            case TYPE_BLUETOOTH:
+                return "BLUETOOTH";
+            case TYPE_DUMMY:
+                return "DUMMY";
+            case TYPE_ETHERNET:
+                return "ETHERNET";
+            case TYPE_MOBILE_FOTA:
+                return "MOBILE_FOTA";
+            case TYPE_MOBILE_IMS:
+                return "MOBILE_IMS";
+            case TYPE_MOBILE_CBS:
+                return "MOBILE_CBS";
+            case TYPE_WIFI_P2P:
+                return "WIFI_P2P";
+            case TYPE_MOBILE_IA:
+                return "MOBILE_IA";
+            case TYPE_MOBILE_EMERGENCY:
+                return "MOBILE_EMERGENCY";
+            case TYPE_PROXY:
+                return "PROXY";
+            case TYPE_VPN:
+                return "VPN";
+            default:
+                return Integer.toString(type);
+        }
+    }
+
+    /**
+     * Checks if a given type uses the cellular data connection.
+     * This should be replaced in the future by a network property.
+     * @param networkType the type to check
+     * @return a boolean - {@code true} if uses cellular network, else {@code false}
+     * @deprecated Types are deprecated. Use {@link NetworkCapabilities} instead.
+     * {@hide}
+     */
+    @Deprecated
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562)
+    public static boolean isNetworkTypeMobile(int networkType) {
+        switch (networkType) {
+            case TYPE_MOBILE:
+            case TYPE_MOBILE_MMS:
+            case TYPE_MOBILE_SUPL:
+            case TYPE_MOBILE_DUN:
+            case TYPE_MOBILE_HIPRI:
+            case TYPE_MOBILE_FOTA:
+            case TYPE_MOBILE_IMS:
+            case TYPE_MOBILE_CBS:
+            case TYPE_MOBILE_IA:
+            case TYPE_MOBILE_EMERGENCY:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    /**
+     * Checks if the given network type is backed by a Wi-Fi radio.
+     *
+     * @deprecated Types are deprecated. Use {@link NetworkCapabilities} instead.
+     * @hide
+     */
+    @Deprecated
+    public static boolean isNetworkTypeWifi(int networkType) {
+        switch (networkType) {
+            case TYPE_WIFI:
+            case TYPE_WIFI_P2P:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    /**
+     * Specifies the preferred network type.  When the device has more
+     * than one type available the preferred network type will be used.
+     *
+     * @param preference the network type to prefer over all others.  It is
+     *         unspecified what happens to the old preferred network in the
+     *         overall ordering.
+     * @deprecated Functionality has been removed as it no longer makes sense,
+     *             with many more than two networks - we'd need an array to express
+     *             preference.  Instead we use dynamic network properties of
+     *             the networks to describe their precedence.
+     */
+    @Deprecated
+    public void setNetworkPreference(int preference) {
+    }
+
+    /**
+     * Retrieves the current preferred network type.
+     *
+     * @return an integer representing the preferred network type
+     *
+     * @deprecated Functionality has been removed as it no longer makes sense,
+     *             with many more than two networks - we'd need an array to express
+     *             preference.  Instead we use dynamic network properties of
+     *             the networks to describe their precedence.
+     */
+    @Deprecated
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    public int getNetworkPreference() {
+        return TYPE_NONE;
+    }
+
+    /**
+     * Returns details about the currently active default data network. When
+     * connected, this network is the default route for outgoing connections.
+     * You should always check {@link NetworkInfo#isConnected()} before initiating
+     * network traffic. This may return {@code null} when there is no default
+     * network.
+     * Note that if the default network is a VPN, this method will return the
+     * NetworkInfo for one of its underlying networks instead, or null if the
+     * VPN agent did not specify any. Apps interested in learning about VPNs
+     * should use {@link #getNetworkInfo(android.net.Network)} instead.
+     *
+     * @return a {@link NetworkInfo} object for the current default network
+     *        or {@code null} if no default network is currently active
+     * @deprecated See {@link NetworkInfo}.
+     */
+    @Deprecated
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    @Nullable
+    public NetworkInfo getActiveNetworkInfo() {
+        try {
+            return mService.getActiveNetworkInfo();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns a {@link Network} object corresponding to the currently active
+     * default data network.  In the event that the current active default data
+     * network disconnects, the returned {@code Network} object will no longer
+     * be usable.  This will return {@code null} when there is no default
+     * network.
+     *
+     * @return a {@link Network} object for the current default network or
+     *        {@code null} if no default network is currently active
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    @Nullable
+    public Network getActiveNetwork() {
+        try {
+            return mService.getActiveNetwork();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns a {@link Network} object corresponding to the currently active
+     * default data network for a specific UID.  In the event that the default data
+     * network disconnects, the returned {@code Network} object will no longer
+     * be usable.  This will return {@code null} when there is no default
+     * network for the UID.
+     *
+     * @return a {@link Network} object for the current default network for the
+     *         given UID or {@code null} if no default network is currently active
+     *
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.CONNECTIVITY_INTERNAL)
+    @Nullable
+    public Network getActiveNetworkForUid(int uid) {
+        return getActiveNetworkForUid(uid, false);
+    }
+
+    /** {@hide} */
+    public Network getActiveNetworkForUid(int uid, boolean ignoreBlocked) {
+        try {
+            return mService.getActiveNetworkForUid(uid, ignoreBlocked);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Checks if a VPN app supports always-on mode.
+     *
+     * In order to support the always-on feature, an app has to
+     * <ul>
+     *     <li>target {@link VERSION_CODES#N API 24} or above, and
+     *     <li>not opt out through the {@link VpnService#SERVICE_META_DATA_SUPPORTS_ALWAYS_ON}
+     *         meta-data field.
+     * </ul>
+     *
+     * @param userId The identifier of the user for whom the VPN app is installed.
+     * @param vpnPackage The canonical package name of the VPN app.
+     * @return {@code true} if and only if the VPN app exists and supports always-on mode.
+     * @hide
+     */
+    public boolean isAlwaysOnVpnPackageSupportedForUser(int userId, @Nullable String vpnPackage) {
+        try {
+            return mService.isAlwaysOnVpnPackageSupported(userId, vpnPackage);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Configures an always-on VPN connection through a specific application.
+     * This connection is automatically granted and persisted after a reboot.
+     *
+     * <p>The designated package should declare a {@link VpnService} in its
+     *    manifest guarded by {@link android.Manifest.permission.BIND_VPN_SERVICE},
+     *    otherwise the call will fail.
+     *
+     * @param userId The identifier of the user to set an always-on VPN for.
+     * @param vpnPackage The package name for an installed VPN app on the device, or {@code null}
+     *                   to remove an existing always-on VPN configuration.
+     * @param lockdownEnabled {@code true} to disallow networking when the VPN is not connected or
+     *        {@code false} otherwise.
+     * @param lockdownWhitelist The list of packages that are allowed to access network directly
+     *         when VPN is in lockdown mode but is not running. Non-existent packages are ignored so
+     *         this method must be called when a package that should be whitelisted is installed or
+     *         uninstalled.
+     * @return {@code true} if the package is set as always-on VPN controller;
+     *         {@code false} otherwise.
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.CONTROL_ALWAYS_ON_VPN)
+    public boolean setAlwaysOnVpnPackageForUser(int userId, @Nullable String vpnPackage,
+            boolean lockdownEnabled, @Nullable List<String> lockdownWhitelist) {
+        try {
+            return mService.setAlwaysOnVpnPackage(
+                    userId, vpnPackage, lockdownEnabled, lockdownWhitelist);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+   /**
+     * Returns the package name of the currently set always-on VPN application.
+     * If there is no always-on VPN set, or the VPN is provided by the system instead
+     * of by an app, {@code null} will be returned.
+     *
+     * @return Package name of VPN controller responsible for always-on VPN,
+     *         or {@code null} if none is set.
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.CONTROL_ALWAYS_ON_VPN)
+    public String getAlwaysOnVpnPackageForUser(int userId) {
+        try {
+            return mService.getAlwaysOnVpnPackage(userId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * @return whether always-on VPN is in lockdown mode.
+     *
+     * @hide
+     **/
+    @RequiresPermission(android.Manifest.permission.CONTROL_ALWAYS_ON_VPN)
+    public boolean isVpnLockdownEnabled(int userId) {
+        try {
+            return mService.isVpnLockdownEnabled(userId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+
+    }
+
+    /**
+     * @return the list of packages that are allowed to access network when always-on VPN is in
+     * lockdown mode but not connected. Returns {@code null} when VPN lockdown is not active.
+     *
+     * @hide
+     **/
+    @RequiresPermission(android.Manifest.permission.CONTROL_ALWAYS_ON_VPN)
+    public List<String> getVpnLockdownWhitelist(int userId) {
+        try {
+            return mService.getVpnLockdownWhitelist(userId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns details about the currently active default data network
+     * for a given uid.  This is for internal use only to avoid spying
+     * other apps.
+     *
+     * @return a {@link NetworkInfo} object for the current default network
+     *        for the given uid or {@code null} if no default network is
+     *        available for the specified uid.
+     *
+     * {@hide}
+     */
+    @RequiresPermission(android.Manifest.permission.CONNECTIVITY_INTERNAL)
+    @UnsupportedAppUsage
+    public NetworkInfo getActiveNetworkInfoForUid(int uid) {
+        return getActiveNetworkInfoForUid(uid, false);
+    }
+
+    /** {@hide} */
+    public NetworkInfo getActiveNetworkInfoForUid(int uid, boolean ignoreBlocked) {
+        try {
+            return mService.getActiveNetworkInfoForUid(uid, ignoreBlocked);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns connection status information about a particular
+     * network type.
+     *
+     * @param networkType integer specifying which networkType in
+     *        which you're interested.
+     * @return a {@link NetworkInfo} object for the requested
+     *        network type or {@code null} if the type is not
+     *        supported by the device. If {@code networkType} is
+     *        TYPE_VPN and a VPN is active for the calling app,
+     *        then this method will try to return one of the
+     *        underlying networks for the VPN or null if the
+     *        VPN agent didn't specify any.
+     *
+     * @deprecated This method does not support multiple connected networks
+     *             of the same type. Use {@link #getAllNetworks} and
+     *             {@link #getNetworkInfo(android.net.Network)} instead.
+     */
+    @Deprecated
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    @Nullable
+    public NetworkInfo getNetworkInfo(int networkType) {
+        try {
+            return mService.getNetworkInfo(networkType);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns connection status information about a particular
+     * Network.
+     *
+     * @param network {@link Network} specifying which network
+     *        in which you're interested.
+     * @return a {@link NetworkInfo} object for the requested
+     *        network or {@code null} if the {@code Network}
+     *        is not valid.
+     * @deprecated See {@link NetworkInfo}.
+     */
+    @Deprecated
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    @Nullable
+    public NetworkInfo getNetworkInfo(@Nullable Network network) {
+        return getNetworkInfoForUid(network, Process.myUid(), false);
+    }
+
+    /** {@hide} */
+    public NetworkInfo getNetworkInfoForUid(Network network, int uid, boolean ignoreBlocked) {
+        try {
+            return mService.getNetworkInfoForUid(network, uid, ignoreBlocked);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns connection status information about all network
+     * types supported by the device.
+     *
+     * @return an array of {@link NetworkInfo} objects.  Check each
+     * {@link NetworkInfo#getType} for which type each applies.
+     *
+     * @deprecated This method does not support multiple connected networks
+     *             of the same type. Use {@link #getAllNetworks} and
+     *             {@link #getNetworkInfo(android.net.Network)} instead.
+     */
+    @Deprecated
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    @NonNull
+    public NetworkInfo[] getAllNetworkInfo() {
+        try {
+            return mService.getAllNetworkInfo();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns the {@link Network} object currently serving a given type, or
+     * null if the given type is not connected.
+     *
+     * @hide
+     * @deprecated This method does not support multiple connected networks
+     *             of the same type. Use {@link #getAllNetworks} and
+     *             {@link #getNetworkInfo(android.net.Network)} instead.
+     */
+    @Deprecated
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    @UnsupportedAppUsage
+    public Network getNetworkForType(int networkType) {
+        try {
+            return mService.getNetworkForType(networkType);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns an array of all {@link Network} currently tracked by the
+     * framework.
+     *
+     * @return an array of {@link Network} objects.
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    @NonNull
+    public Network[] getAllNetworks() {
+        try {
+            return mService.getAllNetworks();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns an array of {@link android.net.NetworkCapabilities} objects, representing
+     * the Networks that applications run by the given user will use by default.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public NetworkCapabilities[] getDefaultNetworkCapabilitiesForUser(int userId) {
+        try {
+            return mService.getDefaultNetworkCapabilitiesForUser(userId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns the IP information for the current default network.
+     *
+     * @return a {@link LinkProperties} object describing the IP info
+     *        for the current default network, or {@code null} if there
+     *        is no current default network.
+     *
+     * {@hide}
+     * @deprecated please use {@link #getLinkProperties(Network)} on the return
+     *             value of {@link #getActiveNetwork()} instead. In particular,
+     *             this method will return non-null LinkProperties even if the
+     *             app is blocked by policy from using this network.
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 109783091)
+    public LinkProperties getActiveLinkProperties() {
+        try {
+            return mService.getActiveLinkProperties();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns the IP information for a given network type.
+     *
+     * @param networkType the network type of interest.
+     * @return a {@link LinkProperties} object describing the IP info
+     *        for the given networkType, or {@code null} if there is
+     *        no current default network.
+     *
+     * {@hide}
+     * @deprecated This method does not support multiple connected networks
+     *             of the same type. Use {@link #getAllNetworks},
+     *             {@link #getNetworkInfo(android.net.Network)}, and
+     *             {@link #getLinkProperties(android.net.Network)} instead.
+     */
+    @Deprecated
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562)
+    public LinkProperties getLinkProperties(int networkType) {
+        try {
+            return mService.getLinkPropertiesForType(networkType);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Get the {@link LinkProperties} for the given {@link Network}.  This
+     * will return {@code null} if the network is unknown.
+     *
+     * @param network The {@link Network} object identifying the network in question.
+     * @return The {@link LinkProperties} for the network, or {@code null}.
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    @Nullable
+    public LinkProperties getLinkProperties(@Nullable Network network) {
+        try {
+            return mService.getLinkProperties(network);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Get the {@link android.net.NetworkCapabilities} for the given {@link Network}.  This
+     * will return {@code null} if the network is unknown.
+     *
+     * @param network The {@link Network} object identifying the network in question.
+     * @return The {@link android.net.NetworkCapabilities} for the network, or {@code null}.
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    @Nullable
+    public NetworkCapabilities getNetworkCapabilities(@Nullable Network network) {
+        try {
+            return mService.getNetworkCapabilities(network);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Gets a URL that can be used for resolving whether a captive portal is present.
+     * 1. This URL should respond with a 204 response to a GET request to indicate no captive
+     *    portal is present.
+     * 2. This URL must be HTTP as redirect responses are used to find captive portal
+     *    sign-in pages. Captive portals cannot respond to HTTPS requests with redirects.
+     *
+     * The system network validation may be using different strategies to detect captive portals,
+     * so this method does not necessarily return a URL used by the system. It only returns a URL
+     * that may be relevant for other components trying to detect captive portals.
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.LOCAL_MAC_ADDRESS)
+    public String getCaptivePortalServerUrl() {
+        try {
+            return mService.getCaptivePortalServerUrl();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Tells the underlying networking system that the caller wants to
+     * begin using the named feature. The interpretation of {@code feature}
+     * is completely up to each networking implementation.
+     *
+     * <p>This method requires the caller to hold either the
+     * {@link android.Manifest.permission#CHANGE_NETWORK_STATE} permission
+     * or the ability to modify system settings as determined by
+     * {@link android.provider.Settings.System#canWrite}.</p>
+     *
+     * @param networkType specifies which network the request pertains to
+     * @param feature the name of the feature to be used
+     * @return an integer value representing the outcome of the request.
+     * The interpretation of this value is specific to each networking
+     * implementation+feature combination, except that the value {@code -1}
+     * always indicates failure.
+     *
+     * @deprecated Deprecated in favor of the cleaner
+     *             {@link #requestNetwork(NetworkRequest, NetworkCallback)} API.
+     *             In {@link VERSION_CODES#M}, and above, this method is unsupported and will
+     *             throw {@code UnsupportedOperationException} if called.
+     * @removed
+     */
+    @Deprecated
+    public int startUsingNetworkFeature(int networkType, String feature) {
+        checkLegacyRoutingApiAccess();
+        NetworkCapabilities netCap = networkCapabilitiesForFeature(networkType, feature);
+        if (netCap == null) {
+            Log.d(TAG, "Can't satisfy startUsingNetworkFeature for " + networkType + ", " +
+                    feature);
+            return PhoneConstants.APN_REQUEST_FAILED;
+        }
+
+        NetworkRequest request = null;
+        synchronized (sLegacyRequests) {
+            LegacyRequest l = sLegacyRequests.get(netCap);
+            if (l != null) {
+                Log.d(TAG, "renewing startUsingNetworkFeature request " + l.networkRequest);
+                renewRequestLocked(l);
+                if (l.currentNetwork != null) {
+                    return PhoneConstants.APN_ALREADY_ACTIVE;
+                } else {
+                    return PhoneConstants.APN_REQUEST_STARTED;
+                }
+            }
+
+            request = requestNetworkForFeatureLocked(netCap);
+        }
+        if (request != null) {
+            Log.d(TAG, "starting startUsingNetworkFeature for request " + request);
+            return PhoneConstants.APN_REQUEST_STARTED;
+        } else {
+            Log.d(TAG, " request Failed");
+            return PhoneConstants.APN_REQUEST_FAILED;
+        }
+    }
+
+    /**
+     * Tells the underlying networking system that the caller is finished
+     * using the named feature. The interpretation of {@code feature}
+     * is completely up to each networking implementation.
+     *
+     * <p>This method requires the caller to hold either the
+     * {@link android.Manifest.permission#CHANGE_NETWORK_STATE} permission
+     * or the ability to modify system settings as determined by
+     * {@link android.provider.Settings.System#canWrite}.</p>
+     *
+     * @param networkType specifies which network the request pertains to
+     * @param feature the name of the feature that is no longer needed
+     * @return an integer value representing the outcome of the request.
+     * The interpretation of this value is specific to each networking
+     * implementation+feature combination, except that the value {@code -1}
+     * always indicates failure.
+     *
+     * @deprecated Deprecated in favor of the cleaner
+     *             {@link #unregisterNetworkCallback(NetworkCallback)} API.
+     *             In {@link VERSION_CODES#M}, and above, this method is unsupported and will
+     *             throw {@code UnsupportedOperationException} if called.
+     * @removed
+     */
+    @Deprecated
+    public int stopUsingNetworkFeature(int networkType, String feature) {
+        checkLegacyRoutingApiAccess();
+        NetworkCapabilities netCap = networkCapabilitiesForFeature(networkType, feature);
+        if (netCap == null) {
+            Log.d(TAG, "Can't satisfy stopUsingNetworkFeature for " + networkType + ", " +
+                    feature);
+            return -1;
+        }
+
+        if (removeRequestForFeature(netCap)) {
+            Log.d(TAG, "stopUsingNetworkFeature for " + networkType + ", " + feature);
+        }
+        return 1;
+    }
+
+    @UnsupportedAppUsage
+    private NetworkCapabilities networkCapabilitiesForFeature(int networkType, String feature) {
+        if (networkType == TYPE_MOBILE) {
+            switch (feature) {
+                case "enableCBS":
+                    return networkCapabilitiesForType(TYPE_MOBILE_CBS);
+                case "enableDUN":
+                case "enableDUNAlways":
+                    return networkCapabilitiesForType(TYPE_MOBILE_DUN);
+                case "enableFOTA":
+                    return networkCapabilitiesForType(TYPE_MOBILE_FOTA);
+                case "enableHIPRI":
+                    return networkCapabilitiesForType(TYPE_MOBILE_HIPRI);
+                case "enableIMS":
+                    return networkCapabilitiesForType(TYPE_MOBILE_IMS);
+                case "enableMMS":
+                    return networkCapabilitiesForType(TYPE_MOBILE_MMS);
+                case "enableSUPL":
+                    return networkCapabilitiesForType(TYPE_MOBILE_SUPL);
+                default:
+                    return null;
+            }
+        } else if (networkType == TYPE_WIFI && "p2p".equals(feature)) {
+            return networkCapabilitiesForType(TYPE_WIFI_P2P);
+        }
+        return null;
+    }
+
+    /**
+     * Guess what the network request was trying to say so that the resulting
+     * network is accessible via the legacy (deprecated) API such as
+     * requestRouteToHost.
+     *
+     * This means we should try to be fairly precise about transport and
+     * capability but ignore things such as networkSpecifier.
+     * If the request has more than one transport or capability it doesn't
+     * match the old legacy requests (they selected only single transport/capability)
+     * so this function cannot map the request to a single legacy type and
+     * the resulting network will not be available to the legacy APIs.
+     *
+     * This code is only called from the requestNetwork API (L and above).
+     *
+     * Setting a legacy type causes CONNECTIVITY_ACTION broadcasts, which are expensive
+     * because they wake up lots of apps - see http://b/23350688 . So we currently only
+     * do this for SUPL requests, which are the only ones that we know need it. If
+     * omitting these broadcasts causes unacceptable app breakage, then for backwards
+     * compatibility we can send them:
+     *
+     * if (targetSdkVersion < Build.VERSION_CODES.M) &&        // legacy API unsupported >= M
+     *     targetSdkVersion >= Build.VERSION_CODES.LOLLIPOP))  // requestNetwork not present < L
+     *
+     * TODO - This should be removed when the legacy APIs are removed.
+     */
+    private int inferLegacyTypeForNetworkCapabilities(NetworkCapabilities netCap) {
+        if (netCap == null) {
+            return TYPE_NONE;
+        }
+
+        if (!netCap.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
+            return TYPE_NONE;
+        }
+
+        // Do this only for SUPL, until GnssLocationProvider is fixed. http://b/25876485 .
+        if (!netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_SUPL)) {
+            // NOTE: if this causes app breakage, we should not just comment out this early return;
+            // instead, we should make this early return conditional on the requesting app's target
+            // SDK version, as described in the comment above.
+            return TYPE_NONE;
+        }
+
+        String type = null;
+        int result = TYPE_NONE;
+
+        if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_CBS)) {
+            type = "enableCBS";
+            result = TYPE_MOBILE_CBS;
+        } else if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_IMS)) {
+            type = "enableIMS";
+            result = TYPE_MOBILE_IMS;
+        } else if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_FOTA)) {
+            type = "enableFOTA";
+            result = TYPE_MOBILE_FOTA;
+        } else if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_DUN)) {
+            type = "enableDUN";
+            result = TYPE_MOBILE_DUN;
+        } else if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_SUPL)) {
+            type = "enableSUPL";
+            result = TYPE_MOBILE_SUPL;
+        // back out this hack for mms as they no longer need this and it's causing
+        // device slowdowns - b/23350688 (note, supl still needs this)
+        //} else if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_MMS)) {
+        //    type = "enableMMS";
+        //    result = TYPE_MOBILE_MMS;
+        } else if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
+            type = "enableHIPRI";
+            result = TYPE_MOBILE_HIPRI;
+        }
+        if (type != null) {
+            NetworkCapabilities testCap = networkCapabilitiesForFeature(TYPE_MOBILE, type);
+            if (testCap.equalsNetCapabilities(netCap) && testCap.equalsTransportTypes(netCap)) {
+                return result;
+            }
+        }
+        return TYPE_NONE;
+    }
+
+    private int legacyTypeForNetworkCapabilities(NetworkCapabilities netCap) {
+        if (netCap == null) return TYPE_NONE;
+        if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_CBS)) {
+            return TYPE_MOBILE_CBS;
+        }
+        if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_IMS)) {
+            return TYPE_MOBILE_IMS;
+        }
+        if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_FOTA)) {
+            return TYPE_MOBILE_FOTA;
+        }
+        if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_DUN)) {
+            return TYPE_MOBILE_DUN;
+        }
+        if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_SUPL)) {
+            return TYPE_MOBILE_SUPL;
+        }
+        if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_MMS)) {
+            return TYPE_MOBILE_MMS;
+        }
+        if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
+            return TYPE_MOBILE_HIPRI;
+        }
+        if (netCap.hasCapability(NetworkCapabilities.NET_CAPABILITY_WIFI_P2P)) {
+            return TYPE_WIFI_P2P;
+        }
+        return TYPE_NONE;
+    }
+
+    private static class LegacyRequest {
+        NetworkCapabilities networkCapabilities;
+        NetworkRequest networkRequest;
+        int expireSequenceNumber;
+        Network currentNetwork;
+        int delay = -1;
+
+        private void clearDnsBinding() {
+            if (currentNetwork != null) {
+                currentNetwork = null;
+                setProcessDefaultNetworkForHostResolution(null);
+            }
+        }
+
+        NetworkCallback networkCallback = new NetworkCallback() {
+            @Override
+            public void onAvailable(Network network) {
+                currentNetwork = network;
+                Log.d(TAG, "startUsingNetworkFeature got Network:" + network);
+                setProcessDefaultNetworkForHostResolution(network);
+            }
+            @Override
+            public void onLost(Network network) {
+                if (network.equals(currentNetwork)) clearDnsBinding();
+                Log.d(TAG, "startUsingNetworkFeature lost Network:" + network);
+            }
+        };
+    }
+
+    @UnsupportedAppUsage
+    private static final HashMap<NetworkCapabilities, LegacyRequest> sLegacyRequests =
+            new HashMap<>();
+
+    private NetworkRequest findRequestForFeature(NetworkCapabilities netCap) {
+        synchronized (sLegacyRequests) {
+            LegacyRequest l = sLegacyRequests.get(netCap);
+            if (l != null) return l.networkRequest;
+        }
+        return null;
+    }
+
+    private void renewRequestLocked(LegacyRequest l) {
+        l.expireSequenceNumber++;
+        Log.d(TAG, "renewing request to seqNum " + l.expireSequenceNumber);
+        sendExpireMsgForFeature(l.networkCapabilities, l.expireSequenceNumber, l.delay);
+    }
+
+    private void expireRequest(NetworkCapabilities netCap, int sequenceNum) {
+        int ourSeqNum = -1;
+        synchronized (sLegacyRequests) {
+            LegacyRequest l = sLegacyRequests.get(netCap);
+            if (l == null) return;
+            ourSeqNum = l.expireSequenceNumber;
+            if (l.expireSequenceNumber == sequenceNum) removeRequestForFeature(netCap);
+        }
+        Log.d(TAG, "expireRequest with " + ourSeqNum + ", " + sequenceNum);
+    }
+
+    @UnsupportedAppUsage
+    private NetworkRequest requestNetworkForFeatureLocked(NetworkCapabilities netCap) {
+        int delay = -1;
+        int type = legacyTypeForNetworkCapabilities(netCap);
+        try {
+            delay = mService.getRestoreDefaultNetworkDelay(type);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+        LegacyRequest l = new LegacyRequest();
+        l.networkCapabilities = netCap;
+        l.delay = delay;
+        l.expireSequenceNumber = 0;
+        l.networkRequest = sendRequestForNetwork(
+                netCap, l.networkCallback, 0, REQUEST, type, getDefaultHandler());
+        if (l.networkRequest == null) return null;
+        sLegacyRequests.put(netCap, l);
+        sendExpireMsgForFeature(netCap, l.expireSequenceNumber, delay);
+        return l.networkRequest;
+    }
+
+    private void sendExpireMsgForFeature(NetworkCapabilities netCap, int seqNum, int delay) {
+        if (delay >= 0) {
+            Log.d(TAG, "sending expire msg with seqNum " + seqNum + " and delay " + delay);
+            CallbackHandler handler = getDefaultHandler();
+            Message msg = handler.obtainMessage(EXPIRE_LEGACY_REQUEST, seqNum, 0, netCap);
+            handler.sendMessageDelayed(msg, delay);
+        }
+    }
+
+    @UnsupportedAppUsage
+    private boolean removeRequestForFeature(NetworkCapabilities netCap) {
+        final LegacyRequest l;
+        synchronized (sLegacyRequests) {
+            l = sLegacyRequests.remove(netCap);
+        }
+        if (l == null) return false;
+        unregisterNetworkCallback(l.networkCallback);
+        l.clearDnsBinding();
+        return true;
+    }
+
+    private static final SparseIntArray sLegacyTypeToTransport = new SparseIntArray();
+    static {
+        sLegacyTypeToTransport.put(TYPE_MOBILE,       NetworkCapabilities.TRANSPORT_CELLULAR);
+        sLegacyTypeToTransport.put(TYPE_MOBILE_CBS,   NetworkCapabilities.TRANSPORT_CELLULAR);
+        sLegacyTypeToTransport.put(TYPE_MOBILE_DUN,   NetworkCapabilities.TRANSPORT_CELLULAR);
+        sLegacyTypeToTransport.put(TYPE_MOBILE_FOTA,  NetworkCapabilities.TRANSPORT_CELLULAR);
+        sLegacyTypeToTransport.put(TYPE_MOBILE_HIPRI, NetworkCapabilities.TRANSPORT_CELLULAR);
+        sLegacyTypeToTransport.put(TYPE_MOBILE_IMS,   NetworkCapabilities.TRANSPORT_CELLULAR);
+        sLegacyTypeToTransport.put(TYPE_MOBILE_MMS,   NetworkCapabilities.TRANSPORT_CELLULAR);
+        sLegacyTypeToTransport.put(TYPE_MOBILE_SUPL,  NetworkCapabilities.TRANSPORT_CELLULAR);
+        sLegacyTypeToTransport.put(TYPE_WIFI,         NetworkCapabilities.TRANSPORT_WIFI);
+        sLegacyTypeToTransport.put(TYPE_WIFI_P2P,     NetworkCapabilities.TRANSPORT_WIFI);
+        sLegacyTypeToTransport.put(TYPE_BLUETOOTH,    NetworkCapabilities.TRANSPORT_BLUETOOTH);
+        sLegacyTypeToTransport.put(TYPE_ETHERNET,     NetworkCapabilities.TRANSPORT_ETHERNET);
+    }
+
+    private static final SparseIntArray sLegacyTypeToCapability = new SparseIntArray();
+    static {
+        sLegacyTypeToCapability.put(TYPE_MOBILE_CBS,  NetworkCapabilities.NET_CAPABILITY_CBS);
+        sLegacyTypeToCapability.put(TYPE_MOBILE_DUN,  NetworkCapabilities.NET_CAPABILITY_DUN);
+        sLegacyTypeToCapability.put(TYPE_MOBILE_FOTA, NetworkCapabilities.NET_CAPABILITY_FOTA);
+        sLegacyTypeToCapability.put(TYPE_MOBILE_IMS,  NetworkCapabilities.NET_CAPABILITY_IMS);
+        sLegacyTypeToCapability.put(TYPE_MOBILE_MMS,  NetworkCapabilities.NET_CAPABILITY_MMS);
+        sLegacyTypeToCapability.put(TYPE_MOBILE_SUPL, NetworkCapabilities.NET_CAPABILITY_SUPL);
+        sLegacyTypeToCapability.put(TYPE_WIFI_P2P,    NetworkCapabilities.NET_CAPABILITY_WIFI_P2P);
+    }
+
+    /**
+     * Given a legacy type (TYPE_WIFI, ...) returns a NetworkCapabilities
+     * instance suitable for registering a request or callback.  Throws an
+     * IllegalArgumentException if no mapping from the legacy type to
+     * NetworkCapabilities is known.
+     *
+     * @deprecated Types are deprecated. Use {@link NetworkCallback} or {@link NetworkRequest}
+     *     to find the network instead.
+     * @hide
+     */
+    public static NetworkCapabilities networkCapabilitiesForType(int type) {
+        final NetworkCapabilities nc = new NetworkCapabilities();
+
+        // Map from type to transports.
+        final int NOT_FOUND = -1;
+        final int transport = sLegacyTypeToTransport.get(type, NOT_FOUND);
+        Preconditions.checkArgument(transport != NOT_FOUND, "unknown legacy type: " + type);
+        nc.addTransportType(transport);
+
+        // Map from type to capabilities.
+        nc.addCapability(sLegacyTypeToCapability.get(
+                type, NetworkCapabilities.NET_CAPABILITY_INTERNET));
+        nc.maybeMarkCapabilitiesRestricted();
+        return nc;
+    }
+
+    /** @hide */
+    public static class PacketKeepaliveCallback {
+        /** The requested keepalive was successfully started. */
+        @UnsupportedAppUsage
+        public void onStarted() {}
+        /** The keepalive was successfully stopped. */
+        @UnsupportedAppUsage
+        public void onStopped() {}
+        /** An error occurred. */
+        @UnsupportedAppUsage
+        public void onError(int error) {}
+    }
+
+    /**
+     * Allows applications to request that the system periodically send specific packets on their
+     * behalf, using hardware offload to save battery power.
+     *
+     * To request that the system send keepalives, call one of the methods that return a
+     * {@link ConnectivityManager.PacketKeepalive} object, such as {@link #startNattKeepalive},
+     * passing in a non-null callback. If the callback is successfully started, the callback's
+     * {@code onStarted} method will be called. If an error occurs, {@code onError} will be called,
+     * specifying one of the {@code ERROR_*} constants in this class.
+     *
+     * To stop an existing keepalive, call {@link PacketKeepalive#stop}. The system will call
+     * {@link PacketKeepaliveCallback#onStopped} if the operation was successful or
+     * {@link PacketKeepaliveCallback#onError} if an error occurred.
+     *
+     * @deprecated Use {@link SocketKeepalive} instead.
+     *
+     * @hide
+     */
+    public class PacketKeepalive {
+
+        private static final String TAG = "PacketKeepalive";
+
+        /** @hide */
+        public static final int SUCCESS = 0;
+
+        /** @hide */
+        public static final int NO_KEEPALIVE = -1;
+
+        /** @hide */
+        public static final int BINDER_DIED = -10;
+
+        /** The specified {@code Network} is not connected. */
+        public static final int ERROR_INVALID_NETWORK = -20;
+        /** The specified IP addresses are invalid. For example, the specified source IP address is
+          * not configured on the specified {@code Network}. */
+        public static final int ERROR_INVALID_IP_ADDRESS = -21;
+        /** The requested port is invalid. */
+        public static final int ERROR_INVALID_PORT = -22;
+        /** The packet length is invalid (e.g., too long). */
+        public static final int ERROR_INVALID_LENGTH = -23;
+        /** The packet transmission interval is invalid (e.g., too short). */
+        public static final int ERROR_INVALID_INTERVAL = -24;
+
+        /** The hardware does not support this request. */
+        public static final int ERROR_HARDWARE_UNSUPPORTED = -30;
+        /** The hardware returned an error. */
+        public static final int ERROR_HARDWARE_ERROR = -31;
+
+        /** The NAT-T destination port for IPsec */
+        public static final int NATT_PORT = 4500;
+
+        /** The minimum interval in seconds between keepalive packet transmissions */
+        public static final int MIN_INTERVAL = 10;
+
+        private final Network mNetwork;
+        private final ISocketKeepaliveCallback mCallback;
+        private final ExecutorService mExecutor;
+
+        private volatile Integer mSlot;
+
+        @UnsupportedAppUsage
+        public void stop() {
+            try {
+                mExecutor.execute(() -> {
+                    try {
+                        if (mSlot != null) {
+                            mService.stopKeepalive(mNetwork, mSlot);
+                        }
+                    } catch (RemoteException e) {
+                        Log.e(TAG, "Error stopping packet keepalive: ", e);
+                        throw e.rethrowFromSystemServer();
+                    }
+                });
+            } catch (RejectedExecutionException e) {
+                // The internal executor has already stopped due to previous event.
+            }
+        }
+
+        private PacketKeepalive(Network network, PacketKeepaliveCallback callback) {
+            Preconditions.checkNotNull(network, "network cannot be null");
+            Preconditions.checkNotNull(callback, "callback cannot be null");
+            mNetwork = network;
+            mExecutor = Executors.newSingleThreadExecutor();
+            mCallback = new ISocketKeepaliveCallback.Stub() {
+                @Override
+                public void onStarted(int slot) {
+                    Binder.withCleanCallingIdentity(() ->
+                            mExecutor.execute(() -> {
+                                mSlot = slot;
+                                callback.onStarted();
+                            }));
+                }
+
+                @Override
+                public void onStopped() {
+                    Binder.withCleanCallingIdentity(() ->
+                            mExecutor.execute(() -> {
+                                mSlot = null;
+                                callback.onStopped();
+                            }));
+                    mExecutor.shutdown();
+                }
+
+                @Override
+                public void onError(int error) {
+                    Binder.withCleanCallingIdentity(() ->
+                            mExecutor.execute(() -> {
+                                mSlot = null;
+                                callback.onError(error);
+                            }));
+                    mExecutor.shutdown();
+                }
+
+                @Override
+                public void onDataReceived() {
+                    // PacketKeepalive is only used for Nat-T keepalive and as such does not invoke
+                    // this callback when data is received.
+                }
+            };
+        }
+    }
+
+    /**
+     * Starts an IPsec NAT-T keepalive packet with the specified parameters.
+     *
+     * @deprecated Use {@link #createSocketKeepalive} instead.
+     *
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public PacketKeepalive startNattKeepalive(
+            Network network, int intervalSeconds, PacketKeepaliveCallback callback,
+            InetAddress srcAddr, int srcPort, InetAddress dstAddr) {
+        final PacketKeepalive k = new PacketKeepalive(network, callback);
+        try {
+            mService.startNattKeepalive(network, intervalSeconds, k.mCallback,
+                    srcAddr.getHostAddress(), srcPort, dstAddr.getHostAddress());
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error starting packet keepalive: ", e);
+            throw e.rethrowFromSystemServer();
+        }
+        return k;
+    }
+
+    /**
+     * Request that keepalives be started on a IPsec NAT-T socket.
+     *
+     * @param network The {@link Network} the socket is on.
+     * @param socket The socket that needs to be kept alive.
+     * @param source The source address of the {@link UdpEncapsulationSocket}.
+     * @param destination The destination address of the {@link UdpEncapsulationSocket}.
+     * @param executor The executor on which callback will be invoked. The provided {@link Executor}
+     *                 must run callback sequentially, otherwise the order of callbacks cannot be
+     *                 guaranteed.
+     * @param callback A {@link SocketKeepalive.Callback}. Used for notifications about keepalive
+     *        changes. Must be extended by applications that use this API.
+     *
+     * @return A {@link SocketKeepalive} object that can be used to control the keepalive on the
+     *         given socket.
+     **/
+    public @NonNull SocketKeepalive createSocketKeepalive(@NonNull Network network,
+            @NonNull UdpEncapsulationSocket socket,
+            @NonNull InetAddress source,
+            @NonNull InetAddress destination,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull Callback callback) {
+        ParcelFileDescriptor dup;
+        try {
+            // Dup is needed here as the pfd inside the socket is owned by the IpSecService,
+            // which cannot be obtained by the app process.
+            dup = ParcelFileDescriptor.dup(socket.getFileDescriptor());
+        } catch (IOException ignored) {
+            // Construct an invalid fd, so that if the user later calls start(), it will fail with
+            // ERROR_INVALID_SOCKET.
+            dup = new ParcelFileDescriptor(new FileDescriptor());
+        }
+        return new NattSocketKeepalive(mService, network, dup, socket.getResourceId(), source,
+                destination, executor, callback);
+    }
+
+    /**
+     * Request that keepalives be started on a IPsec NAT-T socket file descriptor. Directly called
+     * by system apps which don't use IpSecService to create {@link UdpEncapsulationSocket}.
+     *
+     * @param network The {@link Network} the socket is on.
+     * @param pfd The {@link ParcelFileDescriptor} that needs to be kept alive. The provided
+     *        {@link ParcelFileDescriptor} must be bound to a port and the keepalives will be sent
+     *        from that port.
+     * @param source The source address of the {@link UdpEncapsulationSocket}.
+     * @param destination The destination address of the {@link UdpEncapsulationSocket}. The
+     *        keepalive packets will always be sent to port 4500 of the given {@code destination}.
+     * @param executor The executor on which callback will be invoked. The provided {@link Executor}
+     *                 must run callback sequentially, otherwise the order of callbacks cannot be
+     *                 guaranteed.
+     * @param callback A {@link SocketKeepalive.Callback}. Used for notifications about keepalive
+     *        changes. Must be extended by applications that use this API.
+     *
+     * @return A {@link SocketKeepalive} object that can be used to control the keepalive on the
+     *         given socket.
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD)
+    public @NonNull SocketKeepalive createNattKeepalive(@NonNull Network network,
+            @NonNull ParcelFileDescriptor pfd,
+            @NonNull InetAddress source,
+            @NonNull InetAddress destination,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull Callback callback) {
+        ParcelFileDescriptor dup;
+        try {
+            // TODO: Consider remove unnecessary dup.
+            dup = pfd.dup();
+        } catch (IOException ignored) {
+            // Construct an invalid fd, so that if the user later calls start(), it will fail with
+            // ERROR_INVALID_SOCKET.
+            dup = new ParcelFileDescriptor(new FileDescriptor());
+        }
+        return new NattSocketKeepalive(mService, network, dup,
+                INVALID_RESOURCE_ID /* Unused */, source, destination, executor, callback);
+    }
+
+    /**
+     * Request that keepalives be started on a TCP socket.
+     * The socket must be established.
+     *
+     * @param network The {@link Network} the socket is on.
+     * @param socket The socket that needs to be kept alive.
+     * @param executor The executor on which callback will be invoked. This implementation assumes
+     *                 the provided {@link Executor} runs the callbacks in sequence with no
+     *                 concurrency. Failing this, no guarantee of correctness can be made. It is
+     *                 the responsibility of the caller to ensure the executor provides this
+     *                 guarantee. A simple way of creating such an executor is with the standard
+     *                 tool {@code Executors.newSingleThreadExecutor}.
+     * @param callback A {@link SocketKeepalive.Callback}. Used for notifications about keepalive
+     *        changes. Must be extended by applications that use this API.
+     *
+     * @return A {@link SocketKeepalive} object that can be used to control the keepalive on the
+     *         given socket.
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD)
+    public @NonNull SocketKeepalive createSocketKeepalive(@NonNull Network network,
+            @NonNull Socket socket,
+            @NonNull Executor executor,
+            @NonNull Callback callback) {
+        ParcelFileDescriptor dup;
+        try {
+            dup = ParcelFileDescriptor.fromSocket(socket);
+        } catch (UncheckedIOException ignored) {
+            // Construct an invalid fd, so that if the user later calls start(), it will fail with
+            // ERROR_INVALID_SOCKET.
+            dup = new ParcelFileDescriptor(new FileDescriptor());
+        }
+        return new TcpSocketKeepalive(mService, network, dup, executor, callback);
+    }
+
+    /**
+     * Ensure that a network route exists to deliver traffic to the specified
+     * host via the specified network interface. An attempt to add a route that
+     * already exists is ignored, but treated as successful.
+     *
+     * <p>This method requires the caller to hold either the
+     * {@link android.Manifest.permission#CHANGE_NETWORK_STATE} permission
+     * or the ability to modify system settings as determined by
+     * {@link android.provider.Settings.System#canWrite}.</p>
+     *
+     * @param networkType the type of the network over which traffic to the specified
+     * host is to be routed
+     * @param hostAddress the IP address of the host to which the route is desired
+     * @return {@code true} on success, {@code false} on failure
+     *
+     * @deprecated Deprecated in favor of the
+     *             {@link #requestNetwork(NetworkRequest, NetworkCallback)},
+     *             {@link #bindProcessToNetwork} and {@link Network#getSocketFactory} API.
+     *             In {@link VERSION_CODES#M}, and above, this method is unsupported and will
+     *             throw {@code UnsupportedOperationException} if called.
+     * @removed
+     */
+    @Deprecated
+    public boolean requestRouteToHost(int networkType, int hostAddress) {
+        return requestRouteToHostAddress(networkType, NetworkUtils.intToInetAddress(hostAddress));
+    }
+
+    /**
+     * Ensure that a network route exists to deliver traffic to the specified
+     * host via the specified network interface. An attempt to add a route that
+     * already exists is ignored, but treated as successful.
+     *
+     * <p>This method requires the caller to hold either the
+     * {@link android.Manifest.permission#CHANGE_NETWORK_STATE} permission
+     * or the ability to modify system settings as determined by
+     * {@link android.provider.Settings.System#canWrite}.</p>
+     *
+     * @param networkType the type of the network over which traffic to the specified
+     * host is to be routed
+     * @param hostAddress the IP address of the host to which the route is desired
+     * @return {@code true} on success, {@code false} on failure
+     * @hide
+     * @deprecated Deprecated in favor of the {@link #requestNetwork} and
+     *             {@link #bindProcessToNetwork} API.
+     */
+    @Deprecated
+    @UnsupportedAppUsage
+    public boolean requestRouteToHostAddress(int networkType, InetAddress hostAddress) {
+        checkLegacyRoutingApiAccess();
+        try {
+            return mService.requestRouteToHostAddress(networkType, hostAddress.getAddress());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns the value of the setting for background data usage. If false,
+     * applications should not use the network if the application is not in the
+     * foreground. Developers should respect this setting, and check the value
+     * of this before performing any background data operations.
+     * <p>
+     * All applications that have background services that use the network
+     * should listen to {@link #ACTION_BACKGROUND_DATA_SETTING_CHANGED}.
+     * <p>
+     * @deprecated As of {@link VERSION_CODES#ICE_CREAM_SANDWICH}, availability of
+     * background data depends on several combined factors, and this method will
+     * always return {@code true}. Instead, when background data is unavailable,
+     * {@link #getActiveNetworkInfo()} will now appear disconnected.
+     *
+     * @return Whether background data usage is allowed.
+     */
+    @Deprecated
+    public boolean getBackgroundDataSetting() {
+        // assume that background data is allowed; final authority is
+        // NetworkInfo which may be blocked.
+        return true;
+    }
+
+    /**
+     * Sets the value of the setting for background data usage.
+     *
+     * @param allowBackgroundData Whether an application should use data while
+     *            it is in the background.
+     *
+     * @attr ref android.Manifest.permission#CHANGE_BACKGROUND_DATA_SETTING
+     * @see #getBackgroundDataSetting()
+     * @hide
+     */
+    @Deprecated
+    @UnsupportedAppUsage
+    public void setBackgroundDataSetting(boolean allowBackgroundData) {
+        // ignored
+    }
+
+    /** {@hide} */
+    @Deprecated
+    @UnsupportedAppUsage
+    public NetworkQuotaInfo getActiveNetworkQuotaInfo() {
+        try {
+            return mService.getActiveNetworkQuotaInfo();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * @hide
+     * @deprecated Talk to TelephonyManager directly
+     */
+    @Deprecated
+    @UnsupportedAppUsage
+    public boolean getMobileDataEnabled() {
+        IBinder b = ServiceManager.getService(Context.TELEPHONY_SERVICE);
+        if (b != null) {
+            try {
+                ITelephony it = ITelephony.Stub.asInterface(b);
+                int subId = SubscriptionManager.getDefaultDataSubscriptionId();
+                Log.d("ConnectivityManager", "getMobileDataEnabled()+ subId=" + subId);
+                boolean retVal = it.isUserDataEnabled(subId);
+                Log.d("ConnectivityManager", "getMobileDataEnabled()- subId=" + subId
+                        + " retVal=" + retVal);
+                return retVal;
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+        Log.d("ConnectivityManager", "getMobileDataEnabled()- remote exception retVal=false");
+        return false;
+    }
+
+    /**
+     * Callback for use with {@link ConnectivityManager#addDefaultNetworkActiveListener}
+     * to find out when the system default network has gone in to a high power state.
+     */
+    public interface OnNetworkActiveListener {
+        /**
+         * Called on the main thread of the process to report that the current data network
+         * has become active, and it is now a good time to perform any pending network
+         * operations.  Note that this listener only tells you when the network becomes
+         * active; if at any other time you want to know whether it is active (and thus okay
+         * to initiate network traffic), you can retrieve its instantaneous state with
+         * {@link ConnectivityManager#isDefaultNetworkActive}.
+         */
+        void onNetworkActive();
+    }
+
+    private INetworkManagementService getNetworkManagementService() {
+        synchronized (this) {
+            if (mNMService != null) {
+                return mNMService;
+            }
+            IBinder b = ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE);
+            mNMService = INetworkManagementService.Stub.asInterface(b);
+            return mNMService;
+        }
+    }
+
+    private final ArrayMap<OnNetworkActiveListener, INetworkActivityListener>
+            mNetworkActivityListeners = new ArrayMap<>();
+
+    /**
+     * Start listening to reports when the system's default data network is active, meaning it is
+     * a good time to perform network traffic.  Use {@link #isDefaultNetworkActive()}
+     * to determine the current state of the system's default network after registering the
+     * listener.
+     * <p>
+     * If the process default network has been set with
+     * {@link ConnectivityManager#bindProcessToNetwork} this function will not
+     * reflect the process's default, but the system default.
+     *
+     * @param l The listener to be told when the network is active.
+     */
+    public void addDefaultNetworkActiveListener(final OnNetworkActiveListener l) {
+        INetworkActivityListener rl = new INetworkActivityListener.Stub() {
+            @Override
+            public void onNetworkActive() throws RemoteException {
+                l.onNetworkActive();
+            }
+        };
+
+        try {
+            getNetworkManagementService().registerNetworkActivityListener(rl);
+            mNetworkActivityListeners.put(l, rl);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Remove network active listener previously registered with
+     * {@link #addDefaultNetworkActiveListener}.
+     *
+     * @param l Previously registered listener.
+     */
+    public void removeDefaultNetworkActiveListener(@NonNull OnNetworkActiveListener l) {
+        INetworkActivityListener rl = mNetworkActivityListeners.get(l);
+        Preconditions.checkArgument(rl != null, "Listener was not registered.");
+        try {
+            getNetworkManagementService().unregisterNetworkActivityListener(rl);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Return whether the data network is currently active.  An active network means that
+     * it is currently in a high power state for performing data transmission.  On some
+     * types of networks, it may be expensive to move and stay in such a state, so it is
+     * more power efficient to batch network traffic together when the radio is already in
+     * this state.  This method tells you whether right now is currently a good time to
+     * initiate network traffic, as the network is already active.
+     */
+    public boolean isDefaultNetworkActive() {
+        try {
+            return getNetworkManagementService().isNetworkActive();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * {@hide}
+     */
+    public ConnectivityManager(Context context, IConnectivityManager service) {
+        mContext = Preconditions.checkNotNull(context, "missing context");
+        mService = Preconditions.checkNotNull(service, "missing IConnectivityManager");
+        sInstance = this;
+    }
+
+    /** {@hide} */
+    @UnsupportedAppUsage
+    public static ConnectivityManager from(Context context) {
+        return (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+    }
+
+    /** @hide */
+    public NetworkRequest getDefaultRequest() {
+        try {
+            // This is not racy as the default request is final in ConnectivityService.
+            return mService.getDefaultRequest();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /* TODO: These permissions checks don't belong in client-side code. Move them to
+     * services.jar, possibly in com.android.server.net. */
+
+    /** {@hide} */
+    public static final void enforceChangePermission(Context context) {
+        int uid = Binder.getCallingUid();
+        Settings.checkAndNoteChangeNetworkStateOperation(context, uid, Settings
+                .getPackageNameForUid(context, uid), true /* throwException */);
+    }
+
+    /** {@hide} */
+    public static final void enforceTetherChangePermission(Context context, String callingPkg) {
+        Preconditions.checkNotNull(context, "Context cannot be null");
+        Preconditions.checkNotNull(callingPkg, "callingPkg cannot be null");
+
+        if (context.getResources().getStringArray(
+                com.android.internal.R.array.config_mobile_hotspot_provision_app).length == 2) {
+            // Have a provisioning app - must only let system apps (which check this app)
+            // turn on tethering
+            context.enforceCallingOrSelfPermission(
+                    android.Manifest.permission.TETHER_PRIVILEGED, "ConnectivityService");
+        } else {
+            int uid = Binder.getCallingUid();
+            // If callingPkg's uid is not same as Binder.getCallingUid(),
+            // AppOpsService throws SecurityException.
+            Settings.checkAndNoteWriteSettingsOperation(context, uid, callingPkg,
+                    true /* throwException */);
+        }
+    }
+
+    /**
+     * @deprecated - use getSystemService. This is a kludge to support static access in certain
+     *               situations where a Context pointer is unavailable.
+     * @hide
+     */
+    @Deprecated
+    static ConnectivityManager getInstanceOrNull() {
+        return sInstance;
+    }
+
+    /**
+     * @deprecated - use getSystemService. This is a kludge to support static access in certain
+     *               situations where a Context pointer is unavailable.
+     * @hide
+     */
+    @Deprecated
+    @UnsupportedAppUsage
+    private static ConnectivityManager getInstance() {
+        if (getInstanceOrNull() == null) {
+            throw new IllegalStateException("No ConnectivityManager yet constructed");
+        }
+        return getInstanceOrNull();
+    }
+
+    /**
+     * Get the set of tetherable, available interfaces.  This list is limited by
+     * device configuration and current interface existence.
+     *
+     * @return an array of 0 or more Strings of tetherable interface names.
+     *
+     * {@hide}
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    @UnsupportedAppUsage
+    public String[] getTetherableIfaces() {
+        try {
+            return mService.getTetherableIfaces();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Get the set of tethered interfaces.
+     *
+     * @return an array of 0 or more String of currently tethered interface names.
+     *
+     * {@hide}
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    @UnsupportedAppUsage
+    public String[] getTetheredIfaces() {
+        try {
+            return mService.getTetheredIfaces();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Get the set of interface names which attempted to tether but
+     * failed.  Re-attempting to tether may cause them to reset to the Tethered
+     * state.  Alternatively, causing the interface to be destroyed and recreated
+     * may cause them to reset to the available state.
+     * {@link ConnectivityManager#getLastTetherError} can be used to get more
+     * information on the cause of the errors.
+     *
+     * @return an array of 0 or more String indicating the interface names
+     *        which failed to tether.
+     *
+     * {@hide}
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    @UnsupportedAppUsage
+    public String[] getTetheringErroredIfaces() {
+        try {
+            return mService.getTetheringErroredIfaces();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Get the set of tethered dhcp ranges.
+     *
+     * @return an array of 0 or more {@code String} of tethered dhcp ranges.
+     * {@hide}
+     */
+    public String[] getTetheredDhcpRanges() {
+        try {
+            return mService.getTetheredDhcpRanges();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Attempt to tether the named interface.  This will setup a dhcp server
+     * on the interface, forward and NAT IP packets and forward DNS requests
+     * to the best active upstream network interface.  Note that if no upstream
+     * IP network interface is available, dhcp will still run and traffic will be
+     * allowed between the tethered devices and this device, though upstream net
+     * access will of course fail until an upstream network interface becomes
+     * active.
+     *
+     * <p>This method requires the caller to hold either the
+     * {@link android.Manifest.permission#CHANGE_NETWORK_STATE} permission
+     * or the ability to modify system settings as determined by
+     * {@link android.provider.Settings.System#canWrite}.</p>
+     *
+     * <p>WARNING: New clients should not use this function. The only usages should be in PanService
+     * and WifiStateMachine which need direct access. All other clients should use
+     * {@link #startTethering} and {@link #stopTethering} which encapsulate proper provisioning
+     * logic.</p>
+     *
+     * @param iface the interface name to tether.
+     * @return error a {@code TETHER_ERROR} value indicating success or failure type
+     *
+     * {@hide}
+     */
+    @UnsupportedAppUsage
+    public int tether(String iface) {
+        try {
+            String pkgName = mContext.getOpPackageName();
+            Log.i(TAG, "tether caller:" + pkgName);
+            return mService.tether(iface, pkgName);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Stop tethering the named interface.
+     *
+     * <p>This method requires the caller to hold either the
+     * {@link android.Manifest.permission#CHANGE_NETWORK_STATE} permission
+     * or the ability to modify system settings as determined by
+     * {@link android.provider.Settings.System#canWrite}.</p>
+     *
+     * <p>WARNING: New clients should not use this function. The only usages should be in PanService
+     * and WifiStateMachine which need direct access. All other clients should use
+     * {@link #startTethering} and {@link #stopTethering} which encapsulate proper provisioning
+     * logic.</p>
+     *
+     * @param iface the interface name to untether.
+     * @return error a {@code TETHER_ERROR} value indicating success or failure type
+     *
+     * {@hide}
+     */
+    @UnsupportedAppUsage
+    public int untether(String iface) {
+        try {
+            String pkgName = mContext.getOpPackageName();
+            Log.i(TAG, "untether caller:" + pkgName);
+            return mService.untether(iface, pkgName);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Check if the device allows for tethering.  It may be disabled via
+     * {@code ro.tether.denied} system property, Settings.TETHER_SUPPORTED or
+     * due to device configuration.
+     *
+     * <p>If this app does not have permission to use this API, it will always
+     * return false rather than throw an exception.</p>
+     *
+     * <p>If the device has a hotspot provisioning app, the caller is required to hold the
+     * {@link android.Manifest.permission.TETHER_PRIVILEGED} permission.</p>
+     *
+     * <p>Otherwise, this method requires the caller to hold the ability to modify system
+     * settings as determined by {@link android.provider.Settings.System#canWrite}.</p>
+     *
+     * @return a boolean - {@code true} indicating Tethering is supported.
+     *
+     * {@hide}
+     */
+    @SystemApi
+    @RequiresPermission(anyOf = {android.Manifest.permission.TETHER_PRIVILEGED,
+            android.Manifest.permission.WRITE_SETTINGS})
+    public boolean isTetheringSupported() {
+        String pkgName = mContext.getOpPackageName();
+        try {
+            return mService.isTetheringSupported(pkgName);
+        } catch (SecurityException e) {
+            // This API is not available to this caller, but for backward-compatibility
+            // this will just return false instead of throwing.
+            return false;
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Callback for use with {@link #startTethering} to find out whether tethering succeeded.
+     * @hide
+     */
+    @SystemApi
+    public static abstract class OnStartTetheringCallback {
+        /**
+         * Called when tethering has been successfully started.
+         */
+        public void onTetheringStarted() {}
+
+        /**
+         * Called when starting tethering failed.
+         */
+        public void onTetheringFailed() {}
+    }
+
+    /**
+     * Convenient overload for
+     * {@link #startTethering(int, boolean, OnStartTetheringCallback, Handler)} which passes a null
+     * handler to run on the current thread's {@link Looper}.
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
+    public void startTethering(int type, boolean showProvisioningUi,
+            final OnStartTetheringCallback callback) {
+        startTethering(type, showProvisioningUi, callback, null);
+    }
+
+    /**
+     * Runs tether provisioning for the given type if needed and then starts tethering if
+     * the check succeeds. If no carrier provisioning is required for tethering, tethering is
+     * enabled immediately. If provisioning fails, tethering will not be enabled. It also
+     * schedules tether provisioning re-checks if appropriate.
+     *
+     * @param type The type of tethering to start. Must be one of
+     *         {@link ConnectivityManager.TETHERING_WIFI},
+     *         {@link ConnectivityManager.TETHERING_USB}, or
+     *         {@link ConnectivityManager.TETHERING_BLUETOOTH}.
+     * @param showProvisioningUi a boolean indicating to show the provisioning app UI if there
+     *         is one. This should be true the first time this function is called and also any time
+     *         the user can see this UI. It gives users information from their carrier about the
+     *         check failing and how they can sign up for tethering if possible.
+     * @param callback an {@link OnStartTetheringCallback} which will be called to notify the caller
+     *         of the result of trying to tether.
+     * @param handler {@link Handler} to specify the thread upon which the callback will be invoked.
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
+    public void startTethering(int type, boolean showProvisioningUi,
+            final OnStartTetheringCallback callback, Handler handler) {
+        Preconditions.checkNotNull(callback, "OnStartTetheringCallback cannot be null.");
+
+        ResultReceiver wrappedCallback = new ResultReceiver(handler) {
+            @Override
+            protected void onReceiveResult(int resultCode, Bundle resultData) {
+                if (resultCode == TETHER_ERROR_NO_ERROR) {
+                    callback.onTetheringStarted();
+                } else {
+                    callback.onTetheringFailed();
+                }
+            }
+        };
+
+        try {
+            String pkgName = mContext.getOpPackageName();
+            Log.i(TAG, "startTethering caller:" + pkgName);
+            mService.startTethering(type, wrappedCallback, showProvisioningUi, pkgName);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Exception trying to start tethering.", e);
+            wrappedCallback.send(TETHER_ERROR_SERVICE_UNAVAIL, null);
+        }
+    }
+
+    /**
+     * Stops tethering for the given type. Also cancels any provisioning rechecks for that type if
+     * applicable.
+     *
+     * @param type The type of tethering to stop. Must be one of
+     *         {@link ConnectivityManager.TETHERING_WIFI},
+     *         {@link ConnectivityManager.TETHERING_USB}, or
+     *         {@link ConnectivityManager.TETHERING_BLUETOOTH}.
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
+    public void stopTethering(int type) {
+        try {
+            String pkgName = mContext.getOpPackageName();
+            Log.i(TAG, "stopTethering caller:" + pkgName);
+            mService.stopTethering(type, pkgName);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Callback for use with {@link registerTetheringEventCallback} to find out tethering
+     * upstream status.
+     *
+     *@hide
+     */
+    @SystemApi
+    public abstract static class OnTetheringEventCallback {
+
+        /**
+         * Called when tethering upstream changed. This can be called multiple times and can be
+         * called any time.
+         *
+         * @param network the {@link Network} of tethering upstream. Null means tethering doesn't
+         * have any upstream.
+         */
+        public void onUpstreamChanged(@Nullable Network network) {}
+    }
+
+    @GuardedBy("mTetheringEventCallbacks")
+    private final ArrayMap<OnTetheringEventCallback, ITetheringEventCallback>
+            mTetheringEventCallbacks = new ArrayMap<>();
+
+    /**
+     * Start listening to tethering change events. Any new added callback will receive the last
+     * tethering status right away. If callback is registered when tethering has no upstream or
+     * disabled, {@link OnTetheringEventCallback#onUpstreamChanged} will immediately be called
+     * with a null argument. The same callback object cannot be registered twice.
+     *
+     * @param executor the executor on which callback will be invoked.
+     * @param callback the callback to be called when tethering has change events.
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
+    public void registerTetheringEventCallback(
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull final OnTetheringEventCallback callback) {
+        Preconditions.checkNotNull(callback, "OnTetheringEventCallback cannot be null.");
+
+        synchronized (mTetheringEventCallbacks) {
+            Preconditions.checkArgument(!mTetheringEventCallbacks.containsKey(callback),
+                    "callback was already registered.");
+            ITetheringEventCallback remoteCallback = new ITetheringEventCallback.Stub() {
+                @Override
+                public void onUpstreamChanged(Network network) throws RemoteException {
+                    Binder.withCleanCallingIdentity(() ->
+                            executor.execute(() -> {
+                                callback.onUpstreamChanged(network);
+                            }));
+                }
+            };
+            try {
+                String pkgName = mContext.getOpPackageName();
+                Log.i(TAG, "registerTetheringUpstreamCallback:" + pkgName);
+                mService.registerTetheringEventCallback(remoteCallback, pkgName);
+                mTetheringEventCallbacks.put(callback, remoteCallback);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+    }
+
+    /**
+     * Remove tethering event callback previously registered with
+     * {@link #registerTetheringEventCallback}.
+     *
+     * @param callback previously registered callback.
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
+    public void unregisterTetheringEventCallback(
+            @NonNull final OnTetheringEventCallback callback) {
+        synchronized (mTetheringEventCallbacks) {
+            ITetheringEventCallback remoteCallback = mTetheringEventCallbacks.remove(callback);
+            Preconditions.checkNotNull(remoteCallback, "callback was not registered.");
+            try {
+                String pkgName = mContext.getOpPackageName();
+                Log.i(TAG, "unregisterTetheringEventCallback:" + pkgName);
+                mService.unregisterTetheringEventCallback(remoteCallback, pkgName);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+    }
+
+
+    /**
+     * Get the list of regular expressions that define any tetherable
+     * USB network interfaces.  If USB tethering is not supported by the
+     * device, this list should be empty.
+     *
+     * @return an array of 0 or more regular expression Strings defining
+     *        what interfaces are considered tetherable usb interfaces.
+     *
+     * {@hide}
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    @UnsupportedAppUsage
+    public String[] getTetherableUsbRegexs() {
+        try {
+            return mService.getTetherableUsbRegexs();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Get the list of regular expressions that define any tetherable
+     * Wifi network interfaces.  If Wifi tethering is not supported by the
+     * device, this list should be empty.
+     *
+     * @return an array of 0 or more regular expression Strings defining
+     *        what interfaces are considered tetherable wifi interfaces.
+     *
+     * {@hide}
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    @UnsupportedAppUsage
+    public String[] getTetherableWifiRegexs() {
+        try {
+            return mService.getTetherableWifiRegexs();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Get the list of regular expressions that define any tetherable
+     * Bluetooth network interfaces.  If Bluetooth tethering is not supported by the
+     * device, this list should be empty.
+     *
+     * @return an array of 0 or more regular expression Strings defining
+     *        what interfaces are considered tetherable bluetooth interfaces.
+     *
+     * {@hide}
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    @UnsupportedAppUsage
+    public String[] getTetherableBluetoothRegexs() {
+        try {
+            return mService.getTetherableBluetoothRegexs();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Attempt to both alter the mode of USB and Tethering of USB.  A
+     * utility method to deal with some of the complexity of USB - will
+     * attempt to switch to Rndis and subsequently tether the resulting
+     * interface on {@code true} or turn off tethering and switch off
+     * Rndis on {@code false}.
+     *
+     * <p>This method requires the caller to hold either the
+     * {@link android.Manifest.permission#CHANGE_NETWORK_STATE} permission
+     * or the ability to modify system settings as determined by
+     * {@link android.provider.Settings.System#canWrite}.</p>
+     *
+     * @param enable a boolean - {@code true} to enable tethering
+     * @return error a {@code TETHER_ERROR} value indicating success or failure type
+     *
+     * {@hide}
+     */
+    @UnsupportedAppUsage
+    public int setUsbTethering(boolean enable) {
+        try {
+            String pkgName = mContext.getOpPackageName();
+            Log.i(TAG, "setUsbTethering caller:" + pkgName);
+            return mService.setUsbTethering(enable, pkgName);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /** {@hide} */
+    @SystemApi
+    public static final int TETHER_ERROR_NO_ERROR           = 0;
+    /** {@hide} */
+    public static final int TETHER_ERROR_UNKNOWN_IFACE      = 1;
+    /** {@hide} */
+    public static final int TETHER_ERROR_SERVICE_UNAVAIL    = 2;
+    /** {@hide} */
+    public static final int TETHER_ERROR_UNSUPPORTED        = 3;
+    /** {@hide} */
+    public static final int TETHER_ERROR_UNAVAIL_IFACE      = 4;
+    /** {@hide} */
+    public static final int TETHER_ERROR_MASTER_ERROR       = 5;
+    /** {@hide} */
+    public static final int TETHER_ERROR_TETHER_IFACE_ERROR = 6;
+    /** {@hide} */
+    public static final int TETHER_ERROR_UNTETHER_IFACE_ERROR = 7;
+    /** {@hide} */
+    public static final int TETHER_ERROR_ENABLE_NAT_ERROR     = 8;
+    /** {@hide} */
+    public static final int TETHER_ERROR_DISABLE_NAT_ERROR    = 9;
+    /** {@hide} */
+    public static final int TETHER_ERROR_IFACE_CFG_ERROR      = 10;
+    /** {@hide} */
+    @SystemApi
+    public static final int TETHER_ERROR_PROVISION_FAILED     = 11;
+    /** {@hide} */
+    public static final int TETHER_ERROR_DHCPSERVER_ERROR     = 12;
+    /** {@hide} */
+    @SystemApi
+    public static final int TETHER_ERROR_ENTITLEMENT_UNKONWN  = 13;
+
+    /**
+     * Get a more detailed error code after a Tethering or Untethering
+     * request asynchronously failed.
+     *
+     * @param iface The name of the interface of interest
+     * @return error The error code of the last error tethering or untethering the named
+     *               interface
+     *
+     * {@hide}
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    @UnsupportedAppUsage
+    public int getLastTetherError(String iface) {
+        try {
+            return mService.getLastTetherError(iface);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(value = {
+            TETHER_ERROR_NO_ERROR,
+            TETHER_ERROR_PROVISION_FAILED,
+            TETHER_ERROR_ENTITLEMENT_UNKONWN,
+    })
+    public @interface EntitlementResultCode {
+    }
+
+    /**
+     * Callback for use with {@link #getLatestTetheringEntitlementResult} to find out whether
+     * entitlement succeeded.
+     * @hide
+     */
+    @SystemApi
+    public interface OnTetheringEntitlementResultListener  {
+        /**
+         * Called to notify entitlement result.
+         *
+         * @param resultCode an int value of entitlement result. It may be one of
+         *         {@link #TETHER_ERROR_NO_ERROR},
+         *         {@link #TETHER_ERROR_PROVISION_FAILED}, or
+         *         {@link #TETHER_ERROR_ENTITLEMENT_UNKONWN}.
+         */
+        void onTetheringEntitlementResult(@EntitlementResultCode int resultCode);
+    }
+
+    /**
+     * Get the last value of the entitlement check on this downstream. If the cached value is
+     * {@link #TETHER_ERROR_NO_ERROR} or showEntitlementUi argument is false, it just return the
+     * cached value. Otherwise, a UI-based entitlement check would be performed. It is not
+     * guaranteed that the UI-based entitlement check will complete in any specific time period
+     * and may in fact never complete. Any successful entitlement check the platform performs for
+     * any reason will update the cached value.
+     *
+     * @param type the downstream type of tethering. Must be one of
+     *         {@link #TETHERING_WIFI},
+     *         {@link #TETHERING_USB}, or
+     *         {@link #TETHERING_BLUETOOTH}.
+     * @param showEntitlementUi a boolean indicating whether to run UI-based entitlement check.
+     * @param executor the executor on which callback will be invoked.
+     * @param listener an {@link OnTetheringEntitlementResultListener} which will be called to
+     *         notify the caller of the result of entitlement check. The listener may be called zero
+     *         or one time.
+     * {@hide}
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.TETHER_PRIVILEGED)
+    public void getLatestTetheringEntitlementResult(int type, boolean showEntitlementUi,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull final OnTetheringEntitlementResultListener listener) {
+        Preconditions.checkNotNull(listener, "TetheringEntitlementResultListener cannot be null.");
+        ResultReceiver wrappedListener = new ResultReceiver(null) {
+            @Override
+            protected void onReceiveResult(int resultCode, Bundle resultData) {
+                Binder.withCleanCallingIdentity(() ->
+                            executor.execute(() -> {
+                                listener.onTetheringEntitlementResult(resultCode);
+                            }));
+            }
+        };
+
+        try {
+            String pkgName = mContext.getOpPackageName();
+            Log.i(TAG, "getLatestTetheringEntitlementResult:" + pkgName);
+            mService.getLatestTetheringEntitlementResult(type, wrappedListener,
+                    showEntitlementUi, pkgName);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Report network connectivity status.  This is currently used only
+     * to alter status bar UI.
+     * <p>This method requires the caller to hold the permission
+     * {@link android.Manifest.permission#STATUS_BAR}.
+     *
+     * @param networkType The type of network you want to report on
+     * @param percentage The quality of the connection 0 is bad, 100 is good
+     * @deprecated Types are deprecated. Use {@link #reportNetworkConnectivity} instead.
+     * {@hide}
+     */
+    public void reportInetCondition(int networkType, int percentage) {
+        printStackTrace();
+        try {
+            mService.reportInetCondition(networkType, percentage);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Report a problem network to the framework.  This provides a hint to the system
+     * that there might be connectivity problems on this network and may cause
+     * the framework to re-evaluate network connectivity and/or switch to another
+     * network.
+     *
+     * @param network The {@link Network} the application was attempting to use
+     *                or {@code null} to indicate the current default network.
+     * @deprecated Use {@link #reportNetworkConnectivity} which allows reporting both
+     *             working and non-working connectivity.
+     */
+    @Deprecated
+    public void reportBadNetwork(@Nullable Network network) {
+        printStackTrace();
+        try {
+            // One of these will be ignored because it matches system's current state.
+            // The other will trigger the necessary reevaluation.
+            mService.reportNetworkConnectivity(network, true);
+            mService.reportNetworkConnectivity(network, false);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Report to the framework whether a network has working connectivity.
+     * This provides a hint to the system that a particular network is providing
+     * working connectivity or not.  In response the framework may re-evaluate
+     * the network's connectivity and might take further action thereafter.
+     *
+     * @param network The {@link Network} the application was attempting to use
+     *                or {@code null} to indicate the current default network.
+     * @param hasConnectivity {@code true} if the application was able to successfully access the
+     *                        Internet using {@code network} or {@code false} if not.
+     */
+    public void reportNetworkConnectivity(@Nullable Network network, boolean hasConnectivity) {
+        printStackTrace();
+        try {
+            mService.reportNetworkConnectivity(network, hasConnectivity);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Set a network-independent global http proxy.  This is not normally what you want
+     * for typical HTTP proxies - they are general network dependent.  However if you're
+     * doing something unusual like general internal filtering this may be useful.  On
+     * a private network where the proxy is not accessible, you may break HTTP using this.
+     *
+     * @param p A {@link ProxyInfo} object defining the new global
+     *        HTTP proxy.  A {@code null} value will clear the global HTTP proxy.
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.CONNECTIVITY_INTERNAL)
+    public void setGlobalProxy(ProxyInfo p) {
+        try {
+            mService.setGlobalProxy(p);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Retrieve any network-independent global HTTP proxy.
+     *
+     * @return {@link ProxyInfo} for the current global HTTP proxy or {@code null}
+     *        if no global HTTP proxy is set.
+     * @hide
+     */
+    public ProxyInfo getGlobalProxy() {
+        try {
+            return mService.getGlobalProxy();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Retrieve the global HTTP proxy, or if no global HTTP proxy is set, a
+     * network-specific HTTP proxy.  If {@code network} is null, the
+     * network-specific proxy returned is the proxy of the default active
+     * network.
+     *
+     * @return {@link ProxyInfo} for the current global HTTP proxy, or if no
+     *         global HTTP proxy is set, {@code ProxyInfo} for {@code network},
+     *         or when {@code network} is {@code null},
+     *         the {@code ProxyInfo} for the default active network.  Returns
+     *         {@code null} when no proxy applies or the caller doesn't have
+     *         permission to use {@code network}.
+     * @hide
+     */
+    public ProxyInfo getProxyForNetwork(Network network) {
+        try {
+            return mService.getProxyForNetwork(network);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Get the current default HTTP proxy settings.  If a global proxy is set it will be returned,
+     * otherwise if this process is bound to a {@link Network} using
+     * {@link #bindProcessToNetwork} then that {@code Network}'s proxy is returned, otherwise
+     * the default network's proxy is returned.
+     *
+     * @return the {@link ProxyInfo} for the current HTTP proxy, or {@code null} if no
+     *        HTTP proxy is active.
+     */
+    @Nullable
+    public ProxyInfo getDefaultProxy() {
+        return getProxyForNetwork(getBoundNetworkForProcess());
+    }
+
+    /**
+     * Returns true if the hardware supports the given network type
+     * else it returns false.  This doesn't indicate we have coverage
+     * or are authorized onto a network, just whether or not the
+     * hardware supports it.  For example a GSM phone without a SIM
+     * should still return {@code true} for mobile data, but a wifi only
+     * tablet would return {@code false}.
+     *
+     * @param networkType The network type we'd like to check
+     * @return {@code true} if supported, else {@code false}
+     * @deprecated Types are deprecated. Use {@link NetworkCapabilities} instead.
+     * @hide
+     */
+    @Deprecated
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562)
+    public boolean isNetworkSupported(int networkType) {
+        try {
+            return mService.isNetworkSupported(networkType);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns if the currently active data network is metered. A network is
+     * classified as metered when the user is sensitive to heavy data usage on
+     * that connection due to monetary costs, data limitations or
+     * battery/performance issues. You should check this before doing large
+     * data transfers, and warn the user or delay the operation until another
+     * network is available.
+     *
+     * @return {@code true} if large transfers should be avoided, otherwise
+     *        {@code false}.
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    public boolean isActiveNetworkMetered() {
+        try {
+            return mService.isActiveNetworkMetered();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * If the LockdownVpn mechanism is enabled, updates the vpn
+     * with a reload of its profile.
+     *
+     * @return a boolean with {@code} indicating success
+     *
+     * <p>This method can only be called by the system UID
+     * {@hide}
+     */
+    public boolean updateLockdownVpn() {
+        try {
+            return mService.updateLockdownVpn();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Check mobile provisioning.
+     *
+     * @param suggestedTimeOutMs, timeout in milliseconds
+     *
+     * @return time out that will be used, maybe less that suggestedTimeOutMs
+     * -1 if an error.
+     *
+     * {@hide}
+     */
+    public int checkMobileProvisioning(int suggestedTimeOutMs) {
+        int timeOutMs = -1;
+        try {
+            timeOutMs = mService.checkMobileProvisioning(suggestedTimeOutMs);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+        return timeOutMs;
+    }
+
+    /**
+     * Get the mobile provisioning url.
+     * {@hide}
+     */
+    public String getMobileProvisioningUrl() {
+        try {
+            return mService.getMobileProvisioningUrl();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Set sign in error notification to visible or in visible
+     *
+     * {@hide}
+     * @deprecated Doesn't properly deal with multiple connected networks of the same type.
+     */
+    @Deprecated
+    public void setProvisioningNotificationVisible(boolean visible, int networkType,
+            String action) {
+        try {
+            mService.setProvisioningNotificationVisible(visible, networkType, action);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Set the value for enabling/disabling airplane mode
+     *
+     * @param enable whether to enable airplane mode or not
+     *
+     * @hide
+     */
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.NETWORK_SETTINGS,
+            android.Manifest.permission.NETWORK_SETUP_WIZARD,
+            android.Manifest.permission.NETWORK_STACK})
+    @SystemApi
+    public void setAirplaneMode(boolean enable) {
+        try {
+            mService.setAirplaneMode(enable);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /** {@hide} - returns the factory serial number */
+    @UnsupportedAppUsage
+    public int registerNetworkFactory(Messenger messenger, String name) {
+        try {
+            return mService.registerNetworkFactory(messenger, name);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /** {@hide} */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    public void unregisterNetworkFactory(Messenger messenger) {
+        try {
+            mService.unregisterNetworkFactory(messenger);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    // TODO : remove this method. It is a stopgap measure to help sheperding a number
+    // of dependent changes that would conflict throughout the automerger graph. Having this
+    // temporarily helps with the process of going through with all these dependent changes across
+    // the entire tree.
+    /**
+     * @hide
+     * Register a NetworkAgent with ConnectivityService.
+     * @return NetID corresponding to NetworkAgent.
+     */
+    public int registerNetworkAgent(Messenger messenger, NetworkInfo ni, LinkProperties lp,
+            NetworkCapabilities nc, int score, NetworkMisc misc) {
+        return registerNetworkAgent(messenger, ni, lp, nc, score, misc,
+                NetworkFactory.SerialNumber.NONE);
+    }
+
+    /**
+     * @hide
+     * Register a NetworkAgent with ConnectivityService.
+     * @return NetID corresponding to NetworkAgent.
+     */
+    public int registerNetworkAgent(Messenger messenger, NetworkInfo ni, LinkProperties lp,
+            NetworkCapabilities nc, int score, NetworkMisc misc, int factorySerialNumber) {
+        try {
+            return mService.registerNetworkAgent(messenger, ni, lp, nc, score, misc,
+                    factorySerialNumber);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Base class for {@code NetworkRequest} callbacks. Used for notifications about network
+     * changes. Should be extended by applications wanting notifications.
+     *
+     * A {@code NetworkCallback} is registered by calling
+     * {@link #requestNetwork(NetworkRequest, NetworkCallback)},
+     * {@link #registerNetworkCallback(NetworkRequest, NetworkCallback)},
+     * or {@link #registerDefaultNetworkCallback(NetworkCallback)}. A {@code NetworkCallback} is
+     * unregistered by calling {@link #unregisterNetworkCallback(NetworkCallback)}.
+     * A {@code NetworkCallback} should be registered at most once at any time.
+     * A {@code NetworkCallback} that has been unregistered can be registered again.
+     */
+    public static class NetworkCallback {
+        /**
+         * Called when the framework connects to a new network to evaluate whether it satisfies this
+         * request. If evaluation succeeds, this callback may be followed by an {@link #onAvailable}
+         * callback. There is no guarantee that this new network will satisfy any requests, or that
+         * the network will stay connected for longer than the time necessary to evaluate it.
+         * <p>
+         * Most applications <b>should not</b> act on this callback, and should instead use
+         * {@link #onAvailable}. This callback is intended for use by applications that can assist
+         * the framework in properly evaluating the network &mdash; for example, an application that
+         * can automatically log in to a captive portal without user intervention.
+         *
+         * @param network The {@link Network} of the network that is being evaluated.
+         *
+         * @hide
+         */
+        public void onPreCheck(@NonNull Network network) {}
+
+        /**
+         * Called when the framework connects and has declared a new network ready for use.
+         * This callback may be called more than once if the {@link Network} that is
+         * satisfying the request changes.
+         *
+         * @param network The {@link Network} of the satisfying network.
+         * @param networkCapabilities The {@link NetworkCapabilities} of the satisfying network.
+         * @param linkProperties The {@link LinkProperties} of the satisfying network.
+         * @param blocked Whether access to the {@link Network} is blocked due to system policy.
+         * @hide
+         */
+        public void onAvailable(@NonNull Network network,
+                @NonNull NetworkCapabilities networkCapabilities,
+                @NonNull LinkProperties linkProperties, boolean blocked) {
+            // Internally only this method is called when a new network is available, and
+            // it calls the callback in the same way and order that older versions used
+            // to call so as not to change the behavior.
+            onAvailable(network);
+            if (!networkCapabilities.hasCapability(
+                    NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED)) {
+                onNetworkSuspended(network);
+            }
+            onCapabilitiesChanged(network, networkCapabilities);
+            onLinkPropertiesChanged(network, linkProperties);
+            onBlockedStatusChanged(network, blocked);
+        }
+
+        /**
+         * Called when the framework connects and has declared a new network ready for use.
+         * This callback may be called more than once if the {@link Network} that is
+         * satisfying the request changes. This will always immediately be followed by a
+         * call to {@link #onCapabilitiesChanged(Network, NetworkCapabilities)} then by a
+         * call to {@link #onLinkPropertiesChanged(Network, LinkProperties)}, and a call to
+         * {@link #onBlockedStatusChanged(Network, boolean)}.
+         *
+         * @param network The {@link Network} of the satisfying network.
+         */
+        public void onAvailable(@NonNull Network network) {}
+
+        /**
+         * Called when the network is about to be disconnected.  Often paired with an
+         * {@link NetworkCallback#onAvailable} call with the new replacement network
+         * for graceful handover.  This may not be called if we have a hard loss
+         * (loss without warning).  This may be followed by either a
+         * {@link NetworkCallback#onLost} call or a
+         * {@link NetworkCallback#onAvailable} call for this network depending
+         * on whether we lose or regain it.
+         *
+         * @param network The {@link Network} that is about to be disconnected.
+         * @param maxMsToLive The time in ms the framework will attempt to keep the
+         *                     network connected.  Note that the network may suffer a
+         *                     hard loss at any time.
+         */
+        public void onLosing(@NonNull Network network, int maxMsToLive) {}
+
+        /**
+         * Called when the framework has a hard loss of the network or when the
+         * graceful failure ends.
+         *
+         * @param network The {@link Network} lost.
+         */
+        public void onLost(@NonNull Network network) {}
+
+        /**
+         * Called if no network is found in the timeout time specified in
+         * {@link #requestNetwork(NetworkRequest, NetworkCallback, int)} call or if the
+         * requested network request cannot be fulfilled (whether or not a timeout was
+         * specified). When this callback is invoked the associated
+         * {@link NetworkRequest} will have already been removed and released, as if
+         * {@link #unregisterNetworkCallback(NetworkCallback)} had been called.
+         */
+        public void onUnavailable() {}
+
+        /**
+         * Called when the network the framework connected to for this request
+         * changes capabilities but still satisfies the stated need.
+         *
+         * @param network The {@link Network} whose capabilities have changed.
+         * @param networkCapabilities The new {@link android.net.NetworkCapabilities} for this
+         *                            network.
+         */
+        public void onCapabilitiesChanged(@NonNull Network network,
+                @NonNull NetworkCapabilities networkCapabilities) {}
+
+        /**
+         * Called when the network the framework connected to for this request
+         * changes {@link LinkProperties}.
+         *
+         * @param network The {@link Network} whose link properties have changed.
+         * @param linkProperties The new {@link LinkProperties} for this network.
+         */
+        public void onLinkPropertiesChanged(@NonNull Network network,
+                @NonNull LinkProperties linkProperties) {}
+
+        /**
+         * Called when the network the framework connected to for this request
+         * goes into {@link NetworkInfo.State#SUSPENDED}.
+         * This generally means that while the TCP connections are still live,
+         * temporarily network data fails to transfer.  Specifically this is used
+         * on cellular networks to mask temporary outages when driving through
+         * a tunnel, etc.
+         * @hide
+         */
+        public void onNetworkSuspended(@NonNull Network network) {}
+
+        /**
+         * Called when the network the framework connected to for this request
+         * returns from a {@link NetworkInfo.State#SUSPENDED} state. This should always be
+         * preceded by a matching {@link NetworkCallback#onNetworkSuspended} call.
+         * @hide
+         */
+        public void onNetworkResumed(@NonNull Network network) {}
+
+        /**
+         * Called when access to the specified network is blocked or unblocked.
+         *
+         * @param network The {@link Network} whose blocked status has changed.
+         * @param blocked The blocked status of this {@link Network}.
+         */
+        public void onBlockedStatusChanged(@NonNull Network network, boolean blocked) {}
+
+        private NetworkRequest networkRequest;
+    }
+
+    /**
+     * Constant error codes used by ConnectivityService to communicate about failures and errors
+     * across a Binder boundary.
+     * @hide
+     */
+    public interface Errors {
+        int TOO_MANY_REQUESTS = 1;
+    }
+
+    /** @hide */
+    public static class TooManyRequestsException extends RuntimeException {}
+
+    private static RuntimeException convertServiceException(ServiceSpecificException e) {
+        switch (e.errorCode) {
+            case Errors.TOO_MANY_REQUESTS:
+                return new TooManyRequestsException();
+            default:
+                Log.w(TAG, "Unknown service error code " + e.errorCode);
+                return new RuntimeException(e);
+        }
+    }
+
+    private static final int BASE = Protocol.BASE_CONNECTIVITY_MANAGER;
+    /** @hide */
+    public static final int CALLBACK_PRECHECK            = BASE + 1;
+    /** @hide */
+    public static final int CALLBACK_AVAILABLE           = BASE + 2;
+    /** @hide arg1 = TTL */
+    public static final int CALLBACK_LOSING              = BASE + 3;
+    /** @hide */
+    public static final int CALLBACK_LOST                = BASE + 4;
+    /** @hide */
+    public static final int CALLBACK_UNAVAIL             = BASE + 5;
+    /** @hide */
+    public static final int CALLBACK_CAP_CHANGED         = BASE + 6;
+    /** @hide */
+    public static final int CALLBACK_IP_CHANGED          = BASE + 7;
+    /** @hide obj = NetworkCapabilities, arg1 = seq number */
+    private static final int EXPIRE_LEGACY_REQUEST       = BASE + 8;
+    /** @hide */
+    public static final int CALLBACK_SUSPENDED           = BASE + 9;
+    /** @hide */
+    public static final int CALLBACK_RESUMED             = BASE + 10;
+    /** @hide */
+    public static final int CALLBACK_BLK_CHANGED         = BASE + 11;
+
+    /** @hide */
+    public static String getCallbackName(int whichCallback) {
+        switch (whichCallback) {
+            case CALLBACK_PRECHECK:     return "CALLBACK_PRECHECK";
+            case CALLBACK_AVAILABLE:    return "CALLBACK_AVAILABLE";
+            case CALLBACK_LOSING:       return "CALLBACK_LOSING";
+            case CALLBACK_LOST:         return "CALLBACK_LOST";
+            case CALLBACK_UNAVAIL:      return "CALLBACK_UNAVAIL";
+            case CALLBACK_CAP_CHANGED:  return "CALLBACK_CAP_CHANGED";
+            case CALLBACK_IP_CHANGED:   return "CALLBACK_IP_CHANGED";
+            case EXPIRE_LEGACY_REQUEST: return "EXPIRE_LEGACY_REQUEST";
+            case CALLBACK_SUSPENDED:    return "CALLBACK_SUSPENDED";
+            case CALLBACK_RESUMED:      return "CALLBACK_RESUMED";
+            case CALLBACK_BLK_CHANGED:  return "CALLBACK_BLK_CHANGED";
+            default:
+                return Integer.toString(whichCallback);
+        }
+    }
+
+    private class CallbackHandler extends Handler {
+        private static final String TAG = "ConnectivityManager.CallbackHandler";
+        private static final boolean DBG = false;
+
+        CallbackHandler(Looper looper) {
+            super(looper);
+        }
+
+        CallbackHandler(Handler handler) {
+            this(Preconditions.checkNotNull(handler, "Handler cannot be null.").getLooper());
+        }
+
+        @Override
+        public void handleMessage(Message message) {
+            if (message.what == EXPIRE_LEGACY_REQUEST) {
+                expireRequest((NetworkCapabilities) message.obj, message.arg1);
+                return;
+            }
+
+            final NetworkRequest request = getObject(message, NetworkRequest.class);
+            final Network network = getObject(message, Network.class);
+            final NetworkCallback callback;
+            synchronized (sCallbacks) {
+                callback = sCallbacks.get(request);
+                if (callback == null) {
+                    Log.w(TAG,
+                            "callback not found for " + getCallbackName(message.what) + " message");
+                    return;
+                }
+                if (message.what == CALLBACK_UNAVAIL) {
+                    sCallbacks.remove(request);
+                    callback.networkRequest = ALREADY_UNREGISTERED;
+                }
+            }
+            if (DBG) {
+                Log.d(TAG, getCallbackName(message.what) + " for network " + network);
+            }
+
+            switch (message.what) {
+                case CALLBACK_PRECHECK: {
+                    callback.onPreCheck(network);
+                    break;
+                }
+                case CALLBACK_AVAILABLE: {
+                    NetworkCapabilities cap = getObject(message, NetworkCapabilities.class);
+                    LinkProperties lp = getObject(message, LinkProperties.class);
+                    callback.onAvailable(network, cap, lp, message.arg1 != 0);
+                    break;
+                }
+                case CALLBACK_LOSING: {
+                    callback.onLosing(network, message.arg1);
+                    break;
+                }
+                case CALLBACK_LOST: {
+                    callback.onLost(network);
+                    break;
+                }
+                case CALLBACK_UNAVAIL: {
+                    callback.onUnavailable();
+                    break;
+                }
+                case CALLBACK_CAP_CHANGED: {
+                    NetworkCapabilities cap = getObject(message, NetworkCapabilities.class);
+                    callback.onCapabilitiesChanged(network, cap);
+                    break;
+                }
+                case CALLBACK_IP_CHANGED: {
+                    LinkProperties lp = getObject(message, LinkProperties.class);
+                    callback.onLinkPropertiesChanged(network, lp);
+                    break;
+                }
+                case CALLBACK_SUSPENDED: {
+                    callback.onNetworkSuspended(network);
+                    break;
+                }
+                case CALLBACK_RESUMED: {
+                    callback.onNetworkResumed(network);
+                    break;
+                }
+                case CALLBACK_BLK_CHANGED: {
+                    boolean blocked = message.arg1 != 0;
+                    callback.onBlockedStatusChanged(network, blocked);
+                }
+            }
+        }
+
+        private <T> T getObject(Message msg, Class<T> c) {
+            return (T) msg.getData().getParcelable(c.getSimpleName());
+        }
+    }
+
+    private CallbackHandler getDefaultHandler() {
+        synchronized (sCallbacks) {
+            if (sCallbackHandler == null) {
+                sCallbackHandler = new CallbackHandler(ConnectivityThread.getInstanceLooper());
+            }
+            return sCallbackHandler;
+        }
+    }
+
+    private static final HashMap<NetworkRequest, NetworkCallback> sCallbacks = new HashMap<>();
+    private static CallbackHandler sCallbackHandler;
+
+    private static final int LISTEN  = 1;
+    private static final int REQUEST = 2;
+
+    private NetworkRequest sendRequestForNetwork(NetworkCapabilities need, NetworkCallback callback,
+            int timeoutMs, int action, int legacyType, CallbackHandler handler) {
+        printStackTrace();
+        checkCallbackNotNull(callback);
+        Preconditions.checkArgument(action == REQUEST || need != null, "null NetworkCapabilities");
+        final NetworkRequest request;
+        try {
+            synchronized(sCallbacks) {
+                if (callback.networkRequest != null
+                        && callback.networkRequest != ALREADY_UNREGISTERED) {
+                    // TODO: throw exception instead and enforce 1:1 mapping of callbacks
+                    // and requests (http://b/20701525).
+                    Log.e(TAG, "NetworkCallback was already registered");
+                }
+                Messenger messenger = new Messenger(handler);
+                Binder binder = new Binder();
+                if (action == LISTEN) {
+                    request = mService.listenForNetwork(need, messenger, binder);
+                } else {
+                    request = mService.requestNetwork(
+                            need, messenger, timeoutMs, binder, legacyType);
+                }
+                if (request != null) {
+                    sCallbacks.put(request, callback);
+                }
+                callback.networkRequest = request;
+            }
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        } catch (ServiceSpecificException e) {
+            throw convertServiceException(e);
+        }
+        return request;
+    }
+
+    /**
+     * Helper function to request a network with a particular legacy type.
+     *
+     * This is temporarily public @hide so it can be called by system code that uses the
+     * NetworkRequest API to request networks but relies on CONNECTIVITY_ACTION broadcasts for
+     * instead network notifications.
+     *
+     * TODO: update said system code to rely on NetworkCallbacks and make this method private.
+     *
+     * @hide
+     */
+    public void requestNetwork(@NonNull NetworkRequest request,
+            @NonNull NetworkCallback networkCallback, int timeoutMs, int legacyType,
+            @NonNull Handler handler) {
+        CallbackHandler cbHandler = new CallbackHandler(handler);
+        NetworkCapabilities nc = request.networkCapabilities;
+        sendRequestForNetwork(nc, networkCallback, timeoutMs, REQUEST, legacyType, cbHandler);
+    }
+
+    /**
+     * Request a network to satisfy a set of {@link android.net.NetworkCapabilities}.
+     *
+     * This {@link NetworkRequest} will live until released via
+     * {@link #unregisterNetworkCallback(NetworkCallback)} or the calling application exits. A
+     * version of the method which takes a timeout is
+     * {@link #requestNetwork(NetworkRequest, NetworkCallback, int)}.
+     * Status of the request can be followed by listening to the various
+     * callbacks described in {@link NetworkCallback}.  The {@link Network}
+     * can be used to direct traffic to the network.
+     * <p>It is presently unsupported to request a network with mutable
+     * {@link NetworkCapabilities} such as
+     * {@link NetworkCapabilities#NET_CAPABILITY_VALIDATED} or
+     * {@link NetworkCapabilities#NET_CAPABILITY_CAPTIVE_PORTAL}
+     * as these {@code NetworkCapabilities} represent states that a particular
+     * network may never attain, and whether a network will attain these states
+     * is unknown prior to bringing up the network so the framework does not
+     * know how to go about satisfing a request with these capabilities.
+     *
+     * <p>This method requires the caller to hold either the
+     * {@link android.Manifest.permission#CHANGE_NETWORK_STATE} permission
+     * or the ability to modify system settings as determined by
+     * {@link android.provider.Settings.System#canWrite}.</p>
+     *
+     * @param request {@link NetworkRequest} describing this request.
+     * @param networkCallback The {@link NetworkCallback} to be utilized for this request. Note
+     *                        the callback must not be shared - it uniquely specifies this request.
+     *                        The callback is invoked on the default internal Handler.
+     * @throws IllegalArgumentException if {@code request} contains invalid network capabilities.
+     * @throws SecurityException if missing the appropriate permissions.
+     * @throws RuntimeException if request limit per UID is exceeded.
+     */
+    public void requestNetwork(@NonNull NetworkRequest request,
+            @NonNull NetworkCallback networkCallback) {
+        requestNetwork(request, networkCallback, getDefaultHandler());
+    }
+
+    /**
+     * Request a network to satisfy a set of {@link android.net.NetworkCapabilities}.
+     *
+     * This {@link NetworkRequest} will live until released via
+     * {@link #unregisterNetworkCallback(NetworkCallback)} or the calling application exits. A
+     * version of the method which takes a timeout is
+     * {@link #requestNetwork(NetworkRequest, NetworkCallback, int)}.
+     * Status of the request can be followed by listening to the various
+     * callbacks described in {@link NetworkCallback}.  The {@link Network}
+     * can be used to direct traffic to the network.
+     * <p>It is presently unsupported to request a network with mutable
+     * {@link NetworkCapabilities} such as
+     * {@link NetworkCapabilities#NET_CAPABILITY_VALIDATED} or
+     * {@link NetworkCapabilities#NET_CAPABILITY_CAPTIVE_PORTAL}
+     * as these {@code NetworkCapabilities} represent states that a particular
+     * network may never attain, and whether a network will attain these states
+     * is unknown prior to bringing up the network so the framework does not
+     * know how to go about satisfying a request with these capabilities.
+     *
+     * <p>This method requires the caller to hold either the
+     * {@link android.Manifest.permission#CHANGE_NETWORK_STATE} permission
+     * or the ability to modify system settings as determined by
+     * {@link android.provider.Settings.System#canWrite}.</p>
+     *
+     * @param request {@link NetworkRequest} describing this request.
+     * @param networkCallback The {@link NetworkCallback} to be utilized for this request. Note
+     *                        the callback must not be shared - it uniquely specifies this request.
+     * @param handler {@link Handler} to specify the thread upon which the callback will be invoked.
+     * @throws IllegalArgumentException if {@code request} contains invalid network capabilities.
+     * @throws SecurityException if missing the appropriate permissions.
+     * @throws RuntimeException if request limit per UID is exceeded.
+     */
+    public void requestNetwork(@NonNull NetworkRequest request,
+            @NonNull NetworkCallback networkCallback, @NonNull Handler handler) {
+        int legacyType = inferLegacyTypeForNetworkCapabilities(request.networkCapabilities);
+        CallbackHandler cbHandler = new CallbackHandler(handler);
+        requestNetwork(request, networkCallback, 0, legacyType, cbHandler);
+    }
+
+    /**
+     * Request a network to satisfy a set of {@link android.net.NetworkCapabilities}, limited
+     * by a timeout.
+     *
+     * This function behaves identically to the non-timed-out version
+     * {@link #requestNetwork(NetworkRequest, NetworkCallback)}, but if a suitable network
+     * is not found within the given time (in milliseconds) the
+     * {@link NetworkCallback#onUnavailable()} callback is called. The request can still be
+     * released normally by calling {@link #unregisterNetworkCallback(NetworkCallback)} but does
+     * not have to be released if timed-out (it is automatically released). Unregistering a
+     * request that timed out is not an error.
+     *
+     * <p>Do not use this method to poll for the existence of specific networks (e.g. with a small
+     * timeout) - {@link #registerNetworkCallback(NetworkRequest, NetworkCallback)} is provided
+     * for that purpose. Calling this method will attempt to bring up the requested network.
+     *
+     * <p>This method requires the caller to hold either the
+     * {@link android.Manifest.permission#CHANGE_NETWORK_STATE} permission
+     * or the ability to modify system settings as determined by
+     * {@link android.provider.Settings.System#canWrite}.</p>
+     *
+     * @param request {@link NetworkRequest} describing this request.
+     * @param networkCallback The {@link NetworkCallback} to be utilized for this request. Note
+     *                        the callback must not be shared - it uniquely specifies this request.
+     * @param timeoutMs The time in milliseconds to attempt looking for a suitable network
+     *                  before {@link NetworkCallback#onUnavailable()} is called. The timeout must
+     *                  be a positive value (i.e. >0).
+     * @throws IllegalArgumentException if {@code request} contains invalid network capabilities.
+     * @throws SecurityException if missing the appropriate permissions.
+     * @throws RuntimeException if request limit per UID is exceeded.
+     */
+    public void requestNetwork(@NonNull NetworkRequest request,
+            @NonNull NetworkCallback networkCallback, int timeoutMs) {
+        checkTimeout(timeoutMs);
+        int legacyType = inferLegacyTypeForNetworkCapabilities(request.networkCapabilities);
+        requestNetwork(request, networkCallback, timeoutMs, legacyType, getDefaultHandler());
+    }
+
+    /**
+     * Request a network to satisfy a set of {@link android.net.NetworkCapabilities}, limited
+     * by a timeout.
+     *
+     * This function behaves identically to the version without timeout, but if a suitable
+     * network is not found within the given time (in milliseconds) the
+     * {@link NetworkCallback#onUnavailable} callback is called. The request can still be
+     * released normally by calling {@link #unregisterNetworkCallback(NetworkCallback)} but does
+     * not have to be released if timed-out (it is automatically released). Unregistering a
+     * request that timed out is not an error.
+     *
+     * <p>Do not use this method to poll for the existence of specific networks (e.g. with a small
+     * timeout) - {@link #registerNetworkCallback(NetworkRequest, NetworkCallback)} is provided
+     * for that purpose. Calling this method will attempt to bring up the requested network.
+     *
+     * <p>This method requires the caller to hold either the
+     * {@link android.Manifest.permission#CHANGE_NETWORK_STATE} permission
+     * or the ability to modify system settings as determined by
+     * {@link android.provider.Settings.System#canWrite}.</p>
+     *
+     * @param request {@link NetworkRequest} describing this request.
+     * @param networkCallback The {@link NetworkCallback} to be utilized for this request. Note
+     *                        the callback must not be shared - it uniquely specifies this request.
+     * @param handler {@link Handler} to specify the thread upon which the callback will be invoked.
+     * @param timeoutMs The time in milliseconds to attempt looking for a suitable network
+     *                  before {@link NetworkCallback#onUnavailable} is called.
+     * @throws IllegalArgumentException if {@code request} contains invalid network capabilities.
+     * @throws SecurityException if missing the appropriate permissions.
+     * @throws RuntimeException if request limit per UID is exceeded.
+     */
+    public void requestNetwork(@NonNull NetworkRequest request,
+            @NonNull NetworkCallback networkCallback, @NonNull Handler handler, int timeoutMs) {
+        checkTimeout(timeoutMs);
+        int legacyType = inferLegacyTypeForNetworkCapabilities(request.networkCapabilities);
+        CallbackHandler cbHandler = new CallbackHandler(handler);
+        requestNetwork(request, networkCallback, timeoutMs, legacyType, cbHandler);
+    }
+
+    /**
+     * The lookup key for a {@link Network} object included with the intent after
+     * successfully finding a network for the applications request.  Retrieve it with
+     * {@link android.content.Intent#getParcelableExtra(String)}.
+     * <p>
+     * Note that if you intend to invoke {@link Network#openConnection(java.net.URL)}
+     * then you must get a ConnectivityManager instance before doing so.
+     */
+    public static final String EXTRA_NETWORK = "android.net.extra.NETWORK";
+
+    /**
+     * The lookup key for a {@link NetworkRequest} object included with the intent after
+     * successfully finding a network for the applications request.  Retrieve it with
+     * {@link android.content.Intent#getParcelableExtra(String)}.
+     */
+    public static final String EXTRA_NETWORK_REQUEST = "android.net.extra.NETWORK_REQUEST";
+
+
+    /**
+     * Request a network to satisfy a set of {@link android.net.NetworkCapabilities}.
+     *
+     * This function behaves identically to the version that takes a NetworkCallback, but instead
+     * of {@link NetworkCallback} a {@link PendingIntent} is used.  This means
+     * the request may outlive the calling application and get called back when a suitable
+     * network is found.
+     * <p>
+     * The operation is an Intent broadcast that goes to a broadcast receiver that
+     * you registered with {@link Context#registerReceiver} or through the
+     * &lt;receiver&gt; tag in an AndroidManifest.xml file
+     * <p>
+     * The operation Intent is delivered with two extras, a {@link Network} typed
+     * extra called {@link #EXTRA_NETWORK} and a {@link NetworkRequest}
+     * typed extra called {@link #EXTRA_NETWORK_REQUEST} containing
+     * the original requests parameters.  It is important to create a new,
+     * {@link NetworkCallback} based request before completing the processing of the
+     * Intent to reserve the network or it will be released shortly after the Intent
+     * is processed.
+     * <p>
+     * If there is already a request for this Intent registered (with the equality of
+     * two Intents defined by {@link Intent#filterEquals}), then it will be removed and
+     * replaced by this one, effectively releasing the previous {@link NetworkRequest}.
+     * <p>
+     * The request may be released normally by calling
+     * {@link #releaseNetworkRequest(android.app.PendingIntent)}.
+     * <p>It is presently unsupported to request a network with either
+     * {@link NetworkCapabilities#NET_CAPABILITY_VALIDATED} or
+     * {@link NetworkCapabilities#NET_CAPABILITY_CAPTIVE_PORTAL}
+     * as these {@code NetworkCapabilities} represent states that a particular
+     * network may never attain, and whether a network will attain these states
+     * is unknown prior to bringing up the network so the framework does not
+     * know how to go about satisfying a request with these capabilities.
+     *
+     * <p>This method requires the caller to hold either the
+     * {@link android.Manifest.permission#CHANGE_NETWORK_STATE} permission
+     * or the ability to modify system settings as determined by
+     * {@link android.provider.Settings.System#canWrite}.</p>
+     *
+     * @param request {@link NetworkRequest} describing this request.
+     * @param operation Action to perform when the network is available (corresponds
+     *                  to the {@link NetworkCallback#onAvailable} call.  Typically
+     *                  comes from {@link PendingIntent#getBroadcast}. Cannot be null.
+     * @throws IllegalArgumentException if {@code request} contains invalid network capabilities.
+     * @throws SecurityException if missing the appropriate permissions.
+     * @throws RuntimeException if request limit per UID is exceeded.
+     */
+    public void requestNetwork(@NonNull NetworkRequest request,
+            @NonNull PendingIntent operation) {
+        printStackTrace();
+        checkPendingIntentNotNull(operation);
+        try {
+            mService.pendingRequestForNetwork(request.networkCapabilities, operation);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        } catch (ServiceSpecificException e) {
+            throw convertServiceException(e);
+        }
+    }
+
+    /**
+     * Removes a request made via {@link #requestNetwork(NetworkRequest, android.app.PendingIntent)}
+     * <p>
+     * This method has the same behavior as
+     * {@link #unregisterNetworkCallback(android.app.PendingIntent)} with respect to
+     * releasing network resources and disconnecting.
+     *
+     * @param operation A PendingIntent equal (as defined by {@link Intent#filterEquals}) to the
+     *                  PendingIntent passed to
+     *                  {@link #requestNetwork(NetworkRequest, android.app.PendingIntent)} with the
+     *                  corresponding NetworkRequest you'd like to remove. Cannot be null.
+     */
+    public void releaseNetworkRequest(@NonNull PendingIntent operation) {
+        printStackTrace();
+        checkPendingIntentNotNull(operation);
+        try {
+            mService.releasePendingNetworkRequest(operation);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    private static void checkPendingIntentNotNull(PendingIntent intent) {
+        Preconditions.checkNotNull(intent, "PendingIntent cannot be null.");
+    }
+
+    private static void checkCallbackNotNull(NetworkCallback callback) {
+        Preconditions.checkNotNull(callback, "null NetworkCallback");
+    }
+
+    private static void checkTimeout(int timeoutMs) {
+        Preconditions.checkArgumentPositive(timeoutMs, "timeoutMs must be strictly positive.");
+    }
+
+    /**
+     * Registers to receive notifications about all networks which satisfy the given
+     * {@link NetworkRequest}.  The callbacks will continue to be called until
+     * either the application exits or {@link #unregisterNetworkCallback(NetworkCallback)} is
+     * called.
+     *
+     * @param request {@link NetworkRequest} describing this request.
+     * @param networkCallback The {@link NetworkCallback} that the system will call as suitable
+     *                        networks change state.
+     *                        The callback is invoked on the default internal Handler.
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    public void registerNetworkCallback(@NonNull NetworkRequest request,
+            @NonNull NetworkCallback networkCallback) {
+        registerNetworkCallback(request, networkCallback, getDefaultHandler());
+    }
+
+    /**
+     * Registers to receive notifications about all networks which satisfy the given
+     * {@link NetworkRequest}.  The callbacks will continue to be called until
+     * either the application exits or {@link #unregisterNetworkCallback(NetworkCallback)} is
+     * called.
+     *
+     * @param request {@link NetworkRequest} describing this request.
+     * @param networkCallback The {@link NetworkCallback} that the system will call as suitable
+     *                        networks change state.
+     * @param handler {@link Handler} to specify the thread upon which the callback will be invoked.
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    public void registerNetworkCallback(@NonNull NetworkRequest request,
+            @NonNull NetworkCallback networkCallback, @NonNull Handler handler) {
+        CallbackHandler cbHandler = new CallbackHandler(handler);
+        NetworkCapabilities nc = request.networkCapabilities;
+        sendRequestForNetwork(nc, networkCallback, 0, LISTEN, TYPE_NONE, cbHandler);
+    }
+
+    /**
+     * Registers a PendingIntent to be sent when a network is available which satisfies the given
+     * {@link NetworkRequest}.
+     *
+     * This function behaves identically to the version that takes a NetworkCallback, but instead
+     * of {@link NetworkCallback} a {@link PendingIntent} is used.  This means
+     * the request may outlive the calling application and get called back when a suitable
+     * network is found.
+     * <p>
+     * The operation is an Intent broadcast that goes to a broadcast receiver that
+     * you registered with {@link Context#registerReceiver} or through the
+     * &lt;receiver&gt; tag in an AndroidManifest.xml file
+     * <p>
+     * The operation Intent is delivered with two extras, a {@link Network} typed
+     * extra called {@link #EXTRA_NETWORK} and a {@link NetworkRequest}
+     * typed extra called {@link #EXTRA_NETWORK_REQUEST} containing
+     * the original requests parameters.
+     * <p>
+     * If there is already a request for this Intent registered (with the equality of
+     * two Intents defined by {@link Intent#filterEquals}), then it will be removed and
+     * replaced by this one, effectively releasing the previous {@link NetworkRequest}.
+     * <p>
+     * The request may be released normally by calling
+     * {@link #unregisterNetworkCallback(android.app.PendingIntent)}.
+     * @param request {@link NetworkRequest} describing this request.
+     * @param operation Action to perform when the network is available (corresponds
+     *                  to the {@link NetworkCallback#onAvailable} call.  Typically
+     *                  comes from {@link PendingIntent#getBroadcast}. Cannot be null.
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    public void registerNetworkCallback(@NonNull NetworkRequest request,
+            @NonNull PendingIntent operation) {
+        printStackTrace();
+        checkPendingIntentNotNull(operation);
+        try {
+            mService.pendingListenForNetwork(request.networkCapabilities, operation);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        } catch (ServiceSpecificException e) {
+            throw convertServiceException(e);
+        }
+    }
+
+    /**
+     * Registers to receive notifications about changes in the system default network. The callbacks
+     * will continue to be called until either the application exits or
+     * {@link #unregisterNetworkCallback(NetworkCallback)} is called.
+     *
+     * @param networkCallback The {@link NetworkCallback} that the system will call as the
+     *                        system default network changes.
+     *                        The callback is invoked on the default internal Handler.
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    public void registerDefaultNetworkCallback(@NonNull NetworkCallback networkCallback) {
+        registerDefaultNetworkCallback(networkCallback, getDefaultHandler());
+    }
+
+    /**
+     * Registers to receive notifications about changes in the system default network. The callbacks
+     * will continue to be called until either the application exits or
+     * {@link #unregisterNetworkCallback(NetworkCallback)} is called.
+     *
+     * @param networkCallback The {@link NetworkCallback} that the system will call as the
+     *                        system default network changes.
+     * @param handler {@link Handler} to specify the thread upon which the callback will be invoked.
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    public void registerDefaultNetworkCallback(@NonNull NetworkCallback networkCallback,
+            @NonNull Handler handler) {
+        // This works because if the NetworkCapabilities are null,
+        // ConnectivityService takes them from the default request.
+        //
+        // Since the capabilities are exactly the same as the default request's
+        // capabilities, this request is guaranteed, at all times, to be
+        // satisfied by the same network, if any, that satisfies the default
+        // request, i.e., the system default network.
+        CallbackHandler cbHandler = new CallbackHandler(handler);
+        sendRequestForNetwork(null /* NetworkCapabilities need */, networkCallback, 0,
+                REQUEST, TYPE_NONE, cbHandler);
+    }
+
+    /**
+     * Requests bandwidth update for a given {@link Network} and returns whether the update request
+     * is accepted by ConnectivityService. Once accepted, ConnectivityService will poll underlying
+     * network connection for updated bandwidth information. The caller will be notified via
+     * {@link ConnectivityManager.NetworkCallback} if there is an update. Notice that this
+     * method assumes that the caller has previously called
+     * {@link #registerNetworkCallback(NetworkRequest, NetworkCallback)} to listen for network
+     * changes.
+     *
+     * @param network {@link Network} specifying which network you're interested.
+     * @return {@code true} on success, {@code false} if the {@link Network} is no longer valid.
+     */
+    public boolean requestBandwidthUpdate(@NonNull Network network) {
+        try {
+            return mService.requestBandwidthUpdate(network);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Unregisters a {@code NetworkCallback} and possibly releases networks originating from
+     * {@link #requestNetwork(NetworkRequest, NetworkCallback)} and
+     * {@link #registerNetworkCallback(NetworkRequest, NetworkCallback)} calls.
+     * If the given {@code NetworkCallback} had previously been used with
+     * {@code #requestNetwork}, any networks that had been connected to only to satisfy that request
+     * will be disconnected.
+     *
+     * Notifications that would have triggered that {@code NetworkCallback} will immediately stop
+     * triggering it as soon as this call returns.
+     *
+     * @param networkCallback The {@link NetworkCallback} used when making the request.
+     */
+    public void unregisterNetworkCallback(@NonNull NetworkCallback networkCallback) {
+        printStackTrace();
+        checkCallbackNotNull(networkCallback);
+        final List<NetworkRequest> reqs = new ArrayList<>();
+        // Find all requests associated to this callback and stop callback triggers immediately.
+        // Callback is reusable immediately. http://b/20701525, http://b/35921499.
+        synchronized (sCallbacks) {
+            Preconditions.checkArgument(networkCallback.networkRequest != null,
+                    "NetworkCallback was not registered");
+            if (networkCallback.networkRequest == ALREADY_UNREGISTERED) {
+                Log.d(TAG, "NetworkCallback was already unregistered");
+                return;
+            }
+            for (Map.Entry<NetworkRequest, NetworkCallback> e : sCallbacks.entrySet()) {
+                if (e.getValue() == networkCallback) {
+                    reqs.add(e.getKey());
+                }
+            }
+            // TODO: throw exception if callback was registered more than once (http://b/20701525).
+            for (NetworkRequest r : reqs) {
+                try {
+                    mService.releaseNetworkRequest(r);
+                } catch (RemoteException e) {
+                    throw e.rethrowFromSystemServer();
+                }
+                // Only remove mapping if rpc was successful.
+                sCallbacks.remove(r);
+            }
+            networkCallback.networkRequest = ALREADY_UNREGISTERED;
+        }
+    }
+
+    /**
+     * Unregisters a callback previously registered via
+     * {@link #registerNetworkCallback(NetworkRequest, android.app.PendingIntent)}.
+     *
+     * @param operation A PendingIntent equal (as defined by {@link Intent#filterEquals}) to the
+     *                  PendingIntent passed to
+     *                  {@link #registerNetworkCallback(NetworkRequest, android.app.PendingIntent)}.
+     *                  Cannot be null.
+     */
+    public void unregisterNetworkCallback(@NonNull PendingIntent operation) {
+        checkPendingIntentNotNull(operation);
+        releaseNetworkRequest(operation);
+    }
+
+    /**
+     * Informs the system whether it should switch to {@code network} regardless of whether it is
+     * validated or not. If {@code accept} is true, and the network was explicitly selected by the
+     * user (e.g., by selecting a Wi-Fi network in the Settings app), then the network will become
+     * the system default network regardless of any other network that's currently connected. If
+     * {@code always} is true, then the choice is remembered, so that the next time the user
+     * connects to this network, the system will switch to it.
+     *
+     * @param network The network to accept.
+     * @param accept Whether to accept the network even if unvalidated.
+     * @param always Whether to remember this choice in the future.
+     *
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.NETWORK_SETTINGS)
+    public void setAcceptUnvalidated(Network network, boolean accept, boolean always) {
+        try {
+            mService.setAcceptUnvalidated(network, accept, always);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Informs the system whether it should consider the network as validated even if it only has
+     * partial connectivity. If {@code accept} is true, then the network will be considered as
+     * validated even if connectivity is only partial. If {@code always} is true, then the choice
+     * is remembered, so that the next time the user connects to this network, the system will
+     * switch to it.
+     *
+     * @param network The network to accept.
+     * @param accept Whether to consider the network as validated even if it has partial
+     *               connectivity.
+     * @param always Whether to remember this choice in the future.
+     *
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.NETWORK_STACK)
+    public void setAcceptPartialConnectivity(Network network, boolean accept, boolean always) {
+        try {
+            mService.setAcceptPartialConnectivity(network, accept, always);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Informs the system to penalize {@code network}'s score when it becomes unvalidated. This is
+     * only meaningful if the system is configured not to penalize such networks, e.g., if the
+     * {@code config_networkAvoidBadWifi} configuration variable is set to 0 and the {@code
+     * NETWORK_AVOID_BAD_WIFI setting is unset}.
+     *
+     * @param network The network to accept.
+     *
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.NETWORK_SETTINGS)
+    public void setAvoidUnvalidated(Network network) {
+        try {
+            mService.setAvoidUnvalidated(network);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Requests that the system open the captive portal app on the specified network.
+     *
+     * @param network The network to log into.
+     *
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.CONNECTIVITY_INTERNAL)
+    public void startCaptivePortalApp(Network network) {
+        try {
+            mService.startCaptivePortalApp(network);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Requests that the system open the captive portal app with the specified extras.
+     *
+     * <p>This endpoint is exclusively for use by the NetworkStack and is protected by the
+     * corresponding permission.
+     * @param network Network on which the captive portal was detected.
+     * @param appExtras Extras to include in the app start intent.
+     * @hide
+     */
+    @SystemApi
+    @TestApi
+    @RequiresPermission(NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK)
+    public void startCaptivePortalApp(@NonNull Network network, @NonNull Bundle appExtras) {
+        try {
+            mService.startCaptivePortalAppInternal(network, appExtras);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Determine whether the device is configured to avoid bad wifi.
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(anyOf = {
+            NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK,
+            android.Manifest.permission.NETWORK_STACK})
+    public boolean shouldAvoidBadWifi() {
+        try {
+            return mService.shouldAvoidBadWifi();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * It is acceptable to briefly use multipath data to provide seamless connectivity for
+     * time-sensitive user-facing operations when the system default network is temporarily
+     * unresponsive. The amount of data should be limited (less than one megabyte for every call to
+     * this method), and the operation should be infrequent to ensure that data usage is limited.
+     *
+     * An example of such an operation might be a time-sensitive foreground activity, such as a
+     * voice command, that the user is performing while walking out of range of a Wi-Fi network.
+     */
+    public static final int MULTIPATH_PREFERENCE_HANDOVER = 1 << 0;
+
+    /**
+     * It is acceptable to use small amounts of multipath data on an ongoing basis to provide
+     * a backup channel for traffic that is primarily going over another network.
+     *
+     * An example might be maintaining backup connections to peers or servers for the purpose of
+     * fast fallback if the default network is temporarily unresponsive or disconnects. The traffic
+     * on backup paths should be negligible compared to the traffic on the main path.
+     */
+    public static final int MULTIPATH_PREFERENCE_RELIABILITY = 1 << 1;
+
+    /**
+     * It is acceptable to use metered data to improve network latency and performance.
+     */
+    public static final int MULTIPATH_PREFERENCE_PERFORMANCE = 1 << 2;
+
+    /**
+     * Return value to use for unmetered networks. On such networks we currently set all the flags
+     * to true.
+     * @hide
+     */
+    public static final int MULTIPATH_PREFERENCE_UNMETERED =
+            MULTIPATH_PREFERENCE_HANDOVER |
+            MULTIPATH_PREFERENCE_RELIABILITY |
+            MULTIPATH_PREFERENCE_PERFORMANCE;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(flag = true, value = {
+            MULTIPATH_PREFERENCE_HANDOVER,
+            MULTIPATH_PREFERENCE_RELIABILITY,
+            MULTIPATH_PREFERENCE_PERFORMANCE,
+    })
+    public @interface MultipathPreference {
+    }
+
+    /**
+     * Provides a hint to the calling application on whether it is desirable to use the
+     * multinetwork APIs (e.g., {@link Network#openConnection}, {@link Network#bindSocket}, etc.)
+     * for multipath data transfer on this network when it is not the system default network.
+     * Applications desiring to use multipath network protocols should call this method before
+     * each such operation.
+     *
+     * @param network The network on which the application desires to use multipath data.
+     *                If {@code null}, this method will return the a preference that will generally
+     *                apply to metered networks.
+     * @return a bitwise OR of zero or more of the  {@code MULTIPATH_PREFERENCE_*} constants.
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_NETWORK_STATE)
+    public @MultipathPreference int getMultipathPreference(@Nullable Network network) {
+        try {
+            return mService.getMultipathPreference(network);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Resets all connectivity manager settings back to factory defaults.
+     * @hide
+     */
+    public void factoryReset() {
+        try {
+            mService.factoryReset();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Binds the current process to {@code network}.  All Sockets created in the future
+     * (and not explicitly bound via a bound SocketFactory from
+     * {@link Network#getSocketFactory() Network.getSocketFactory()}) will be bound to
+     * {@code network}.  All host name resolutions will be limited to {@code network} as well.
+     * Note that if {@code network} ever disconnects, all Sockets created in this way will cease to
+     * work and all host name resolutions will fail.  This is by design so an application doesn't
+     * accidentally use Sockets it thinks are still bound to a particular {@link Network}.
+     * To clear binding pass {@code null} for {@code network}.  Using individually bound
+     * Sockets created by Network.getSocketFactory().createSocket() and
+     * performing network-specific host name resolutions via
+     * {@link Network#getAllByName Network.getAllByName} is preferred to calling
+     * {@code bindProcessToNetwork}.
+     *
+     * @param network The {@link Network} to bind the current process to, or {@code null} to clear
+     *                the current binding.
+     * @return {@code true} on success, {@code false} if the {@link Network} is no longer valid.
+     */
+    public boolean bindProcessToNetwork(@Nullable Network network) {
+        // Forcing callers to call through non-static function ensures ConnectivityManager
+        // instantiated.
+        return setProcessDefaultNetwork(network);
+    }
+
+    /**
+     * Binds the current process to {@code network}.  All Sockets created in the future
+     * (and not explicitly bound via a bound SocketFactory from
+     * {@link Network#getSocketFactory() Network.getSocketFactory()}) will be bound to
+     * {@code network}.  All host name resolutions will be limited to {@code network} as well.
+     * Note that if {@code network} ever disconnects, all Sockets created in this way will cease to
+     * work and all host name resolutions will fail.  This is by design so an application doesn't
+     * accidentally use Sockets it thinks are still bound to a particular {@link Network}.
+     * To clear binding pass {@code null} for {@code network}.  Using individually bound
+     * Sockets created by Network.getSocketFactory().createSocket() and
+     * performing network-specific host name resolutions via
+     * {@link Network#getAllByName Network.getAllByName} is preferred to calling
+     * {@code setProcessDefaultNetwork}.
+     *
+     * @param network The {@link Network} to bind the current process to, or {@code null} to clear
+     *                the current binding.
+     * @return {@code true} on success, {@code false} if the {@link Network} is no longer valid.
+     * @deprecated This function can throw {@link IllegalStateException}.  Use
+     *             {@link #bindProcessToNetwork} instead.  {@code bindProcessToNetwork}
+     *             is a direct replacement.
+     */
+    @Deprecated
+    public static boolean setProcessDefaultNetwork(@Nullable Network network) {
+        int netId = (network == null) ? NETID_UNSET : network.netId;
+        boolean isSameNetId = (netId == NetworkUtils.getBoundNetworkForProcess());
+
+        if (netId != NETID_UNSET) {
+            netId = network.getNetIdForResolv();
+        }
+
+        if (!NetworkUtils.bindProcessToNetwork(netId)) {
+            return false;
+        }
+
+        if (!isSameNetId) {
+            // Set HTTP proxy system properties to match network.
+            // TODO: Deprecate this static method and replace it with a non-static version.
+            try {
+                Proxy.setHttpProxySystemProperty(getInstance().getDefaultProxy());
+            } catch (SecurityException e) {
+                // The process doesn't have ACCESS_NETWORK_STATE, so we can't fetch the proxy.
+                Log.e(TAG, "Can't set proxy properties", e);
+            }
+            // Must flush DNS cache as new network may have different DNS resolutions.
+            InetAddress.clearDnsCache();
+            // Must flush socket pool as idle sockets will be bound to previous network and may
+            // cause subsequent fetches to be performed on old network.
+            NetworkEventDispatcher.getInstance().onNetworkConfigurationChanged();
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns the {@link Network} currently bound to this process via
+     * {@link #bindProcessToNetwork}, or {@code null} if no {@link Network} is explicitly bound.
+     *
+     * @return {@code Network} to which this process is bound, or {@code null}.
+     */
+    @Nullable
+    public Network getBoundNetworkForProcess() {
+        // Forcing callers to call thru non-static function ensures ConnectivityManager
+        // instantiated.
+        return getProcessDefaultNetwork();
+    }
+
+    /**
+     * Returns the {@link Network} currently bound to this process via
+     * {@link #bindProcessToNetwork}, or {@code null} if no {@link Network} is explicitly bound.
+     *
+     * @return {@code Network} to which this process is bound, or {@code null}.
+     * @deprecated Using this function can lead to other functions throwing
+     *             {@link IllegalStateException}.  Use {@link #getBoundNetworkForProcess} instead.
+     *             {@code getBoundNetworkForProcess} is a direct replacement.
+     */
+    @Deprecated
+    @Nullable
+    public static Network getProcessDefaultNetwork() {
+        int netId = NetworkUtils.getBoundNetworkForProcess();
+        if (netId == NETID_UNSET) return null;
+        return new Network(netId);
+    }
+
+    private void unsupportedStartingFrom(int version) {
+        if (Process.myUid() == Process.SYSTEM_UID) {
+            // The getApplicationInfo() call we make below is not supported in system context. Let
+            // the call through here, and rely on the fact that ConnectivityService will refuse to
+            // allow the system to use these APIs anyway.
+            return;
+        }
+
+        if (mContext.getApplicationInfo().targetSdkVersion >= version) {
+            throw new UnsupportedOperationException(
+                    "This method is not supported in target SDK version " + version + " and above");
+        }
+    }
+
+    // Checks whether the calling app can use the legacy routing API (startUsingNetworkFeature,
+    // stopUsingNetworkFeature, requestRouteToHost), and if not throw UnsupportedOperationException.
+    // TODO: convert the existing system users (Tethering, GnssLocationProvider) to the new APIs and
+    // remove these exemptions. Note that this check is not secure, and apps can still access these
+    // functions by accessing ConnectivityService directly. However, it should be clear that doing
+    // so is unsupported and may break in the future. http://b/22728205
+    private void checkLegacyRoutingApiAccess() {
+        unsupportedStartingFrom(VERSION_CODES.M);
+    }
+
+    /**
+     * Binds host resolutions performed by this process to {@code network}.
+     * {@link #bindProcessToNetwork} takes precedence over this setting.
+     *
+     * @param network The {@link Network} to bind host resolutions from the current process to, or
+     *                {@code null} to clear the current binding.
+     * @return {@code true} on success, {@code false} if the {@link Network} is no longer valid.
+     * @hide
+     * @deprecated This is strictly for legacy usage to support {@link #startUsingNetworkFeature}.
+     */
+    @Deprecated
+    @UnsupportedAppUsage
+    public static boolean setProcessDefaultNetworkForHostResolution(Network network) {
+        return NetworkUtils.bindProcessToNetworkForHostResolution(
+                (network == null) ? NETID_UNSET : network.getNetIdForResolv());
+    }
+
+    /**
+     * Device is not restricting metered network activity while application is running on
+     * background.
+     */
+    public static final int RESTRICT_BACKGROUND_STATUS_DISABLED = 1;
+
+    /**
+     * Device is restricting metered network activity while application is running on background,
+     * but application is allowed to bypass it.
+     * <p>
+     * In this state, application should take action to mitigate metered network access.
+     * For example, a music streaming application should switch to a low-bandwidth bitrate.
+     */
+    public static final int RESTRICT_BACKGROUND_STATUS_WHITELISTED = 2;
+
+    /**
+     * Device is restricting metered network activity while application is running on background.
+     * <p>
+     * In this state, application should not try to use the network while running on background,
+     * because it would be denied.
+     */
+    public static final int RESTRICT_BACKGROUND_STATUS_ENABLED = 3;
+
+    /**
+     * A change in the background metered network activity restriction has occurred.
+     * <p>
+     * Applications should call {@link #getRestrictBackgroundStatus()} to check if the restriction
+     * applies to them.
+     * <p>
+     * This is only sent to registered receivers, not manifest receivers.
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String ACTION_RESTRICT_BACKGROUND_CHANGED =
+            "android.net.conn.RESTRICT_BACKGROUND_CHANGED";
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(flag = false, value = {
+            RESTRICT_BACKGROUND_STATUS_DISABLED,
+            RESTRICT_BACKGROUND_STATUS_WHITELISTED,
+            RESTRICT_BACKGROUND_STATUS_ENABLED,
+    })
+    public @interface RestrictBackgroundStatus {
+    }
+
+    private INetworkPolicyManager getNetworkPolicyManager() {
+        synchronized (this) {
+            if (mNPManager != null) {
+                return mNPManager;
+            }
+            mNPManager = INetworkPolicyManager.Stub.asInterface(ServiceManager
+                    .getService(Context.NETWORK_POLICY_SERVICE));
+            return mNPManager;
+        }
+    }
+
+    /**
+     * Determines if the calling application is subject to metered network restrictions while
+     * running on background.
+     *
+     * @return {@link #RESTRICT_BACKGROUND_STATUS_DISABLED},
+     * {@link #RESTRICT_BACKGROUND_STATUS_ENABLED},
+     * or {@link #RESTRICT_BACKGROUND_STATUS_WHITELISTED}
+     */
+    public @RestrictBackgroundStatus int getRestrictBackgroundStatus() {
+        try {
+            return getNetworkPolicyManager().getRestrictBackgroundByCaller();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * The network watchlist is a list of domains and IP addresses that are associated with
+     * potentially harmful apps. This method returns the SHA-256 of the watchlist config file
+     * currently used by the system for validation purposes.
+     *
+     * @return Hash of network watchlist config file. Null if config does not exist.
+     */
+    @Nullable
+    public byte[] getNetworkWatchlistConfigHash() {
+        try {
+            return mService.getNetworkWatchlistConfigHash();
+        } catch (RemoteException e) {
+            Log.e(TAG, "Unable to get watchlist config hash");
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns the {@code uid} of the owner of a network connection.
+     *
+     * @param protocol The protocol of the connection. Only {@code IPPROTO_TCP} and
+     * {@code IPPROTO_UDP} currently supported.
+     * @param local The local {@link InetSocketAddress} of a connection.
+     * @param remote The remote {@link InetSocketAddress} of a connection.
+     *
+     * @return {@code uid} if the connection is found and the app has permission to observe it
+     * (e.g., if it is associated with the calling VPN app's tunnel) or
+     * {@link android.os.Process#INVALID_UID} if the connection is not found.
+     * Throws {@link SecurityException} if the caller is not the active VPN for the current user.
+     * Throws {@link IllegalArgumentException} if an unsupported protocol is requested.
+     */
+    public int getConnectionOwnerUid(int protocol, @NonNull InetSocketAddress local,
+            @NonNull InetSocketAddress remote) {
+        ConnectionInfo connectionInfo = new ConnectionInfo(protocol, local, remote);
+        try {
+            return mService.getConnectionOwnerUid(connectionInfo);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    private void printStackTrace() {
+        if (DEBUG) {
+            final StackTraceElement[] callStack = Thread.currentThread().getStackTrace();
+            final StringBuffer sb = new StringBuffer();
+            for (int i = 3; i < callStack.length; i++) {
+                final String stackTrace = callStack[i].toString();
+                if (stackTrace == null || stackTrace.contains("android.os")) {
+                    break;
+                }
+                sb.append(" [").append(stackTrace).append("]");
+            }
+            Log.d(TAG, "StackLog:" + sb.toString());
+        }
+    }
+}
diff --git a/android/net/ConnectivityMetricsEvent.java b/android/net/ConnectivityMetricsEvent.java
new file mode 100644
index 0000000..522add1
--- /dev/null
+++ b/android/net/ConnectivityMetricsEvent.java
@@ -0,0 +1,95 @@
+/*
+ * 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.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.BitUtils;
+
+/**
+ * Represents a core networking event defined in package android.net.metrics.
+ * Logged by IpConnectivityLog and managed by ConnectivityMetrics service.
+ * {@hide}
+ * */
+public final class ConnectivityMetricsEvent implements Parcelable {
+
+    /** Time when this event was collected, as returned by System.currentTimeMillis(). */
+    public long timestamp;
+    /** Transports of the network associated with the event, as defined in NetworkCapabilities. */
+    public long transports;
+    /** Network id of the network associated with the event, or 0 if unspecified. */
+    public int netId;
+    /** Name of the network interface associated with the event, or null if unspecified. */
+    public String ifname;
+    /** Opaque event-specific data. */
+    public Parcelable data;
+
+    public ConnectivityMetricsEvent() {
+    }
+
+    private ConnectivityMetricsEvent(Parcel in) {
+        timestamp = in.readLong();
+        transports = in.readLong();
+        netId = in.readInt();
+        ifname = in.readString();
+        data = in.readParcelable(null);
+    }
+
+    /** Implement the Parcelable interface */
+    public static final @android.annotation.NonNull Parcelable.Creator<ConnectivityMetricsEvent> CREATOR
+            = new Parcelable.Creator<ConnectivityMetricsEvent> (){
+        public ConnectivityMetricsEvent createFromParcel(Parcel source) {
+            return new ConnectivityMetricsEvent(source);
+        }
+
+        public ConnectivityMetricsEvent[] newArray(int size) {
+            return new ConnectivityMetricsEvent[size];
+        }
+    };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeLong(timestamp);
+        dest.writeLong(transports);
+        dest.writeInt(netId);
+        dest.writeString(ifname);
+        dest.writeParcelable(data, 0);
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder buffer = new StringBuilder("ConnectivityMetricsEvent(");
+        buffer.append(String.format("%tT.%tL", timestamp, timestamp));
+        if (netId != 0) {
+            buffer.append(", ").append("netId=").append(netId);
+        }
+        if (ifname != null) {
+            buffer.append(", ").append(ifname);
+        }
+        for (int t : BitUtils.unpackBits(transports)) {
+            buffer.append(", ").append(NetworkCapabilities.transportNameOf(t));
+        }
+        buffer.append("): ").append(data.toString());
+        return buffer.toString();
+    }
+}
diff --git a/android/net/ConnectivityThread.java b/android/net/ConnectivityThread.java
new file mode 100644
index 0000000..0b218e7
--- /dev/null
+++ b/android/net/ConnectivityThread.java
@@ -0,0 +1,56 @@
+/*
+ * 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.os.HandlerThread;
+import android.os.Looper;
+
+/**
+ * Shared singleton connectivity thread for the system.  This is a thread for
+ * connectivity operations such as AsyncChannel connections to system services.
+ * Various connectivity manager objects can use this singleton as a common
+ * resource for their handlers instead of creating separate threads of their own.
+ * @hide
+ */
+public final class ConnectivityThread extends HandlerThread {
+
+    // A class implementing the lazy holder idiom: the unique static instance
+    // of ConnectivityThread is instantiated in a thread-safe way (guaranteed by
+    // the language specs) the first time that Singleton is referenced in get()
+    // or getInstanceLooper().
+    private static class Singleton {
+        private static final ConnectivityThread INSTANCE = createInstance();
+    }
+
+    private ConnectivityThread() {
+        super("ConnectivityThread");
+    }
+
+    private static ConnectivityThread createInstance() {
+        ConnectivityThread t = new ConnectivityThread();
+        t.start();
+        return t;
+    }
+
+    public static ConnectivityThread get() {
+        return Singleton.INSTANCE;
+    }
+
+    public static Looper getInstanceLooper() {
+        return Singleton.INSTANCE.getLooper();
+    }
+}
diff --git a/android/net/Credentials.java b/android/net/Credentials.java
new file mode 100644
index 0000000..7f6cf9d
--- /dev/null
+++ b/android/net/Credentials.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2007 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;
+
+/**
+ * A class for representing UNIX credentials passed via ancillary data
+ * on UNIX domain sockets. See "man 7 unix" on a desktop linux distro.
+ */
+public class Credentials {
+    /** pid of process. root peers may lie. */
+    private final int pid;
+    /** uid of process. root peers may lie. */
+    private final int uid;
+    /** gid of process. root peers may lie. */
+    private final int gid;
+
+    public Credentials (int pid, int uid, int gid) {
+        this.pid = pid;
+        this.uid = uid;
+        this.gid = gid;
+    }
+
+    public int getPid() {
+        return pid;
+    }
+
+    public int getUid() {
+        return uid;
+    }
+    
+    public int getGid() {
+        return gid;
+    }
+}
diff --git a/android/net/DataUsageRequest.java b/android/net/DataUsageRequest.java
new file mode 100644
index 0000000..0ac8f7e
--- /dev/null
+++ b/android/net/DataUsageRequest.java
@@ -0,0 +1,111 @@
+/**
+ * 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.NetworkTemplate;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * Defines a request to register a callbacks. Used to be notified on data usage via
+ * {@link android.app.usage.NetworkStatsManager#registerDataUsageCallback}.
+ * If no {@code uid}s are set, callbacks are restricted to device-owners,
+ * carrier-privileged apps, or system apps.
+ *
+ * @hide
+ */
+public final class DataUsageRequest implements Parcelable {
+
+    public static final String PARCELABLE_KEY = "DataUsageRequest";
+    public static final int REQUEST_ID_UNSET = 0;
+
+    /**
+     * Identifies the request.  {@link DataUsageRequest}s should only be constructed by
+     * the Framework and it is used internally to identify the request.
+     */
+    public final int requestId;
+
+    /**
+     * {@link NetworkTemplate} describing the network to monitor.
+     */
+    public final NetworkTemplate template;
+
+    /**
+     * Threshold in bytes to be notified on.
+     */
+    public final long thresholdInBytes;
+
+    public DataUsageRequest(int requestId, NetworkTemplate template, long thresholdInBytes) {
+        this.requestId = requestId;
+        this.template = template;
+        this.thresholdInBytes = thresholdInBytes;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(requestId);
+        dest.writeParcelable(template, flags);
+        dest.writeLong(thresholdInBytes);
+    }
+
+    public static final @android.annotation.NonNull Creator<DataUsageRequest> CREATOR =
+            new Creator<DataUsageRequest>() {
+                @Override
+                public DataUsageRequest createFromParcel(Parcel in) {
+                    int requestId = in.readInt();
+                    NetworkTemplate template = in.readParcelable(null);
+                    long thresholdInBytes = in.readLong();
+                    DataUsageRequest result = new DataUsageRequest(requestId, template,
+                            thresholdInBytes);
+                    return result;
+                }
+
+                @Override
+                public DataUsageRequest[] newArray(int size) {
+                    return new DataUsageRequest[size];
+                }
+            };
+
+    @Override
+    public String toString() {
+        return "DataUsageRequest [ requestId=" + requestId
+                + ", networkTemplate=" + template
+                + ", thresholdInBytes=" + thresholdInBytes + " ]";
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj instanceof DataUsageRequest == false) return false;
+        DataUsageRequest that = (DataUsageRequest) obj;
+        return that.requestId == this.requestId
+                && Objects.equals(that.template, this.template)
+                && that.thresholdInBytes == this.thresholdInBytes;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(requestId, template, thresholdInBytes);
+   }
+
+}
diff --git a/android/net/DhcpInfo.java b/android/net/DhcpInfo.java
new file mode 100644
index 0000000..98bab44
--- /dev/null
+++ b/android/net/DhcpInfo.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2008 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.os.Parcelable;
+import android.os.Parcel;
+
+/**
+ * A simple object for retrieving the results of a DHCP request.
+ */
+public class DhcpInfo implements Parcelable {
+    public int ipAddress;
+    public int gateway;
+    public int netmask;
+    public int dns1;
+    public int dns2;
+    public int serverAddress;
+
+    public int leaseDuration;
+
+    public DhcpInfo() {
+        super();
+    }
+
+    /** copy constructor {@hide} */
+    public DhcpInfo(DhcpInfo source) {
+        if (source != null) {
+            ipAddress = source.ipAddress;
+            gateway = source.gateway;
+            netmask = source.netmask;
+            dns1 = source.dns1;
+            dns2 = source.dns2;
+            serverAddress = source.serverAddress;
+            leaseDuration = source.leaseDuration;
+        }
+    }
+
+    public String toString() {
+        StringBuffer str = new StringBuffer();
+
+        str.append("ipaddr "); putAddress(str, ipAddress);
+        str.append(" gateway "); putAddress(str, gateway);
+        str.append(" netmask "); putAddress(str, netmask);
+        str.append(" dns1 "); putAddress(str, dns1);
+        str.append(" dns2 "); putAddress(str, dns2);
+        str.append(" DHCP server "); putAddress(str, serverAddress);
+        str.append(" lease ").append(leaseDuration).append(" seconds");
+
+        return str.toString();
+    }
+
+    private static void putAddress(StringBuffer buf, int addr) {
+        buf.append(NetworkUtils.intToInetAddress(addr).getHostAddress());
+    }
+
+    /** Implement the Parcelable interface {@hide} */
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Implement the Parcelable interface {@hide} */
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(ipAddress);
+        dest.writeInt(gateway);
+        dest.writeInt(netmask);
+        dest.writeInt(dns1);
+        dest.writeInt(dns2);
+        dest.writeInt(serverAddress);
+        dest.writeInt(leaseDuration);
+    }
+
+    /** Implement the Parcelable interface {@hide} */
+    public static final @android.annotation.NonNull Creator<DhcpInfo> CREATOR =
+        new Creator<DhcpInfo>() {
+            public DhcpInfo createFromParcel(Parcel in) {
+                DhcpInfo info = new DhcpInfo();
+                info.ipAddress = in.readInt();
+                info.gateway = in.readInt();
+                info.netmask = in.readInt();
+                info.dns1 = in.readInt();
+                info.dns2 = in.readInt();
+                info.serverAddress = in.readInt();
+                info.leaseDuration = in.readInt();
+                return info;
+            }
+
+            public DhcpInfo[] newArray(int size) {
+                return new DhcpInfo[size];
+            }
+        };
+}
diff --git a/android/net/DhcpResults.java b/android/net/DhcpResults.java
new file mode 100644
index 0000000..97be51a
--- /dev/null
+++ b/android/net/DhcpResults.java
@@ -0,0 +1,308 @@
+/*
+ * Copyright (C) 2012 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.UnsupportedAppUsage;
+import android.net.shared.InetAddressUtils;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A simple object for retrieving the results of a DHCP request.
+ * Optimized (attempted) for that jni interface
+ * TODO: remove this class and replace with other existing constructs
+ * @hide
+ */
+public final class DhcpResults implements Parcelable {
+    private static final String TAG = "DhcpResults";
+
+    @UnsupportedAppUsage
+    public LinkAddress ipAddress;
+
+    @UnsupportedAppUsage
+    public InetAddress gateway;
+
+    @UnsupportedAppUsage
+    public final ArrayList<InetAddress> dnsServers = new ArrayList<>();
+
+    @UnsupportedAppUsage
+    public String domains;
+
+    @UnsupportedAppUsage
+    public Inet4Address serverAddress;
+
+    /** Vendor specific information (from RFC 2132). */
+    @UnsupportedAppUsage
+    public String vendorInfo;
+
+    @UnsupportedAppUsage
+    public int leaseDuration;
+
+    /** Link MTU option. 0 means unset. */
+    @UnsupportedAppUsage
+    public int mtu;
+
+    public String serverHostName;
+
+    public DhcpResults() {
+        super();
+    }
+
+    /**
+     * Create a {@link StaticIpConfiguration} based on the DhcpResults.
+     */
+    public StaticIpConfiguration toStaticIpConfiguration() {
+        return new StaticIpConfiguration.Builder()
+                .setIpAddress(ipAddress)
+                .setGateway(gateway)
+                .setDnsServers(dnsServers)
+                .setDomains(domains)
+                .build();
+    }
+
+    public DhcpResults(StaticIpConfiguration source) {
+        if (source != null) {
+            ipAddress = source.getIpAddress();
+            gateway = source.getGateway();
+            dnsServers.addAll(source.getDnsServers());
+            domains = source.getDomains();
+        }
+    }
+
+    /** copy constructor */
+    public DhcpResults(DhcpResults source) {
+        this(source == null ? null : source.toStaticIpConfiguration());
+        if (source != null) {
+            serverAddress = source.serverAddress;
+            vendorInfo = source.vendorInfo;
+            leaseDuration = source.leaseDuration;
+            mtu = source.mtu;
+            serverHostName = source.serverHostName;
+        }
+    }
+
+    /**
+     * @see StaticIpConfiguration#getRoutes(String)
+     * @hide
+     */
+    public List<RouteInfo> getRoutes(String iface) {
+        return toStaticIpConfiguration().getRoutes(iface);
+    }
+
+    /**
+     * Test if this DHCP lease includes vendor hint that network link is
+     * metered, and sensitive to heavy data transfers.
+     */
+    public boolean hasMeteredHint() {
+        if (vendorInfo != null) {
+            return vendorInfo.contains("ANDROID_METERED");
+        } else {
+            return false;
+        }
+    }
+
+    public void clear() {
+        ipAddress = null;
+        gateway = null;
+        dnsServers.clear();
+        domains = null;
+        serverAddress = null;
+        vendorInfo = null;
+        leaseDuration = 0;
+        mtu = 0;
+        serverHostName = null;
+    }
+
+    @Override
+    public String toString() {
+        StringBuffer str = new StringBuffer(super.toString());
+
+        str.append(" DHCP server ").append(serverAddress);
+        str.append(" Vendor info ").append(vendorInfo);
+        str.append(" lease ").append(leaseDuration).append(" seconds");
+        if (mtu != 0) str.append(" MTU ").append(mtu);
+        str.append(" Servername ").append(serverHostName);
+
+        return str.toString();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) return true;
+
+        if (!(obj instanceof DhcpResults)) return false;
+
+        DhcpResults target = (DhcpResults)obj;
+
+        return toStaticIpConfiguration().equals(target.toStaticIpConfiguration())
+                && Objects.equals(serverAddress, target.serverAddress)
+                && Objects.equals(vendorInfo, target.vendorInfo)
+                && Objects.equals(serverHostName, target.serverHostName)
+                && leaseDuration == target.leaseDuration
+                && mtu == target.mtu;
+    }
+
+    /**
+     * Implement the Parcelable interface
+     */
+    public static final @android.annotation.NonNull Creator<DhcpResults> CREATOR =
+        new Creator<DhcpResults>() {
+            public DhcpResults createFromParcel(Parcel in) {
+                return readFromParcel(in);
+            }
+
+            public DhcpResults[] newArray(int size) {
+                return new DhcpResults[size];
+            }
+        };
+
+    /** Implement the Parcelable interface */
+    public void writeToParcel(Parcel dest, int flags) {
+        toStaticIpConfiguration().writeToParcel(dest, flags);
+        dest.writeInt(leaseDuration);
+        dest.writeInt(mtu);
+        InetAddressUtils.parcelInetAddress(dest, serverAddress, flags);
+        dest.writeString(vendorInfo);
+        dest.writeString(serverHostName);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    private static DhcpResults readFromParcel(Parcel in) {
+        final StaticIpConfiguration s = StaticIpConfiguration.CREATOR.createFromParcel(in);
+        final DhcpResults dhcpResults = new DhcpResults(s);
+        dhcpResults.leaseDuration = in.readInt();
+        dhcpResults.mtu = in.readInt();
+        dhcpResults.serverAddress = (Inet4Address) InetAddressUtils.unparcelInetAddress(in);
+        dhcpResults.vendorInfo = in.readString();
+        dhcpResults.serverHostName = in.readString();
+        return dhcpResults;
+    }
+
+    // Utils for jni population - false on success
+    // Not part of the superclass because they're only used by the JNI iterface to the DHCP daemon.
+    public boolean setIpAddress(String addrString, int prefixLength) {
+        try {
+            Inet4Address addr = (Inet4Address) InetAddresses.parseNumericAddress(addrString);
+            ipAddress = new LinkAddress(addr, prefixLength);
+        } catch (IllegalArgumentException|ClassCastException e) {
+            Log.e(TAG, "setIpAddress failed with addrString " + addrString + "/" + prefixLength);
+            return true;
+        }
+        return false;
+    }
+
+    public boolean setGateway(String addrString) {
+        try {
+            gateway = InetAddresses.parseNumericAddress(addrString);
+        } catch (IllegalArgumentException e) {
+            Log.e(TAG, "setGateway failed with addrString " + addrString);
+            return true;
+        }
+        return false;
+    }
+
+    public boolean addDns(String addrString) {
+        if (TextUtils.isEmpty(addrString) == false) {
+            try {
+                dnsServers.add(InetAddresses.parseNumericAddress(addrString));
+            } catch (IllegalArgumentException e) {
+                Log.e(TAG, "addDns failed with addrString " + addrString);
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public LinkAddress getIpAddress() {
+        return ipAddress;
+    }
+
+    public void setIpAddress(LinkAddress ipAddress) {
+        this.ipAddress = ipAddress;
+    }
+
+    public InetAddress getGateway() {
+        return gateway;
+    }
+
+    public void setGateway(InetAddress gateway) {
+        this.gateway = gateway;
+    }
+
+    public List<InetAddress> getDnsServers() {
+        return dnsServers;
+    }
+
+    /**
+     * Add a DNS server to this configuration.
+     */
+    public void addDnsServer(InetAddress server) {
+        dnsServers.add(server);
+    }
+
+    public String getDomains() {
+        return domains;
+    }
+
+    public void setDomains(String domains) {
+        this.domains = domains;
+    }
+
+    public Inet4Address getServerAddress() {
+        return serverAddress;
+    }
+
+    public void setServerAddress(Inet4Address addr) {
+        serverAddress = addr;
+    }
+
+    public int getLeaseDuration() {
+        return leaseDuration;
+    }
+
+    public void setLeaseDuration(int duration) {
+        leaseDuration = duration;
+    }
+
+    public String getVendorInfo() {
+        return vendorInfo;
+    }
+
+    public void setVendorInfo(String info) {
+        vendorInfo = info;
+    }
+
+    public int getMtu() {
+        return mtu;
+    }
+
+    public void setMtu(int mtu) {
+        this.mtu = mtu;
+    }
+}
diff --git a/android/net/DnsPacket.java b/android/net/DnsPacket.java
new file mode 100644
index 0000000..83e57e0
--- /dev/null
+++ b/android/net/DnsPacket.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2019 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.NonNull;
+import android.annotation.Nullable;
+import android.text.TextUtils;
+
+import com.android.internal.util.BitUtils;
+
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.text.DecimalFormat;
+import java.text.FieldPosition;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Defines basic data for DNS protocol based on RFC 1035.
+ * Subclasses create the specific format used in DNS packet.
+ *
+ * @hide
+ */
+public abstract class DnsPacket {
+    public class DnsHeader {
+        private static final String TAG = "DnsHeader";
+        public final int id;
+        public final int flags;
+        public final int rcode;
+        private final int[] mRecordCount;
+
+        /**
+         * Create a new DnsHeader from a positioned ByteBuffer.
+         *
+         * The ByteBuffer must be in network byte order (which is the default).
+         * Reads the passed ByteBuffer from its current position and decodes a DNS header.
+         * When this constructor returns, the reading position of the ByteBuffer has been
+         * advanced to the end of the DNS header record.
+         * This is meant to chain with other methods reading a DNS response in sequence.
+         */
+        DnsHeader(@NonNull ByteBuffer buf) throws BufferUnderflowException {
+            id = BitUtils.uint16(buf.getShort());
+            flags = BitUtils.uint16(buf.getShort());
+            rcode = flags & 0xF;
+            mRecordCount = new int[NUM_SECTIONS];
+            for (int i = 0; i < NUM_SECTIONS; ++i) {
+                mRecordCount[i] = BitUtils.uint16(buf.getShort());
+            }
+        }
+
+        /**
+         * Get record count by type.
+         */
+        public int getRecordCount(int type) {
+            return mRecordCount[type];
+        }
+    }
+
+    /**
+     * Superclass for DNS questions and DNS resource records.
+     *
+     * DNS questions (No TTL/RDATA)
+     * DNS resource records (With TTL/RDATA)
+     */
+    public class DnsRecord {
+        private static final int MAXNAMESIZE = 255;
+        private static final int MAXLABELSIZE = 63;
+        private static final int MAXLABELCOUNT = 128;
+        private static final int NAME_NORMAL = 0;
+        private static final int NAME_COMPRESSION = 0xC0;
+        private final DecimalFormat byteFormat = new DecimalFormat();
+        private final FieldPosition pos = new FieldPosition(0);
+
+        private static final String TAG = "DnsRecord";
+
+        public final String dName;
+        public final int nsType;
+        public final int nsClass;
+        public final long ttl;
+        private final byte[] mRdata;
+
+        /**
+         * Create a new DnsRecord from a positioned ByteBuffer.
+         *
+         * Reads the passed ByteBuffer from its current position and decodes a DNS record.
+         * When this constructor returns, the reading position of the ByteBuffer has been
+         * advanced to the end of the DNS header record.
+         * This is meant to chain with other methods reading a DNS response in sequence.
+         *
+         * @param ByteBuffer input of record, must be in network byte order
+         *         (which is the default).
+         */
+        DnsRecord(int recordType, @NonNull ByteBuffer buf)
+                throws BufferUnderflowException, ParseException {
+            dName = parseName(buf, 0 /* Parse depth */);
+            if (dName.length() > MAXNAMESIZE) {
+                throw new ParseException(
+                        "Parse name fail, name size is too long: " + dName.length());
+            }
+            nsType = BitUtils.uint16(buf.getShort());
+            nsClass = BitUtils.uint16(buf.getShort());
+
+            if (recordType != QDSECTION) {
+                ttl = BitUtils.uint32(buf.getInt());
+                final int length = BitUtils.uint16(buf.getShort());
+                mRdata = new byte[length];
+                buf.get(mRdata);
+            } else {
+                ttl = 0;
+                mRdata = null;
+            }
+        }
+
+        /**
+         * Get a copy of rdata.
+         */
+        @Nullable
+        public byte[] getRR() {
+            return (mRdata == null) ? null : mRdata.clone();
+        }
+
+        /**
+         * Convert label from {@code byte[]} to {@code String}
+         *
+         * Follows the same conversion rules of the native code (ns_name.c in libc)
+         */
+        private String labelToString(@NonNull byte[] label) {
+            final StringBuffer sb = new StringBuffer();
+            for (int i = 0; i < label.length; ++i) {
+                int b = BitUtils.uint8(label[i]);
+                // Control characters and non-ASCII characters.
+                if (b <= 0x20 || b >= 0x7f) {
+                    // Append the byte as an escaped decimal number, e.g., "\19" for 0x13.
+                    sb.append('\\');
+                    byteFormat.format(b, sb, pos);
+                } else if (b == '"' || b == '.' || b == ';' || b == '\\'
+                        || b == '(' || b == ')' || b == '@' || b == '$') {
+                    // Append the byte as an escaped character, e.g., "\:" for 0x3a.
+                    sb.append('\\');
+                    sb.append((char) b);
+                } else {
+                    // Append the byte as a character, e.g., "a" for 0x61.
+                    sb.append((char) b);
+                }
+            }
+            return sb.toString();
+        }
+
+        private String parseName(@NonNull ByteBuffer buf, int depth) throws
+                BufferUnderflowException, ParseException {
+            if (depth > MAXLABELCOUNT) {
+                throw new ParseException("Failed to parse name, too many labels");
+            }
+            final int len = BitUtils.uint8(buf.get());
+            final int mask = len & NAME_COMPRESSION;
+            if (0 == len) {
+                return "";
+            } else if (mask != NAME_NORMAL && mask != NAME_COMPRESSION) {
+                throw new ParseException("Parse name fail, bad label type");
+            } else if (mask == NAME_COMPRESSION) {
+                // Name compression based on RFC 1035 - 4.1.4 Message compression
+                final int offset = ((len & ~NAME_COMPRESSION) << 8) + BitUtils.uint8(buf.get());
+                final int oldPos = buf.position();
+                if (offset >= oldPos - 2) {
+                    throw new ParseException("Parse compression name fail, invalid compression");
+                }
+                buf.position(offset);
+                final String pointed = parseName(buf, depth + 1);
+                buf.position(oldPos);
+                return pointed;
+            } else {
+                final byte[] label = new byte[len];
+                buf.get(label);
+                final String head = labelToString(label);
+                if (head.length() > MAXLABELSIZE) {
+                    throw new ParseException("Parse name fail, invalid label length");
+                }
+                final String tail = parseName(buf, depth + 1);
+                return TextUtils.isEmpty(tail) ? head : head + "." + tail;
+            }
+        }
+    }
+
+    public static final int QDSECTION = 0;
+    public static final int ANSECTION = 1;
+    public static final int NSSECTION = 2;
+    public static final int ARSECTION = 3;
+    private static final int NUM_SECTIONS = ARSECTION + 1;
+
+    private static final String TAG = DnsPacket.class.getSimpleName();
+
+    protected final DnsHeader mHeader;
+    protected final List<DnsRecord>[] mRecords;
+
+    protected DnsPacket(@NonNull byte[] data) throws ParseException {
+        if (null == data) throw new ParseException("Parse header failed, null input data");
+        final ByteBuffer buffer;
+        try {
+            buffer = ByteBuffer.wrap(data);
+            mHeader = new DnsHeader(buffer);
+        } catch (BufferUnderflowException e) {
+            throw new ParseException("Parse Header fail, bad input data", e);
+        }
+
+        mRecords = new ArrayList[NUM_SECTIONS];
+
+        for (int i = 0; i < NUM_SECTIONS; ++i) {
+            final int count = mHeader.getRecordCount(i);
+            if (count > 0) {
+                mRecords[i] = new ArrayList(count);
+            }
+            for (int j = 0; j < count; ++j) {
+                try {
+                    mRecords[i].add(new DnsRecord(i, buffer));
+                } catch (BufferUnderflowException e) {
+                    throw new ParseException("Parse record fail", e);
+                }
+            }
+        }
+    }
+}
diff --git a/android/net/DnsResolver.java b/android/net/DnsResolver.java
new file mode 100644
index 0000000..0b1a845
--- /dev/null
+++ b/android/net/DnsResolver.java
@@ -0,0 +1,568 @@
+/*
+ * Copyright (C) 2019 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 static android.net.NetworkUtils.getDnsNetwork;
+import static android.net.NetworkUtils.resNetworkCancel;
+import static android.net.NetworkUtils.resNetworkQuery;
+import static android.net.NetworkUtils.resNetworkResult;
+import static android.net.NetworkUtils.resNetworkSend;
+import static android.net.util.DnsUtils.haveIpv4;
+import static android.net.util.DnsUtils.haveIpv6;
+import static android.net.util.DnsUtils.rfc6724Sort;
+import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_ERROR;
+import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT;
+import static android.system.OsConstants.ENONET;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.CancellationSignal;
+import android.os.Looper;
+import android.os.MessageQueue;
+import android.system.ErrnoException;
+import android.util.Log;
+
+import java.io.FileDescriptor;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * Dns resolver class for asynchronous dns querying
+ *
+ * Note that if a client sends a query with more than 1 record in the question section but
+ * the remote dns server does not support this, it may not respond at all, leading to a timeout.
+ *
+ */
+public final class DnsResolver {
+    private static final String TAG = "DnsResolver";
+    private static final int FD_EVENTS = EVENT_INPUT | EVENT_ERROR;
+    private static final int MAXPACKET = 8 * 1024;
+    private static final int SLEEP_TIME_MS = 2;
+
+    @IntDef(prefix = { "CLASS_" }, value = {
+            CLASS_IN
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @interface QueryClass {}
+    public static final int CLASS_IN = 1;
+
+    @IntDef(prefix = { "TYPE_" },  value = {
+            TYPE_A,
+            TYPE_AAAA
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @interface QueryType {}
+    public static final int TYPE_A = 1;
+    public static final int TYPE_AAAA = 28;
+
+    @IntDef(prefix = { "FLAG_" }, value = {
+            FLAG_EMPTY,
+            FLAG_NO_RETRY,
+            FLAG_NO_CACHE_STORE,
+            FLAG_NO_CACHE_LOOKUP
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @interface QueryFlag {}
+    public static final int FLAG_EMPTY = 0;
+    public static final int FLAG_NO_RETRY = 1 << 0;
+    public static final int FLAG_NO_CACHE_STORE = 1 << 1;
+    public static final int FLAG_NO_CACHE_LOOKUP = 1 << 2;
+
+    @IntDef(prefix = { "ERROR_" }, value = {
+            ERROR_PARSE,
+            ERROR_SYSTEM
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    @interface DnsError {}
+    /**
+     * Indicates that there was an error parsing the response the query.
+     * The cause of this error is available via getCause() and is a ParseException.
+     */
+    public static final int ERROR_PARSE = 0;
+    /**
+     * Indicates that there was an error sending the query.
+     * The cause of this error is available via getCause() and is an ErrnoException.
+     */
+    public static final int ERROR_SYSTEM = 1;
+
+    private static final int NETID_UNSET = 0;
+
+    private static final DnsResolver sInstance = new DnsResolver();
+
+    /**
+     * Get instance for DnsResolver
+     */
+    public static @NonNull DnsResolver getInstance() {
+        return sInstance;
+    }
+
+    private DnsResolver() {}
+
+    /**
+     * Base interface for answer callbacks
+     *
+     * @param <T> The type of the answer
+     */
+    public interface Callback<T> {
+        /**
+         * Success response to
+         * {@link android.net.DnsResolver#query query()} or
+         * {@link android.net.DnsResolver#rawQuery rawQuery()}.
+         *
+         * Invoked when the answer to a query was successfully parsed.
+         *
+         * @param answer <T> answer to the query.
+         * @param rcode The response code in the DNS response.
+         *
+         * {@see android.net.DnsResolver#query query()}
+         */
+        void onAnswer(@NonNull T answer, int rcode);
+        /**
+         * Error response to
+         * {@link android.net.DnsResolver#query query()} or
+         * {@link android.net.DnsResolver#rawQuery rawQuery()}.
+         *
+         * Invoked when there is no valid answer to
+         * {@link android.net.DnsResolver#query query()}
+         * {@link android.net.DnsResolver#rawQuery rawQuery()}.
+         *
+         * @param error a {@link DnsException} object with additional
+         *    detail regarding the failure
+         */
+        void onError(@NonNull DnsException error);
+    }
+
+    /**
+     * Class to represent DNS error
+     */
+    public static class DnsException extends Exception {
+       /**
+        * DNS error code as one of the ERROR_* constants
+        */
+        @DnsError public final int code;
+
+        DnsException(@DnsError int code, @Nullable Throwable cause) {
+            super(cause);
+            this.code = code;
+        }
+    }
+
+    /**
+     * Send a raw DNS query.
+     * The answer will be provided asynchronously through the provided {@link Callback}.
+     *
+     * @param network {@link Network} specifying which network to query on.
+     *         {@code null} for query on default network.
+     * @param query blob message to query
+     * @param flags flags as a combination of the FLAGS_* constants
+     * @param executor The {@link Executor} that the callback should be executed on.
+     * @param cancellationSignal used by the caller to signal if the query should be
+     *    cancelled. May be {@code null}.
+     * @param callback a {@link Callback} which will be called to notify the caller
+     *    of the result of dns query.
+     */
+    public void rawQuery(@Nullable Network network, @NonNull byte[] query, @QueryFlag int flags,
+            @NonNull @CallbackExecutor Executor executor,
+            @Nullable CancellationSignal cancellationSignal,
+            @NonNull Callback<? super byte[]> callback) {
+        if (cancellationSignal != null && cancellationSignal.isCanceled()) {
+            return;
+        }
+        final Object lock = new Object();
+        final FileDescriptor queryfd;
+        try {
+            queryfd = resNetworkSend((network != null)
+                    ? network.getNetIdForResolv() : NETID_UNSET, query, query.length, flags);
+        } catch (ErrnoException e) {
+            executor.execute(() -> callback.onError(new DnsException(ERROR_SYSTEM, e)));
+            return;
+        }
+
+        synchronized (lock)  {
+            registerFDListener(executor, queryfd, callback, cancellationSignal, lock);
+            if (cancellationSignal == null) return;
+            addCancellationSignal(cancellationSignal, queryfd, lock);
+        }
+    }
+
+    /**
+     * Send a DNS query with the specified name, class and query type.
+     * The answer will be provided asynchronously through the provided {@link Callback}.
+     *
+     * @param network {@link Network} specifying which network to query on.
+     *         {@code null} for query on default network.
+     * @param domain domain name to query
+     * @param nsClass dns class as one of the CLASS_* constants
+     * @param nsType dns resource record (RR) type as one of the TYPE_* constants
+     * @param flags flags as a combination of the FLAGS_* constants
+     * @param executor The {@link Executor} that the callback should be executed on.
+     * @param cancellationSignal used by the caller to signal if the query should be
+     *    cancelled. May be {@code null}.
+     * @param callback a {@link Callback} which will be called to notify the caller
+     *    of the result of dns query.
+     */
+    public void rawQuery(@Nullable Network network, @NonNull String domain,
+            @QueryClass int nsClass, @QueryType int nsType, @QueryFlag int flags,
+            @NonNull @CallbackExecutor Executor executor,
+            @Nullable CancellationSignal cancellationSignal,
+            @NonNull Callback<? super byte[]> callback) {
+        if (cancellationSignal != null && cancellationSignal.isCanceled()) {
+            return;
+        }
+        final Object lock = new Object();
+        final FileDescriptor queryfd;
+        try {
+            queryfd = resNetworkQuery((network != null)
+                    ? network.getNetIdForResolv() : NETID_UNSET, domain, nsClass, nsType, flags);
+        } catch (ErrnoException e) {
+            executor.execute(() -> callback.onError(new DnsException(ERROR_SYSTEM, e)));
+            return;
+        }
+        synchronized (lock)  {
+            registerFDListener(executor, queryfd, callback, cancellationSignal, lock);
+            if (cancellationSignal == null) return;
+            addCancellationSignal(cancellationSignal, queryfd, lock);
+        }
+    }
+
+    private class InetAddressAnswerAccumulator implements Callback<byte[]> {
+        private final List<InetAddress> mAllAnswers;
+        private final Network mNetwork;
+        private int mRcode;
+        private DnsException mDnsException;
+        private final Callback<? super List<InetAddress>> mUserCallback;
+        private final int mTargetAnswerCount;
+        private int mReceivedAnswerCount = 0;
+
+        InetAddressAnswerAccumulator(@NonNull Network network, int size,
+                @NonNull Callback<? super List<InetAddress>> callback) {
+            mNetwork = network;
+            mTargetAnswerCount = size;
+            mAllAnswers = new ArrayList<>();
+            mUserCallback = callback;
+        }
+
+        private boolean maybeReportError() {
+            if (mRcode != 0) {
+                mUserCallback.onAnswer(mAllAnswers, mRcode);
+                return true;
+            }
+            if (mDnsException != null) {
+                mUserCallback.onError(mDnsException);
+                return true;
+            }
+            return false;
+        }
+
+        private void maybeReportAnswer() {
+            if (++mReceivedAnswerCount != mTargetAnswerCount) return;
+            if (mAllAnswers.isEmpty() && maybeReportError()) return;
+            mUserCallback.onAnswer(rfc6724Sort(mNetwork, mAllAnswers), mRcode);
+        }
+
+        @Override
+        public void onAnswer(@NonNull byte[] answer, int rcode) {
+            // If at least one query succeeded, return an rcode of 0.
+            // Otherwise, arbitrarily return the first rcode received.
+            if (mReceivedAnswerCount == 0 || rcode == 0) {
+                mRcode = rcode;
+            }
+            try {
+                mAllAnswers.addAll(new DnsAddressAnswer(answer).getAddresses());
+            } catch (ParseException e) {
+                mDnsException = new DnsException(ERROR_PARSE, e);
+            }
+            maybeReportAnswer();
+        }
+
+        @Override
+        public void onError(@NonNull DnsException error) {
+            mDnsException = error;
+            maybeReportAnswer();
+        }
+    }
+
+    /**
+     * Send a DNS query with the specified name on a network with both IPv4 and IPv6,
+     * get back a set of InetAddresses with rfc6724 sorting style asynchronously.
+     *
+     * This method will examine the connection ability on given network, and query IPv4
+     * and IPv6 if connection is available.
+     *
+     * If at least one query succeeded with valid answer, rcode will be 0
+     *
+     * The answer will be provided asynchronously through the provided {@link Callback}.
+     *
+     * @param network {@link Network} specifying which network to query on.
+     *         {@code null} for query on default network.
+     * @param domain domain name to query
+     * @param flags flags as a combination of the FLAGS_* constants
+     * @param executor The {@link Executor} that the callback should be executed on.
+     * @param cancellationSignal used by the caller to signal if the query should be
+     *    cancelled. May be {@code null}.
+     * @param callback a {@link Callback} which will be called to notify the
+     *    caller of the result of dns query.
+     */
+    public void query(@Nullable Network network, @NonNull String domain, @QueryFlag int flags,
+            @NonNull @CallbackExecutor Executor executor,
+            @Nullable CancellationSignal cancellationSignal,
+            @NonNull Callback<? super List<InetAddress>> callback) {
+        if (cancellationSignal != null && cancellationSignal.isCanceled()) {
+            return;
+        }
+        final Object lock = new Object();
+        final Network queryNetwork;
+        try {
+            queryNetwork = (network != null) ? network : getDnsNetwork();
+        } catch (ErrnoException e) {
+            executor.execute(() -> callback.onError(new DnsException(ERROR_SYSTEM, e)));
+            return;
+        }
+        final boolean queryIpv6 = haveIpv6(queryNetwork);
+        final boolean queryIpv4 = haveIpv4(queryNetwork);
+
+        // This can only happen if queryIpv4 and queryIpv6 are both false.
+        // This almost certainly means that queryNetwork does not exist or no longer exists.
+        if (!queryIpv6 && !queryIpv4) {
+            executor.execute(() -> callback.onError(
+                    new DnsException(ERROR_SYSTEM, new ErrnoException("resNetworkQuery", ENONET))));
+            return;
+        }
+
+        final FileDescriptor v4fd;
+        final FileDescriptor v6fd;
+
+        int queryCount = 0;
+
+        if (queryIpv6) {
+            try {
+                v6fd = resNetworkQuery(queryNetwork.getNetIdForResolv(), domain, CLASS_IN,
+                        TYPE_AAAA, flags);
+            } catch (ErrnoException e) {
+                executor.execute(() -> callback.onError(new DnsException(ERROR_SYSTEM, e)));
+                return;
+            }
+            queryCount++;
+        } else v6fd = null;
+
+        // Avoiding gateways drop packets if queries are sent too close together
+        try {
+            Thread.sleep(SLEEP_TIME_MS);
+        } catch (InterruptedException ex) {
+            Thread.currentThread().interrupt();
+        }
+
+        if (queryIpv4) {
+            try {
+                v4fd = resNetworkQuery(queryNetwork.getNetIdForResolv(), domain, CLASS_IN, TYPE_A,
+                        flags);
+            } catch (ErrnoException e) {
+                if (queryIpv6) resNetworkCancel(v6fd);  // Closes fd, marks it invalid.
+                executor.execute(() -> callback.onError(new DnsException(ERROR_SYSTEM, e)));
+                return;
+            }
+            queryCount++;
+        } else v4fd = null;
+
+        final InetAddressAnswerAccumulator accumulator =
+                new InetAddressAnswerAccumulator(queryNetwork, queryCount, callback);
+
+        synchronized (lock)  {
+            if (queryIpv6) {
+                registerFDListener(executor, v6fd, accumulator, cancellationSignal, lock);
+            }
+            if (queryIpv4) {
+                registerFDListener(executor, v4fd, accumulator, cancellationSignal, lock);
+            }
+            if (cancellationSignal == null) return;
+            cancellationSignal.setOnCancelListener(() -> {
+                synchronized (lock)  {
+                    if (queryIpv4) cancelQuery(v4fd);
+                    if (queryIpv6) cancelQuery(v6fd);
+                }
+            });
+        }
+    }
+
+    /**
+     * Send a DNS query with the specified name and query type, get back a set of
+     * InetAddresses with rfc6724 sorting style asynchronously.
+     *
+     * The answer will be provided asynchronously through the provided {@link Callback}.
+     *
+     * @param network {@link Network} specifying which network to query on.
+     *         {@code null} for query on default network.
+     * @param domain domain name to query
+     * @param nsType dns resource record (RR) type as one of the TYPE_* constants
+     * @param flags flags as a combination of the FLAGS_* constants
+     * @param executor The {@link Executor} that the callback should be executed on.
+     * @param cancellationSignal used by the caller to signal if the query should be
+     *    cancelled. May be {@code null}.
+     * @param callback a {@link Callback} which will be called to notify the caller
+     *    of the result of dns query.
+     */
+    public void query(@Nullable Network network, @NonNull String domain,
+            @QueryType int nsType, @QueryFlag int flags,
+            @NonNull @CallbackExecutor Executor executor,
+            @Nullable CancellationSignal cancellationSignal,
+            @NonNull Callback<? super List<InetAddress>> callback) {
+        if (cancellationSignal != null && cancellationSignal.isCanceled()) {
+            return;
+        }
+        final Object lock = new Object();
+        final FileDescriptor queryfd;
+        final Network queryNetwork;
+        try {
+            queryNetwork = (network != null) ? network : getDnsNetwork();
+            queryfd = resNetworkQuery(queryNetwork.getNetIdForResolv(), domain, CLASS_IN, nsType,
+                    flags);
+        } catch (ErrnoException e) {
+            executor.execute(() -> callback.onError(new DnsException(ERROR_SYSTEM, e)));
+            return;
+        }
+        final InetAddressAnswerAccumulator accumulator =
+                new InetAddressAnswerAccumulator(queryNetwork, 1, callback);
+        synchronized (lock)  {
+            registerFDListener(executor, queryfd, accumulator, cancellationSignal, lock);
+            if (cancellationSignal == null) return;
+            addCancellationSignal(cancellationSignal, queryfd, lock);
+        }
+    }
+
+    /**
+     * Class to retrieve DNS response
+     *
+     * @hide
+     */
+    public static final class DnsResponse {
+        public final @NonNull byte[] answerbuf;
+        public final int rcode;
+        public DnsResponse(@NonNull byte[] answerbuf, int rcode) {
+            this.answerbuf = answerbuf;
+            this.rcode = rcode;
+        }
+    }
+
+    private void registerFDListener(@NonNull Executor executor,
+            @NonNull FileDescriptor queryfd, @NonNull Callback<? super byte[]> answerCallback,
+            @Nullable CancellationSignal cancellationSignal, @NonNull Object lock) {
+        final MessageQueue mainThreadMessageQueue = Looper.getMainLooper().getQueue();
+        mainThreadMessageQueue.addOnFileDescriptorEventListener(
+                queryfd,
+                FD_EVENTS,
+                (fd, events) -> {
+                    // b/134310704
+                    // Unregister fd event listener before resNetworkResult is called to prevent
+                    // race condition caused by fd reused.
+                    // For example when querying v4 and v6, it's possible that the first query ends
+                    // and the fd is closed before the second request starts, which might return
+                    // the same fd for the second request. By that time, the looper must have
+                    // unregistered the fd, otherwise another event listener can't be registered.
+                    mainThreadMessageQueue.removeOnFileDescriptorEventListener(fd);
+
+                    executor.execute(() -> {
+                        DnsResponse resp = null;
+                        ErrnoException exception = null;
+                        synchronized (lock) {
+                            if (cancellationSignal != null && cancellationSignal.isCanceled()) {
+                                return;
+                            }
+                            try {
+                                resp = resNetworkResult(fd);  // Closes fd, marks it invalid.
+                            } catch (ErrnoException e) {
+                                Log.e(TAG, "resNetworkResult:" + e.toString());
+                                exception = e;
+                            }
+                        }
+                        if (exception != null) {
+                            answerCallback.onError(new DnsException(ERROR_SYSTEM, exception));
+                            return;
+                        }
+                        answerCallback.onAnswer(resp.answerbuf, resp.rcode);
+                    });
+
+                    // The file descriptor has already been unregistered, so it does not really
+                    // matter what is returned here. In spirit 0 (meaning "unregister this FD")
+                    // is still the closest to what the looper needs to do. When returning 0,
+                    // Looper knows to ignore the fd if it has already been unregistered.
+                    return 0;
+                });
+    }
+
+    private void cancelQuery(@NonNull FileDescriptor queryfd) {
+        if (!queryfd.valid()) return;
+        Looper.getMainLooper().getQueue().removeOnFileDescriptorEventListener(queryfd);
+        resNetworkCancel(queryfd);  // Closes fd, marks it invalid.
+    }
+
+    private void addCancellationSignal(@NonNull CancellationSignal cancellationSignal,
+            @NonNull FileDescriptor queryfd, @NonNull Object lock) {
+        cancellationSignal.setOnCancelListener(() -> {
+            synchronized (lock)  {
+                cancelQuery(queryfd);
+            }
+        });
+    }
+
+    private static class DnsAddressAnswer extends DnsPacket {
+        private static final String TAG = "DnsResolver.DnsAddressAnswer";
+        private static final boolean DBG = false;
+
+        private final int mQueryType;
+
+        DnsAddressAnswer(@NonNull byte[] data) throws ParseException {
+            super(data);
+            if ((mHeader.flags & (1 << 15)) == 0) {
+                throw new ParseException("Not an answer packet");
+            }
+            if (mHeader.getRecordCount(QDSECTION) == 0) {
+                throw new ParseException("No question found");
+            }
+            // Expect only one question in question section.
+            mQueryType = mRecords[QDSECTION].get(0).nsType;
+        }
+
+        public @NonNull List<InetAddress> getAddresses() {
+            final List<InetAddress> results = new ArrayList<InetAddress>();
+            if (mHeader.getRecordCount(ANSECTION) == 0) return results;
+
+            for (final DnsRecord ansSec : mRecords[ANSECTION]) {
+                // Only support A and AAAA, also ignore answers if query type != answer type.
+                int nsType = ansSec.nsType;
+                if (nsType != mQueryType || (nsType != TYPE_A && nsType != TYPE_AAAA)) {
+                    continue;
+                }
+                try {
+                    results.add(InetAddress.getByAddress(ansSec.getRR()));
+                } catch (UnknownHostException e) {
+                    if (DBG) {
+                        Log.w(TAG, "rr to address fail");
+                    }
+                }
+            }
+            return results;
+        }
+    }
+
+}
diff --git a/android/net/EthernetManager.java b/android/net/EthernetManager.java
new file mode 100644
index 0000000..7256502
--- /dev/null
+++ b/android/net/EthernetManager.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.annotation.SystemService;
+import android.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.os.Handler;
+import android.os.Message;
+import android.os.RemoteException;
+
+import java.util.ArrayList;
+
+/**
+ * A class representing the IP configuration of the Ethernet network.
+ *
+ * @hide
+ */
+@SystemService(Context.ETHERNET_SERVICE)
+public class EthernetManager {
+    private static final String TAG = "EthernetManager";
+    private static final int MSG_AVAILABILITY_CHANGED = 1000;
+
+    private final Context mContext;
+    private final IEthernetManager mService;
+    private final Handler mHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            if (msg.what == MSG_AVAILABILITY_CHANGED) {
+                boolean isAvailable = (msg.arg1 == 1);
+                for (Listener listener : mListeners) {
+                    listener.onAvailabilityChanged((String) msg.obj, isAvailable);
+                }
+            }
+        }
+    };
+    private final ArrayList<Listener> mListeners = new ArrayList<>();
+    private final IEthernetServiceListener.Stub mServiceListener =
+            new IEthernetServiceListener.Stub() {
+                @Override
+                public void onAvailabilityChanged(String iface, boolean isAvailable) {
+                    mHandler.obtainMessage(
+                            MSG_AVAILABILITY_CHANGED, isAvailable ? 1 : 0, 0, iface).sendToTarget();
+                }
+            };
+
+    /**
+     * A listener interface to receive notification on changes in Ethernet.
+     */
+    public interface Listener {
+        /**
+         * Called when Ethernet port's availability is changed.
+         * @param iface Ethernet interface name
+         * @param isAvailable {@code true} if Ethernet port exists.
+         */
+        @UnsupportedAppUsage
+        void onAvailabilityChanged(String iface, boolean isAvailable);
+    }
+
+    /**
+     * Create a new EthernetManager instance.
+     * Applications will almost always want to use
+     * {@link android.content.Context#getSystemService Context.getSystemService()} to retrieve
+     * the standard {@link android.content.Context#ETHERNET_SERVICE Context.ETHERNET_SERVICE}.
+     */
+    public EthernetManager(Context context, IEthernetManager service) {
+        mContext = context;
+        mService = service;
+    }
+
+    /**
+     * Get Ethernet configuration.
+     * @return the Ethernet Configuration, contained in {@link IpConfiguration}.
+     */
+    @UnsupportedAppUsage
+    public IpConfiguration getConfiguration(String iface) {
+        try {
+            return mService.getConfiguration(iface);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Set Ethernet configuration.
+     */
+    @UnsupportedAppUsage
+    public void setConfiguration(String iface, IpConfiguration config) {
+        try {
+            mService.setConfiguration(iface, config);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Indicates whether the system currently has one or more Ethernet interfaces.
+     */
+    @UnsupportedAppUsage
+    public boolean isAvailable() {
+        return getAvailableInterfaces().length > 0;
+    }
+
+    /**
+     * Indicates whether the system has given interface.
+     *
+     * @param iface Ethernet interface name
+     */
+    @UnsupportedAppUsage
+    public boolean isAvailable(String iface) {
+        try {
+            return mService.isAvailable(iface);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Adds a listener.
+     * @param listener A {@link Listener} to add.
+     * @throws IllegalArgumentException If the listener is null.
+     */
+    @UnsupportedAppUsage
+    public void addListener(Listener listener) {
+        if (listener == null) {
+            throw new IllegalArgumentException("listener must not be null");
+        }
+        mListeners.add(listener);
+        if (mListeners.size() == 1) {
+            try {
+                mService.addListener(mServiceListener);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+    }
+
+    /**
+     * Returns an array of available Ethernet interface names.
+     */
+    @UnsupportedAppUsage
+    public String[] getAvailableInterfaces() {
+        try {
+            return mService.getAvailableInterfaces();
+        } catch (RemoteException e) {
+            throw e.rethrowAsRuntimeException();
+        }
+    }
+
+    /**
+     * Removes a listener.
+     * @param listener A {@link Listener} to remove.
+     * @throws IllegalArgumentException If the listener is null.
+     */
+    @UnsupportedAppUsage
+    public void removeListener(Listener listener) {
+        if (listener == null) {
+            throw new IllegalArgumentException("listener must not be null");
+        }
+        mListeners.remove(listener);
+        if (mListeners.isEmpty()) {
+            try {
+                mService.removeListener(mServiceListener);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+    }
+}
diff --git a/android/net/InetAddresses.java b/android/net/InetAddresses.java
new file mode 100644
index 0000000..01b795e
--- /dev/null
+++ b/android/net/InetAddresses.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2018 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.NonNull;
+
+import libcore.net.InetAddressUtils;
+
+import java.net.InetAddress;
+
+/**
+ * Utility methods for {@link InetAddress} implementations.
+ */
+public class InetAddresses {
+
+    private InetAddresses() {}
+
+    /**
+     * Checks to see if the {@code address} is a numeric address (such as {@code "192.0.2.1"} or
+     * {@code "2001:db8::1:2"}).
+     *
+     * <p>A numeric address is either an IPv4 address containing exactly 4 decimal numbers or an
+     * IPv6 numeric address. IPv4 addresses that consist of either hexadecimal or octal digits or
+     * do not have exactly 4 numbers are not treated as numeric.
+     *
+     * <p>This method will never do a DNS lookup.
+     *
+     * @param address the address to parse.
+     * @return true if the supplied address is numeric, false otherwise.
+     */
+    public static boolean isNumericAddress(@NonNull String address) {
+        return InetAddressUtils.isNumericAddress(address);
+    }
+
+    /**
+     * Returns an InetAddress corresponding to the given numeric address (such
+     * as {@code "192.168.0.1"} or {@code "2001:4860:800d::68"}).
+     *
+     * <p>See {@link #isNumericAddress(String)} (String)} for a definition as to what constitutes a
+     * numeric address.
+     *
+     * <p>This method will never do a DNS lookup.
+     *
+     * @param address the address to parse, must be numeric.
+     * @return an {@link InetAddress} instance corresponding to the address.
+     * @throws IllegalArgumentException if {@code address} is not a numeric address.
+     */
+    public static @NonNull InetAddress parseNumericAddress(@NonNull String address) {
+        return InetAddressUtils.parseNumericAddress(address);
+    }
+}
diff --git a/android/net/InterfaceConfiguration.java b/android/net/InterfaceConfiguration.java
new file mode 100644
index 0000000..c97b37b
--- /dev/null
+++ b/android/net/InterfaceConfiguration.java
@@ -0,0 +1,186 @@
+/*
+ * Copyright (C) 2008 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.UnsupportedAppUsage;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.google.android.collect.Sets;
+
+import java.util.HashSet;
+
+/**
+ * Configuration details for a network interface.
+ *
+ * @hide
+ */
+public class InterfaceConfiguration implements Parcelable {
+    private String mHwAddr;
+    private LinkAddress mAddr;
+    private HashSet<String> mFlags = Sets.newHashSet();
+
+    // Must be kept in sync with constant in INetd.aidl
+    private static final String FLAG_UP = "up";
+    private static final String FLAG_DOWN = "down";
+
+    private static final  String[] EMPTY_STRING_ARRAY = new String[0];
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("mHwAddr=").append(mHwAddr);
+        builder.append(" mAddr=").append(String.valueOf(mAddr));
+        builder.append(" mFlags=").append(getFlags());
+        return builder.toString();
+    }
+
+    @UnsupportedAppUsage
+    public Iterable<String> getFlags() {
+        return mFlags;
+    }
+
+    public boolean hasFlag(String flag) {
+        validateFlag(flag);
+        return mFlags.contains(flag);
+    }
+
+    @UnsupportedAppUsage
+    public void clearFlag(String flag) {
+        validateFlag(flag);
+        mFlags.remove(flag);
+    }
+
+    @UnsupportedAppUsage
+    public void setFlag(String flag) {
+        validateFlag(flag);
+        mFlags.add(flag);
+    }
+
+    /**
+     * Set flags to mark interface as up.
+     */
+    @UnsupportedAppUsage
+    public void setInterfaceUp() {
+        mFlags.remove(FLAG_DOWN);
+        mFlags.add(FLAG_UP);
+    }
+
+    /**
+     * Set flags to mark interface as down.
+     */
+    @UnsupportedAppUsage
+    public void setInterfaceDown() {
+        mFlags.remove(FLAG_UP);
+        mFlags.add(FLAG_DOWN);
+    }
+
+    /**
+     * Set flags so that no changes will be made to the up/down status.
+     */
+    public void ignoreInterfaceUpDownStatus() {
+        mFlags.remove(FLAG_UP);
+        mFlags.remove(FLAG_DOWN);
+    }
+
+    public LinkAddress getLinkAddress() {
+        return mAddr;
+    }
+
+    @UnsupportedAppUsage
+    public void setLinkAddress(LinkAddress addr) {
+        mAddr = addr;
+    }
+
+    public String getHardwareAddress() {
+        return mHwAddr;
+    }
+
+    public void setHardwareAddress(String hwAddr) {
+        mHwAddr = hwAddr;
+    }
+
+    /**
+     * This function determines if the interface is up and has a valid IP
+     * configuration (IP address has a non zero octet).
+     *
+     * Note: It is supposed to be quick and hence should not initiate
+     * any network activity
+     */
+    public boolean isActive() {
+        try {
+            if (isUp()) {
+                for (byte b : mAddr.getAddress().getAddress()) {
+                    if (b != 0) return true;
+                }
+            }
+        } catch (NullPointerException e) {
+            return false;
+        }
+        return false;
+    }
+
+    public boolean isUp() {
+        return hasFlag(FLAG_UP);
+    }
+
+    /** {@inheritDoc} */
+    public int describeContents() {
+        return 0;
+    }
+
+    /** {@inheritDoc} */
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeString(mHwAddr);
+        if (mAddr != null) {
+            dest.writeByte((byte)1);
+            dest.writeParcelable(mAddr, flags);
+        } else {
+            dest.writeByte((byte)0);
+        }
+        dest.writeInt(mFlags.size());
+        for (String flag : mFlags) {
+            dest.writeString(flag);
+        }
+    }
+
+    public static final @android.annotation.NonNull Creator<InterfaceConfiguration> CREATOR = new Creator<
+            InterfaceConfiguration>() {
+        public InterfaceConfiguration createFromParcel(Parcel in) {
+            InterfaceConfiguration info = new InterfaceConfiguration();
+            info.mHwAddr = in.readString();
+            if (in.readByte() == 1) {
+                info.mAddr = in.readParcelable(null);
+            }
+            final int size = in.readInt();
+            for (int i = 0; i < size; i++) {
+                info.mFlags.add(in.readString());
+            }
+            return info;
+        }
+
+        public InterfaceConfiguration[] newArray(int size) {
+            return new InterfaceConfiguration[size];
+        }
+    };
+
+    private static void validateFlag(String flag) {
+        if (flag.indexOf(' ') >= 0) {
+            throw new IllegalArgumentException("flag contains space: " + flag);
+        }
+    }
+}
diff --git a/android/net/IpConfiguration.java b/android/net/IpConfiguration.java
new file mode 100644
index 0000000..2af82d7
--- /dev/null
+++ b/android/net/IpConfiguration.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.annotation.UnsupportedAppUsage;
+import android.net.StaticIpConfiguration;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * A class representing a configured network.
+ * @hide
+ */
+public class IpConfiguration implements Parcelable {
+    private static final String TAG = "IpConfiguration";
+
+    public enum IpAssignment {
+        /* Use statically configured IP settings. Configuration can be accessed
+         * with staticIpConfiguration */
+        @UnsupportedAppUsage
+        STATIC,
+        /* Use dynamically configured IP settings */
+        DHCP,
+        /* no IP details are assigned, this is used to indicate
+         * that any existing IP settings should be retained */
+        UNASSIGNED
+    }
+
+    public IpAssignment ipAssignment;
+
+    public StaticIpConfiguration staticIpConfiguration;
+
+    public enum ProxySettings {
+        /* No proxy is to be used. Any existing proxy settings
+         * should be cleared. */
+        @UnsupportedAppUsage
+        NONE,
+        /* Use statically configured proxy. Configuration can be accessed
+         * with httpProxy. */
+        STATIC,
+        /* no proxy details are assigned, this is used to indicate
+         * that any existing proxy settings should be retained */
+        UNASSIGNED,
+        /* Use a Pac based proxy.
+         */
+        PAC
+    }
+
+    public ProxySettings proxySettings;
+
+    @UnsupportedAppUsage
+    public ProxyInfo httpProxy;
+
+    private void init(IpAssignment ipAssignment,
+                      ProxySettings proxySettings,
+                      StaticIpConfiguration staticIpConfiguration,
+                      ProxyInfo httpProxy) {
+        this.ipAssignment = ipAssignment;
+        this.proxySettings = proxySettings;
+        this.staticIpConfiguration = (staticIpConfiguration == null) ?
+                null : new StaticIpConfiguration(staticIpConfiguration);
+        this.httpProxy = (httpProxy == null) ?
+                null : new ProxyInfo(httpProxy);
+    }
+
+    public IpConfiguration() {
+        init(IpAssignment.UNASSIGNED, ProxySettings.UNASSIGNED, null, null);
+    }
+
+    @UnsupportedAppUsage
+    public IpConfiguration(IpAssignment ipAssignment,
+                           ProxySettings proxySettings,
+                           StaticIpConfiguration staticIpConfiguration,
+                           ProxyInfo httpProxy) {
+        init(ipAssignment, proxySettings, staticIpConfiguration, httpProxy);
+    }
+
+    public IpConfiguration(IpConfiguration source) {
+        this();
+        if (source != null) {
+            init(source.ipAssignment, source.proxySettings,
+                 source.staticIpConfiguration, source.httpProxy);
+        }
+    }
+
+    public IpAssignment getIpAssignment() {
+        return ipAssignment;
+    }
+
+    public void setIpAssignment(IpAssignment ipAssignment) {
+        this.ipAssignment = ipAssignment;
+    }
+
+    public StaticIpConfiguration getStaticIpConfiguration() {
+        return staticIpConfiguration;
+    }
+
+    public void setStaticIpConfiguration(StaticIpConfiguration staticIpConfiguration) {
+        this.staticIpConfiguration = staticIpConfiguration;
+    }
+
+    public ProxySettings getProxySettings() {
+        return proxySettings;
+    }
+
+    public void setProxySettings(ProxySettings proxySettings) {
+        this.proxySettings = proxySettings;
+    }
+
+    public ProxyInfo getHttpProxy() {
+        return httpProxy;
+    }
+
+    public void setHttpProxy(ProxyInfo httpProxy) {
+        this.httpProxy = httpProxy;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sbuf = new StringBuilder();
+        sbuf.append("IP assignment: " + ipAssignment.toString());
+        sbuf.append("\n");
+        if (staticIpConfiguration != null) {
+            sbuf.append("Static configuration: " + staticIpConfiguration.toString());
+            sbuf.append("\n");
+        }
+        sbuf.append("Proxy settings: " + proxySettings.toString());
+        sbuf.append("\n");
+        if (httpProxy != null) {
+            sbuf.append("HTTP proxy: " + httpProxy.toString());
+            sbuf.append("\n");
+        }
+
+        return sbuf.toString();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o == this) {
+            return true;
+        }
+
+        if (!(o instanceof IpConfiguration)) {
+            return false;
+        }
+
+        IpConfiguration other = (IpConfiguration) o;
+        return this.ipAssignment == other.ipAssignment &&
+                this.proxySettings == other.proxySettings &&
+                Objects.equals(this.staticIpConfiguration, other.staticIpConfiguration) &&
+                Objects.equals(this.httpProxy, other.httpProxy);
+    }
+
+    @Override
+    public int hashCode() {
+        return 13 + (staticIpConfiguration != null ? staticIpConfiguration.hashCode() : 0) +
+               17 * ipAssignment.ordinal() +
+               47 * proxySettings.ordinal() +
+               83 * httpProxy.hashCode();
+    }
+
+    /** Implement the Parcelable interface */
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Implement the Parcelable interface  */
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeString(ipAssignment.name());
+        dest.writeString(proxySettings.name());
+        dest.writeParcelable(staticIpConfiguration, flags);
+        dest.writeParcelable(httpProxy, flags);
+    }
+
+    /** Implement the Parcelable interface */
+    public static final @android.annotation.NonNull Creator<IpConfiguration> CREATOR =
+        new Creator<IpConfiguration>() {
+            public IpConfiguration createFromParcel(Parcel in) {
+                IpConfiguration config = new IpConfiguration();
+                config.ipAssignment = IpAssignment.valueOf(in.readString());
+                config.proxySettings = ProxySettings.valueOf(in.readString());
+                config.staticIpConfiguration = in.readParcelable(null);
+                config.httpProxy = in.readParcelable(null);
+                return config;
+            }
+
+            public IpConfiguration[] newArray(int size) {
+                return new IpConfiguration[size];
+            }
+        };
+}
diff --git a/android/net/IpMemoryStore.java b/android/net/IpMemoryStore.java
new file mode 100644
index 0000000..6f91e00
--- /dev/null
+++ b/android/net/IpMemoryStore.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2019 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.NonNull;
+import android.content.Context;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Consumer;
+
+/**
+ * Manager class used to communicate with the ip memory store service in the network stack,
+ * which is running in a separate module.
+ * @hide
+*/
+public class IpMemoryStore extends IpMemoryStoreClient {
+    private static final String TAG = IpMemoryStore.class.getSimpleName();
+    @NonNull private final CompletableFuture<IIpMemoryStore> mService;
+    @NonNull private final AtomicReference<CompletableFuture<IIpMemoryStore>> mTailNode;
+
+    public IpMemoryStore(@NonNull final Context context) {
+        super(context);
+        mService = new CompletableFuture<>();
+        mTailNode = new AtomicReference<CompletableFuture<IIpMemoryStore>>(mService);
+        getNetworkStackClient().fetchIpMemoryStore(
+                new IIpMemoryStoreCallbacks.Stub() {
+                    @Override
+                    public void onIpMemoryStoreFetched(@NonNull final IIpMemoryStore memoryStore) {
+                        mService.complete(memoryStore);
+                    }
+
+                    @Override
+                    public int getInterfaceVersion() {
+                        return this.VERSION;
+                    }
+                });
+    }
+
+    /*
+     *  If the IpMemoryStore is ready, this function will run the request synchronously.
+     *  Otherwise, it will enqueue the requests for execution immediately after the
+     *  service becomes ready. The requests are guaranteed to be executed in the order
+     *  they are sumbitted.
+     */
+    @Override
+    protected void runWhenServiceReady(Consumer<IIpMemoryStore> cb) throws ExecutionException {
+        mTailNode.getAndUpdate(future -> future.handle((store, exception) -> {
+            if (exception != null) {
+                // this should never happens since we also catch the exception below
+                Log.wtf(TAG, "Error fetching IpMemoryStore", exception);
+                return store;
+            }
+
+            try {
+                cb.accept(store);
+            } catch (Exception e) {
+                Log.wtf(TAG, "Exception occured: " + e.getMessage());
+            }
+            return store;
+        }));
+    }
+
+    @VisibleForTesting
+    protected NetworkStackClient getNetworkStackClient() {
+        return NetworkStackClient.getInstance();
+    }
+
+    /** Gets an instance of the memory store */
+    @NonNull
+    public static IpMemoryStore getMemoryStore(final Context context) {
+        return new IpMemoryStore(context);
+    }
+}
diff --git a/android/net/IpMemoryStoreClient.java b/android/net/IpMemoryStoreClient.java
new file mode 100644
index 0000000..014b528
--- /dev/null
+++ b/android/net/IpMemoryStoreClient.java
@@ -0,0 +1,227 @@
+/*
+ * Copyright (C) 2019 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.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.net.ipmemorystore.Blob;
+import android.net.ipmemorystore.NetworkAttributes;
+import android.net.ipmemorystore.OnBlobRetrievedListener;
+import android.net.ipmemorystore.OnL2KeyResponseListener;
+import android.net.ipmemorystore.OnNetworkAttributesRetrievedListener;
+import android.net.ipmemorystore.OnSameL3NetworkResponseListener;
+import android.net.ipmemorystore.OnStatusListener;
+import android.net.ipmemorystore.Status;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.util.concurrent.ExecutionException;
+import java.util.function.Consumer;
+
+/**
+ * service used to communicate with the ip memory store service in network stack,
+ * which is running in a separate module.
+ * @hide
+ */
+public abstract class IpMemoryStoreClient {
+    private static final String TAG = IpMemoryStoreClient.class.getSimpleName();
+    private final Context mContext;
+
+    public IpMemoryStoreClient(@NonNull final Context context) {
+        if (context == null) throw new IllegalArgumentException("missing context");
+        mContext = context;
+    }
+
+    protected abstract void runWhenServiceReady(Consumer<IIpMemoryStore> cb)
+            throws ExecutionException;
+
+    @FunctionalInterface
+    private interface ThrowingRunnable {
+        void run() throws RemoteException;
+    }
+
+    private void ignoringRemoteException(ThrowingRunnable r) {
+        ignoringRemoteException("Failed to execute remote procedure call", r);
+    }
+
+    private void ignoringRemoteException(String message, ThrowingRunnable r) {
+        try {
+            r.run();
+        } catch (RemoteException e) {
+            Log.e(TAG, message, e);
+        }
+    }
+
+    /**
+     * Store network attributes for a given L2 key.
+     * If L2Key is null, choose automatically from the attributes ; passing null is equivalent to
+     * calling findL2Key with the attributes and storing in the returned value.
+     *
+     * @param l2Key The L2 key for the L2 network. Clients that don't know or care about the L2
+     *              key and only care about grouping can pass a unique ID here like the ones
+     *              generated by {@code java.util.UUID.randomUUID()}, but keep in mind the low
+     *              relevance of such a network will lead to it being evicted soon if it's not
+     *              refreshed. Use findL2Key to try and find a similar L2Key to these attributes.
+     * @param attributes The attributes for this network.
+     * @param listener A listener that will be invoked to inform of the completion of this call,
+     *                 or null if the client is not interested in learning about success/failure.
+     * Through the listener, returns the L2 key. This is useful if the L2 key was not specified.
+     * If the call failed, the L2 key will be null.
+     */
+    public void storeNetworkAttributes(@NonNull final String l2Key,
+            @NonNull final NetworkAttributes attributes,
+            @Nullable final OnStatusListener listener) {
+        try {
+            runWhenServiceReady(service -> ignoringRemoteException(
+                    () -> service.storeNetworkAttributes(l2Key, attributes.toParcelable(),
+                            OnStatusListener.toAIDL(listener))));
+        } catch (ExecutionException m) {
+            ignoringRemoteException("Error storing network attributes",
+                    () -> listener.onComplete(new Status(Status.ERROR_UNKNOWN)));
+        }
+    }
+
+    /**
+     * Store a binary blob associated with an L2 key and a name.
+     *
+     * @param l2Key The L2 key for this network.
+     * @param clientId The ID of the client.
+     * @param name The name of this data.
+     * @param data The data to store.
+     * @param listener A listener to inform of the completion of this call, or null if the client
+     *        is not interested in learning about success/failure.
+     * Through the listener, returns a status to indicate success or failure.
+     */
+    public void storeBlob(@NonNull final String l2Key, @NonNull final String clientId,
+            @NonNull final String name, @NonNull final Blob data,
+            @Nullable final OnStatusListener listener) {
+        try {
+            runWhenServiceReady(service -> ignoringRemoteException(
+                    () -> service.storeBlob(l2Key, clientId, name, data,
+                            OnStatusListener.toAIDL(listener))));
+        } catch (ExecutionException m) {
+            ignoringRemoteException("Error storing blob",
+                    () -> listener.onComplete(new Status(Status.ERROR_UNKNOWN)));
+        }
+    }
+
+    /**
+     * Returns the best L2 key associated with the attributes.
+     *
+     * This will find a record that would be in the same group as the passed attributes. This is
+     * useful to choose the key for storing a sample or private data when the L2 key is not known.
+     * If multiple records are group-close to these attributes, the closest match is returned.
+     * If multiple records have the same closeness, the one with the smaller (unicode codepoint
+     * order) L2 key is returned.
+     * If no record matches these attributes, null is returned.
+     *
+     * @param attributes The attributes of the network to find.
+     * @param listener The listener that will be invoked to return the answer.
+     * Through the listener, returns the L2 key if one matched, or null.
+     */
+    public void findL2Key(@NonNull final NetworkAttributes attributes,
+            @NonNull final OnL2KeyResponseListener listener) {
+        try {
+            runWhenServiceReady(service -> ignoringRemoteException(
+                    () -> service.findL2Key(attributes.toParcelable(),
+                            OnL2KeyResponseListener.toAIDL(listener))));
+        } catch (ExecutionException m) {
+            ignoringRemoteException("Error finding L2 Key",
+                    () -> listener.onL2KeyResponse(new Status(Status.ERROR_UNKNOWN), null));
+        }
+    }
+
+    /**
+     * Returns whether, to the best of the store's ability to tell, the two specified L2 keys point
+     * to the same L3 network. Group-closeness is used to determine this.
+     *
+     * @param l2Key1 The key for the first network.
+     * @param l2Key2 The key for the second network.
+     * @param listener The listener that will be invoked to return the answer.
+     * Through the listener, a SameL3NetworkResponse containing the answer and confidence.
+     */
+    public void isSameNetwork(@NonNull final String l2Key1, @NonNull final String l2Key2,
+            @NonNull final OnSameL3NetworkResponseListener listener) {
+        try {
+            runWhenServiceReady(service -> ignoringRemoteException(
+                    () -> service.isSameNetwork(l2Key1, l2Key2,
+                            OnSameL3NetworkResponseListener.toAIDL(listener))));
+        } catch (ExecutionException m) {
+            ignoringRemoteException("Error checking for network sameness",
+                    () -> listener.onSameL3NetworkResponse(new Status(Status.ERROR_UNKNOWN), null));
+        }
+    }
+
+    /**
+     * Retrieve the network attributes for a key.
+     * If no record is present for this key, this will return null attributes.
+     *
+     * @param l2Key The key of the network to query.
+     * @param listener The listener that will be invoked to return the answer.
+     * Through the listener, returns the network attributes and the L2 key associated with
+     *         the query.
+     */
+    public void retrieveNetworkAttributes(@NonNull final String l2Key,
+            @NonNull final OnNetworkAttributesRetrievedListener listener) {
+        try {
+            runWhenServiceReady(service -> ignoringRemoteException(
+                    () -> service.retrieveNetworkAttributes(l2Key,
+                            OnNetworkAttributesRetrievedListener.toAIDL(listener))));
+        } catch (ExecutionException m) {
+            ignoringRemoteException("Error retrieving network attributes",
+                    () -> listener.onNetworkAttributesRetrieved(new Status(Status.ERROR_UNKNOWN),
+                            null, null));
+        }
+    }
+
+    /**
+     * Retrieve previously stored private data.
+     * If no data was stored for this L2 key and name this will return null.
+     *
+     * @param l2Key The L2 key.
+     * @param clientId The id of the client that stored this data.
+     * @param name The name of the data.
+     * @param listener The listener that will be invoked to return the answer.
+     * Through the listener, returns the private data (or null), with the L2 key
+     *         and the name of the data associated with the query.
+     */
+    public void retrieveBlob(@NonNull final String l2Key, @NonNull final String clientId,
+            @NonNull final String name, @NonNull final OnBlobRetrievedListener listener) {
+        try {
+            runWhenServiceReady(service -> ignoringRemoteException(
+                    () -> service.retrieveBlob(l2Key, clientId, name,
+                            OnBlobRetrievedListener.toAIDL(listener))));
+        } catch (ExecutionException m) {
+            ignoringRemoteException("Error retrieving blob",
+                    () -> listener.onBlobRetrieved(new Status(Status.ERROR_UNKNOWN),
+                            null, null, null));
+        }
+    }
+
+    /**
+     * Wipe the data in the database upon network factory reset.
+     */
+    public void factoryReset() {
+        try {
+            runWhenServiceReady(service -> ignoringRemoteException(
+                    () -> service.factoryReset()));
+        } catch (ExecutionException m) {
+            Log.e(TAG, "Error executing factory reset", m);
+        }
+    }
+}
diff --git a/android/net/IpPrefix.java b/android/net/IpPrefix.java
new file mode 100644
index 0000000..8cfe6df
--- /dev/null
+++ b/android/net/IpPrefix.java
@@ -0,0 +1,303 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Pair;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.Comparator;
+
+/**
+ * This class represents an IP prefix, i.e., a contiguous block of IP addresses aligned on a
+ * power of two boundary (also known as an "IP subnet"). A prefix is specified by two pieces of
+ * information:
+ *
+ * <ul>
+ * <li>A starting IP address (IPv4 or IPv6). This is the first IP address of the prefix.
+ * <li>A prefix length. This specifies the length of the prefix by specifing the number of bits
+ *     in the IP address, starting from the most significant bit in network byte order, that
+ *     are constant for all addresses in the prefix.
+ * </ul>
+ *
+ * For example, the prefix <code>192.0.2.0/24</code> covers the 256 IPv4 addresses from
+ * <code>192.0.2.0</code> to <code>192.0.2.255</code>, inclusive, and the prefix
+ * <code>2001:db8:1:2</code>  covers the 2^64 IPv6 addresses from <code>2001:db8:1:2::</code> to
+ * <code>2001:db8:1:2:ffff:ffff:ffff:ffff</code>, inclusive.
+ *
+ * Objects of this class are immutable.
+ */
+public final class IpPrefix implements Parcelable {
+    private final byte[] address;  // network byte order
+    private final int prefixLength;
+
+    private void checkAndMaskAddressAndPrefixLength() {
+        if (address.length != 4 && address.length != 16) {
+            throw new IllegalArgumentException(
+                    "IpPrefix has " + address.length + " bytes which is neither 4 nor 16");
+        }
+        NetworkUtils.maskRawAddress(address, prefixLength);
+    }
+
+    /**
+     * Constructs a new {@code IpPrefix} from a byte array containing an IPv4 or IPv6 address in
+     * network byte order and a prefix length. Silently truncates the address to the prefix length,
+     * so for example {@code 192.0.2.1/24} is silently converted to {@code 192.0.2.0/24}.
+     *
+     * @param address the IP address. Must be non-null and exactly 4 or 16 bytes long.
+     * @param prefixLength the prefix length. Must be &gt;= 0 and &lt;= (32 or 128) (IPv4 or IPv6).
+     *
+     * @hide
+     */
+    public IpPrefix(@NonNull byte[] address, @IntRange(from = 0, to = 128) int prefixLength) {
+        this.address = address.clone();
+        this.prefixLength = prefixLength;
+        checkAndMaskAddressAndPrefixLength();
+    }
+
+    /**
+     * Constructs a new {@code IpPrefix} from an IPv4 or IPv6 address and a prefix length. Silently
+     * truncates the address to the prefix length, so for example {@code 192.0.2.1/24} is silently
+     * converted to {@code 192.0.2.0/24}.
+     *
+     * @param address the IP address. Must be non-null.
+     * @param prefixLength the prefix length. Must be &gt;= 0 and &lt;= (32 or 128) (IPv4 or IPv6).
+     * @hide
+     */
+    @SystemApi
+    @TestApi
+    public IpPrefix(@NonNull InetAddress address, @IntRange(from = 0, to = 128) int prefixLength) {
+        // We don't reuse the (byte[], int) constructor because it calls clone() on the byte array,
+        // which is unnecessary because getAddress() already returns a clone.
+        this.address = address.getAddress();
+        this.prefixLength = prefixLength;
+        checkAndMaskAddressAndPrefixLength();
+    }
+
+    /**
+     * Constructs a new IpPrefix from a string such as "192.0.2.1/24" or "2001:db8::1/64".
+     * Silently truncates the address to the prefix length, so for example {@code 192.0.2.1/24}
+     * is silently converted to {@code 192.0.2.0/24}.
+     *
+     * @param prefix the prefix to parse
+     *
+     * @hide
+     */
+    @SystemApi
+    @TestApi
+    public IpPrefix(@NonNull String prefix) {
+        // We don't reuse the (InetAddress, int) constructor because "error: call to this must be
+        // first statement in constructor". We could factor out setting the member variables to an
+        // init() method, but if we did, then we'd have to make the members non-final, or "error:
+        // cannot assign a value to final variable address". So we just duplicate the code here.
+        Pair<InetAddress, Integer> ipAndMask = NetworkUtils.parseIpAndMask(prefix);
+        this.address = ipAndMask.first.getAddress();
+        this.prefixLength = ipAndMask.second;
+        checkAndMaskAddressAndPrefixLength();
+    }
+
+    /**
+     * Compares this {@code IpPrefix} object against the specified object in {@code obj}. Two
+     * objects are equal if they have the same startAddress and prefixLength.
+     *
+     * @param obj the object to be tested for equality.
+     * @return {@code true} if both objects are equal, {@code false} otherwise.
+     */
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof IpPrefix)) {
+            return false;
+        }
+        IpPrefix that = (IpPrefix) obj;
+        return Arrays.equals(this.address, that.address) && this.prefixLength == that.prefixLength;
+    }
+
+    /**
+     * Gets the hashcode of the represented IP prefix.
+     *
+     * @return the appropriate hashcode value.
+     */
+    @Override
+    public int hashCode() {
+        return Arrays.hashCode(address) + 11 * prefixLength;
+    }
+
+    /**
+     * Returns a copy of the first IP address in the prefix. Modifying the returned object does not
+     * change this object's contents.
+     *
+     * @return the address in the form of a byte array.
+     */
+    public @NonNull InetAddress getAddress() {
+        try {
+            return InetAddress.getByAddress(address);
+        } catch (UnknownHostException e) {
+            // Cannot happen. InetAddress.getByAddress can only throw an exception if the byte
+            // array is the wrong length, but we check that in the constructor.
+            throw new IllegalArgumentException("Address is invalid");
+        }
+    }
+
+    /**
+     * Returns a copy of the IP address bytes in network order (the highest order byte is the zeroth
+     * element). Modifying the returned array does not change this object's contents.
+     *
+     * @return the address in the form of a byte array.
+     */
+    public @NonNull byte[] getRawAddress() {
+        return address.clone();
+    }
+
+    /**
+     * Returns the prefix length of this {@code IpPrefix}.
+     *
+     * @return the prefix length.
+     */
+    @IntRange(from = 0, to = 128)
+    public int getPrefixLength() {
+        return prefixLength;
+    }
+
+    /**
+     * Determines whether the prefix contains the specified address.
+     *
+     * @param address An {@link InetAddress} to test.
+     * @return {@code true} if the prefix covers the given address. {@code false} otherwise.
+     */
+    public boolean contains(@NonNull InetAddress address) {
+        byte[] addrBytes = address.getAddress();
+        if (addrBytes == null || addrBytes.length != this.address.length) {
+            return false;
+        }
+        NetworkUtils.maskRawAddress(addrBytes, prefixLength);
+        return Arrays.equals(this.address, addrBytes);
+    }
+
+    /**
+     * Returns whether the specified prefix is entirely contained in this prefix.
+     *
+     * Note this is mathematical inclusion, so a prefix is always contained within itself.
+     * @param otherPrefix the prefix to test
+     * @hide
+     */
+    public boolean containsPrefix(@NonNull IpPrefix otherPrefix) {
+        if (otherPrefix.getPrefixLength() < prefixLength) return false;
+        final byte[] otherAddress = otherPrefix.getRawAddress();
+        NetworkUtils.maskRawAddress(otherAddress, prefixLength);
+        return Arrays.equals(otherAddress, address);
+    }
+
+    /**
+     * @hide
+     */
+    public boolean isIPv6() {
+        return getAddress() instanceof Inet6Address;
+    }
+
+    /**
+     * @hide
+     */
+    public boolean isIPv4() {
+        return getAddress() instanceof Inet4Address;
+    }
+
+    /**
+     * Returns a string representation of this {@code IpPrefix}.
+     *
+     * @return a string such as {@code "192.0.2.0/24"} or {@code "2001:db8:1:2::/64"}.
+     */
+    public String toString() {
+        try {
+            return InetAddress.getByAddress(address).getHostAddress() + "/" + prefixLength;
+        } catch(UnknownHostException e) {
+            // Cosmic rays?
+            throw new IllegalStateException("IpPrefix with invalid address! Shouldn't happen.", e);
+        }
+    }
+
+    /**
+     * Implement the Parcelable interface.
+     */
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Implement the Parcelable interface.
+     */
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeByteArray(address);
+        dest.writeInt(prefixLength);
+    }
+
+    /**
+     * Returns a comparator ordering IpPrefixes by length, shorter to longer.
+     * Contents of the address will break ties.
+     * @hide
+     */
+    public static Comparator<IpPrefix> lengthComparator() {
+        return new Comparator<IpPrefix>() {
+            @Override
+            public int compare(IpPrefix prefix1, IpPrefix prefix2) {
+                if (prefix1.isIPv4()) {
+                    if (prefix2.isIPv6()) return -1;
+                } else {
+                    if (prefix2.isIPv4()) return 1;
+                }
+                final int p1len = prefix1.getPrefixLength();
+                final int p2len = prefix2.getPrefixLength();
+                if (p1len < p2len) return -1;
+                if (p2len < p1len) return 1;
+                final byte[] a1 = prefix1.address;
+                final byte[] a2 = prefix2.address;
+                final int len = a1.length < a2.length ? a1.length : a2.length;
+                for (int i = 0; i < len; ++i) {
+                    if (a1[i] < a2[i]) return -1;
+                    if (a1[i] > a2[i]) return 1;
+                }
+                if (a2.length < len) return 1;
+                if (a1.length < len) return -1;
+                return 0;
+            }
+        };
+    }
+
+    /**
+     * Implement the Parcelable interface.
+     */
+    public static final @android.annotation.NonNull Creator<IpPrefix> CREATOR =
+            new Creator<IpPrefix>() {
+                public IpPrefix createFromParcel(Parcel in) {
+                    byte[] address = in.createByteArray();
+                    int prefixLength = in.readInt();
+                    return new IpPrefix(address, prefixLength);
+                }
+
+                public IpPrefix[] newArray(int size) {
+                    return new IpPrefix[size];
+                }
+            };
+}
diff --git a/android/net/IpSecAlgorithm.java b/android/net/IpSecAlgorithm.java
new file mode 100644
index 0000000..38d9883
--- /dev/null
+++ b/android/net/IpSecAlgorithm.java
@@ -0,0 +1,307 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net;
+
+import android.annotation.NonNull;
+import android.annotation.StringDef;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.HexDump;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+
+/**
+ * This class represents a single algorithm that can be used by an {@link IpSecTransform}.
+ *
+ * @see <a href="https://tools.ietf.org/html/rfc4301">RFC 4301, Security Architecture for the
+ * Internet Protocol</a>
+ */
+public final class IpSecAlgorithm implements Parcelable {
+    private static final String TAG = "IpSecAlgorithm";
+
+    /**
+     * Null cipher.
+     *
+     * @hide
+     */
+    public static final String CRYPT_NULL = "ecb(cipher_null)";
+
+    /**
+     * AES-CBC Encryption/Ciphering Algorithm.
+     *
+     * <p>Valid lengths for this key are {128, 192, 256}.
+     */
+    public static final String CRYPT_AES_CBC = "cbc(aes)";
+
+    /**
+     * MD5 HMAC Authentication/Integrity Algorithm. <b>This algorithm is not recommended for use in
+     * new applications and is provided for legacy compatibility with 3gpp infrastructure.</b>
+     *
+     * <p>Keys for this algorithm must be 128 bits in length.
+     *
+     * <p>Valid truncation lengths are multiples of 8 bits from 96 to 128.
+     */
+    public static final String AUTH_HMAC_MD5 = "hmac(md5)";
+
+    /**
+     * SHA1 HMAC Authentication/Integrity Algorithm. <b>This algorithm is not recommended for use in
+     * new applications and is provided for legacy compatibility with 3gpp infrastructure.</b>
+     *
+     * <p>Keys for this algorithm must be 160 bits in length.
+     *
+     * <p>Valid truncation lengths are multiples of 8 bits from 96 to 160.
+     */
+    public static final String AUTH_HMAC_SHA1 = "hmac(sha1)";
+
+    /**
+     * SHA256 HMAC Authentication/Integrity Algorithm.
+     *
+     * <p>Keys for this algorithm must be 256 bits in length.
+     *
+     * <p>Valid truncation lengths are multiples of 8 bits from 96 to 256.
+     */
+    public static final String AUTH_HMAC_SHA256 = "hmac(sha256)";
+
+    /**
+     * SHA384 HMAC Authentication/Integrity Algorithm.
+     *
+     * <p>Keys for this algorithm must be 384 bits in length.
+     *
+     * <p>Valid truncation lengths are multiples of 8 bits from 192 to 384.
+     */
+    public static final String AUTH_HMAC_SHA384 = "hmac(sha384)";
+
+    /**
+     * SHA512 HMAC Authentication/Integrity Algorithm.
+     *
+     * <p>Keys for this algorithm must be 512 bits in length.
+     *
+     * <p>Valid truncation lengths are multiples of 8 bits from 256 to 512.
+     */
+    public static final String AUTH_HMAC_SHA512 = "hmac(sha512)";
+
+    /**
+     * AES-GCM Authentication/Integrity + Encryption/Ciphering Algorithm.
+     *
+     * <p>Valid lengths for keying material are {160, 224, 288}.
+     *
+     * <p>As per <a href="https://tools.ietf.org/html/rfc4106#section-8.1">RFC4106 (Section
+     * 8.1)</a>, keying material consists of a 128, 192, or 256 bit AES key followed by a 32-bit
+     * salt. RFC compliance requires that the salt must be unique per invocation with the same key.
+     *
+     * <p>Valid ICV (truncation) lengths are {64, 96, 128}.
+     */
+    public static final String AUTH_CRYPT_AES_GCM = "rfc4106(gcm(aes))";
+
+    /** @hide */
+    @StringDef({
+        CRYPT_AES_CBC,
+        AUTH_HMAC_MD5,
+        AUTH_HMAC_SHA1,
+        AUTH_HMAC_SHA256,
+        AUTH_HMAC_SHA384,
+        AUTH_HMAC_SHA512,
+        AUTH_CRYPT_AES_GCM
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface AlgorithmName {}
+
+    private final String mName;
+    private final byte[] mKey;
+    private final int mTruncLenBits;
+
+    /**
+     * Creates an IpSecAlgorithm of one of the supported types. Supported algorithm names are
+     * defined as constants in this class.
+     *
+     * <p>For algorithms that produce an integrity check value, the truncation length is a required
+     * parameter. See {@link #IpSecAlgorithm(String algorithm, byte[] key, int truncLenBits)}
+     *
+     * @param algorithm name of the algorithm.
+     * @param key key padded to a multiple of 8 bits.
+     */
+    public IpSecAlgorithm(@NonNull @AlgorithmName String algorithm, @NonNull byte[] key) {
+        this(algorithm, key, 0);
+    }
+
+    /**
+     * Creates an IpSecAlgorithm of one of the supported types. Supported algorithm names are
+     * defined as constants in this class.
+     *
+     * <p>This constructor only supports algorithms that use a truncation length. i.e.
+     * Authentication and Authenticated Encryption algorithms.
+     *
+     * @param algorithm name of the algorithm.
+     * @param key key padded to a multiple of 8 bits.
+     * @param truncLenBits number of bits of output hash to use.
+     */
+    public IpSecAlgorithm(
+            @NonNull @AlgorithmName String algorithm, @NonNull byte[] key, int truncLenBits) {
+        mName = algorithm;
+        mKey = key.clone();
+        mTruncLenBits = truncLenBits;
+        checkValidOrThrow(mName, mKey.length * 8, mTruncLenBits);
+    }
+
+    /** Get the algorithm name */
+    @NonNull
+    public String getName() {
+        return mName;
+    }
+
+    /** Get the key for this algorithm */
+    @NonNull
+    public byte[] getKey() {
+        return mKey.clone();
+    }
+
+    /** Get the truncation length of this algorithm, in bits */
+    public int getTruncationLengthBits() {
+        return mTruncLenBits;
+    }
+
+    /* Parcelable Implementation */
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Write to parcel */
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeString(mName);
+        out.writeByteArray(mKey);
+        out.writeInt(mTruncLenBits);
+    }
+
+    /** Parcelable Creator */
+    public static final @android.annotation.NonNull Parcelable.Creator<IpSecAlgorithm> CREATOR =
+            new Parcelable.Creator<IpSecAlgorithm>() {
+                public IpSecAlgorithm createFromParcel(Parcel in) {
+                    final String name = in.readString();
+                    final byte[] key = in.createByteArray();
+                    final int truncLenBits = in.readInt();
+
+                    return new IpSecAlgorithm(name, key, truncLenBits);
+                }
+
+                public IpSecAlgorithm[] newArray(int size) {
+                    return new IpSecAlgorithm[size];
+                }
+            };
+
+    private static void checkValidOrThrow(String name, int keyLen, int truncLen) {
+        boolean isValidLen = true;
+        boolean isValidTruncLen = true;
+
+        switch(name) {
+            case CRYPT_AES_CBC:
+                isValidLen = keyLen == 128 || keyLen == 192 || keyLen == 256;
+                break;
+            case AUTH_HMAC_MD5:
+                isValidLen = keyLen == 128;
+                isValidTruncLen = truncLen >= 96 && truncLen <= 128;
+                break;
+            case AUTH_HMAC_SHA1:
+                isValidLen = keyLen == 160;
+                isValidTruncLen = truncLen >= 96 && truncLen <= 160;
+                break;
+            case AUTH_HMAC_SHA256:
+                isValidLen = keyLen == 256;
+                isValidTruncLen = truncLen >= 96 && truncLen <= 256;
+                break;
+            case AUTH_HMAC_SHA384:
+                isValidLen = keyLen == 384;
+                isValidTruncLen = truncLen >= 192 && truncLen <= 384;
+                break;
+            case AUTH_HMAC_SHA512:
+                isValidLen = keyLen == 512;
+                isValidTruncLen = truncLen >= 256 && truncLen <= 512;
+                break;
+            case AUTH_CRYPT_AES_GCM:
+                // The keying material for GCM is a key plus a 32-bit salt
+                isValidLen = keyLen == 128 + 32 || keyLen == 192 + 32 || keyLen == 256 + 32;
+                isValidTruncLen = truncLen == 64 || truncLen == 96 || truncLen == 128;
+                break;
+            default:
+                throw new IllegalArgumentException("Couldn't find an algorithm: " + name);
+        }
+
+        if (!isValidLen) {
+            throw new IllegalArgumentException("Invalid key material keyLength: " + keyLen);
+        }
+        if (!isValidTruncLen) {
+            throw new IllegalArgumentException("Invalid truncation keyLength: " + truncLen);
+        }
+    }
+
+    /** @hide */
+    public boolean isAuthentication() {
+        switch (getName()) {
+            // Fallthrough
+            case AUTH_HMAC_MD5:
+            case AUTH_HMAC_SHA1:
+            case AUTH_HMAC_SHA256:
+            case AUTH_HMAC_SHA384:
+            case AUTH_HMAC_SHA512:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    /** @hide */
+    public boolean isEncryption() {
+        return getName().equals(CRYPT_AES_CBC);
+    }
+
+    /** @hide */
+    public boolean isAead() {
+        return getName().equals(AUTH_CRYPT_AES_GCM);
+    }
+
+    // Because encryption keys are sensitive and userdebug builds are used by large user pools
+    // such as beta testers, we only allow sensitive info such as keys on eng builds.
+    private static boolean isUnsafeBuild() {
+        return Build.IS_DEBUGGABLE && Build.IS_ENG;
+    }
+
+    @Override
+    @NonNull
+    public String toString() {
+        return new StringBuilder()
+                .append("{mName=")
+                .append(mName)
+                .append(", mKey=")
+                .append(isUnsafeBuild() ? HexDump.toHexString(mKey) : "<hidden>")
+                .append(", mTruncLenBits=")
+                .append(mTruncLenBits)
+                .append("}")
+                .toString();
+    }
+
+    /** @hide */
+    @VisibleForTesting
+    public static boolean equals(IpSecAlgorithm lhs, IpSecAlgorithm rhs) {
+        if (lhs == null || rhs == null) return (lhs == rhs);
+        return (lhs.mName.equals(rhs.mName)
+                && Arrays.equals(lhs.mKey, rhs.mKey)
+                && lhs.mTruncLenBits == rhs.mTruncLenBits);
+    }
+};
diff --git a/android/net/IpSecConfig.java b/android/net/IpSecConfig.java
new file mode 100644
index 0000000..a64014f
--- /dev/null
+++ b/android/net/IpSecConfig.java
@@ -0,0 +1,357 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * This class encapsulates all the configuration parameters needed to create IPsec transforms and
+ * policies.
+ *
+ * @hide
+ */
+public final class IpSecConfig implements Parcelable {
+    private static final String TAG = "IpSecConfig";
+
+    // MODE_TRANSPORT or MODE_TUNNEL
+    private int mMode = IpSecTransform.MODE_TRANSPORT;
+
+    // Preventing this from being null simplifies Java->Native binder
+    private String mSourceAddress = "";
+
+    // Preventing this from being null simplifies Java->Native binder
+    private String mDestinationAddress = "";
+
+    // The underlying Network that represents the "gateway" Network
+    // for outbound packets. It may also be used to select packets.
+    private Network mNetwork;
+
+    // Minimum requirements for identifying a transform
+    // SPI identifying the IPsec SA in packet processing
+    // and a destination IP address
+    private int mSpiResourceId = IpSecManager.INVALID_RESOURCE_ID;
+
+    // Encryption Algorithm
+    private IpSecAlgorithm mEncryption;
+
+    // Authentication Algorithm
+    private IpSecAlgorithm mAuthentication;
+
+    // Authenticated Encryption Algorithm
+    private IpSecAlgorithm mAuthenticatedEncryption;
+
+    // For tunnel mode IPv4 UDP Encapsulation
+    // IpSecTransform#ENCAP_ESP_*, such as ENCAP_ESP_OVER_UDP_IKE
+    private int mEncapType = IpSecTransform.ENCAP_NONE;
+    private int mEncapSocketResourceId = IpSecManager.INVALID_RESOURCE_ID;
+    private int mEncapRemotePort;
+
+    // An interval, in seconds between the NattKeepalive packets
+    private int mNattKeepaliveInterval;
+
+    // XFRM mark and mask; defaults to 0 (no mark/mask)
+    private int mMarkValue;
+    private int mMarkMask;
+
+    // XFRM interface id
+    private int mXfrmInterfaceId;
+
+    /** Set the mode for this IPsec transform */
+    public void setMode(int mode) {
+        mMode = mode;
+    }
+
+    /** Set the source IP addres for this IPsec transform */
+    public void setSourceAddress(String sourceAddress) {
+        mSourceAddress = sourceAddress;
+    }
+
+    /** Set the destination IP address for this IPsec transform */
+    public void setDestinationAddress(String destinationAddress) {
+        mDestinationAddress = destinationAddress;
+    }
+
+    /** Set the SPI by resource ID */
+    public void setSpiResourceId(int resourceId) {
+        mSpiResourceId = resourceId;
+    }
+
+    /** Set the encryption algorithm */
+    public void setEncryption(IpSecAlgorithm encryption) {
+        mEncryption = encryption;
+    }
+
+    /** Set the authentication algorithm */
+    public void setAuthentication(IpSecAlgorithm authentication) {
+        mAuthentication = authentication;
+    }
+
+    /** Set the authenticated encryption algorithm */
+    public void setAuthenticatedEncryption(IpSecAlgorithm authenticatedEncryption) {
+        mAuthenticatedEncryption = authenticatedEncryption;
+    }
+
+    /** Set the underlying network that will carry traffic for this transform */
+    public void setNetwork(Network network) {
+        mNetwork = network;
+    }
+
+    public void setEncapType(int encapType) {
+        mEncapType = encapType;
+    }
+
+    public void setEncapSocketResourceId(int resourceId) {
+        mEncapSocketResourceId = resourceId;
+    }
+
+    public void setEncapRemotePort(int port) {
+        mEncapRemotePort = port;
+    }
+
+    public void setNattKeepaliveInterval(int interval) {
+        mNattKeepaliveInterval = interval;
+    }
+
+    /**
+     * Sets the mark value
+     *
+     * <p>Internal (System server) use only. Marks passed in by users will be overwritten or
+     * ignored.
+     */
+    public void setMarkValue(int mark) {
+        mMarkValue = mark;
+    }
+
+    /**
+     * Sets the mark mask
+     *
+     * <p>Internal (System server) use only. Marks passed in by users will be overwritten or
+     * ignored.
+     */
+    public void setMarkMask(int mask) {
+        mMarkMask = mask;
+    }
+
+    public void setXfrmInterfaceId(int xfrmInterfaceId) {
+        mXfrmInterfaceId = xfrmInterfaceId;
+    }
+
+    // Transport or Tunnel
+    public int getMode() {
+        return mMode;
+    }
+
+    public String getSourceAddress() {
+        return mSourceAddress;
+    }
+
+    public int getSpiResourceId() {
+        return mSpiResourceId;
+    }
+
+    public String getDestinationAddress() {
+        return mDestinationAddress;
+    }
+
+    public IpSecAlgorithm getEncryption() {
+        return mEncryption;
+    }
+
+    public IpSecAlgorithm getAuthentication() {
+        return mAuthentication;
+    }
+
+    public IpSecAlgorithm getAuthenticatedEncryption() {
+        return mAuthenticatedEncryption;
+    }
+
+    public Network getNetwork() {
+        return mNetwork;
+    }
+
+    public int getEncapType() {
+        return mEncapType;
+    }
+
+    public int getEncapSocketResourceId() {
+        return mEncapSocketResourceId;
+    }
+
+    public int getEncapRemotePort() {
+        return mEncapRemotePort;
+    }
+
+    public int getNattKeepaliveInterval() {
+        return mNattKeepaliveInterval;
+    }
+
+    public int getMarkValue() {
+        return mMarkValue;
+    }
+
+    public int getMarkMask() {
+        return mMarkMask;
+    }
+
+    public int getXfrmInterfaceId() {
+        return mXfrmInterfaceId;
+    }
+
+    // Parcelable Methods
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeInt(mMode);
+        out.writeString(mSourceAddress);
+        out.writeString(mDestinationAddress);
+        out.writeParcelable(mNetwork, flags);
+        out.writeInt(mSpiResourceId);
+        out.writeParcelable(mEncryption, flags);
+        out.writeParcelable(mAuthentication, flags);
+        out.writeParcelable(mAuthenticatedEncryption, flags);
+        out.writeInt(mEncapType);
+        out.writeInt(mEncapSocketResourceId);
+        out.writeInt(mEncapRemotePort);
+        out.writeInt(mNattKeepaliveInterval);
+        out.writeInt(mMarkValue);
+        out.writeInt(mMarkMask);
+        out.writeInt(mXfrmInterfaceId);
+    }
+
+    @VisibleForTesting
+    public IpSecConfig() {}
+
+    /** Copy constructor */
+    @VisibleForTesting
+    public IpSecConfig(IpSecConfig c) {
+        mMode = c.mMode;
+        mSourceAddress = c.mSourceAddress;
+        mDestinationAddress = c.mDestinationAddress;
+        mNetwork = c.mNetwork;
+        mSpiResourceId = c.mSpiResourceId;
+        mEncryption = c.mEncryption;
+        mAuthentication = c.mAuthentication;
+        mAuthenticatedEncryption = c.mAuthenticatedEncryption;
+        mEncapType = c.mEncapType;
+        mEncapSocketResourceId = c.mEncapSocketResourceId;
+        mEncapRemotePort = c.mEncapRemotePort;
+        mNattKeepaliveInterval = c.mNattKeepaliveInterval;
+        mMarkValue = c.mMarkValue;
+        mMarkMask = c.mMarkMask;
+        mXfrmInterfaceId = c.mXfrmInterfaceId;
+    }
+
+    private IpSecConfig(Parcel in) {
+        mMode = in.readInt();
+        mSourceAddress = in.readString();
+        mDestinationAddress = in.readString();
+        mNetwork = (Network) in.readParcelable(Network.class.getClassLoader());
+        mSpiResourceId = in.readInt();
+        mEncryption =
+                (IpSecAlgorithm) in.readParcelable(IpSecAlgorithm.class.getClassLoader());
+        mAuthentication =
+                (IpSecAlgorithm) in.readParcelable(IpSecAlgorithm.class.getClassLoader());
+        mAuthenticatedEncryption =
+                (IpSecAlgorithm) in.readParcelable(IpSecAlgorithm.class.getClassLoader());
+        mEncapType = in.readInt();
+        mEncapSocketResourceId = in.readInt();
+        mEncapRemotePort = in.readInt();
+        mNattKeepaliveInterval = in.readInt();
+        mMarkValue = in.readInt();
+        mMarkMask = in.readInt();
+        mXfrmInterfaceId = in.readInt();
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder strBuilder = new StringBuilder();
+        strBuilder
+                .append("{mMode=")
+                .append(mMode == IpSecTransform.MODE_TUNNEL ? "TUNNEL" : "TRANSPORT")
+                .append(", mSourceAddress=")
+                .append(mSourceAddress)
+                .append(", mDestinationAddress=")
+                .append(mDestinationAddress)
+                .append(", mNetwork=")
+                .append(mNetwork)
+                .append(", mEncapType=")
+                .append(mEncapType)
+                .append(", mEncapSocketResourceId=")
+                .append(mEncapSocketResourceId)
+                .append(", mEncapRemotePort=")
+                .append(mEncapRemotePort)
+                .append(", mNattKeepaliveInterval=")
+                .append(mNattKeepaliveInterval)
+                .append("{mSpiResourceId=")
+                .append(mSpiResourceId)
+                .append(", mEncryption=")
+                .append(mEncryption)
+                .append(", mAuthentication=")
+                .append(mAuthentication)
+                .append(", mAuthenticatedEncryption=")
+                .append(mAuthenticatedEncryption)
+                .append(", mMarkValue=")
+                .append(mMarkValue)
+                .append(", mMarkMask=")
+                .append(mMarkMask)
+                .append(", mXfrmInterfaceId=")
+                .append(mXfrmInterfaceId)
+                .append("}");
+
+        return strBuilder.toString();
+    }
+
+    public static final @android.annotation.NonNull Parcelable.Creator<IpSecConfig> CREATOR =
+            new Parcelable.Creator<IpSecConfig>() {
+                public IpSecConfig createFromParcel(Parcel in) {
+                    return new IpSecConfig(in);
+                }
+
+                public IpSecConfig[] newArray(int size) {
+                    return new IpSecConfig[size];
+                }
+            };
+
+    @VisibleForTesting
+    /** Equals method used for testing */
+    public static boolean equals(IpSecConfig lhs, IpSecConfig rhs) {
+        if (lhs == null || rhs == null) return (lhs == rhs);
+        return (lhs.mMode == rhs.mMode
+                && lhs.mSourceAddress.equals(rhs.mSourceAddress)
+                && lhs.mDestinationAddress.equals(rhs.mDestinationAddress)
+                && ((lhs.mNetwork != null && lhs.mNetwork.equals(rhs.mNetwork))
+                        || (lhs.mNetwork == rhs.mNetwork))
+                && lhs.mEncapType == rhs.mEncapType
+                && lhs.mEncapSocketResourceId == rhs.mEncapSocketResourceId
+                && lhs.mEncapRemotePort == rhs.mEncapRemotePort
+                && lhs.mNattKeepaliveInterval == rhs.mNattKeepaliveInterval
+                && lhs.mSpiResourceId == rhs.mSpiResourceId
+                && IpSecAlgorithm.equals(lhs.mEncryption, rhs.mEncryption)
+                && IpSecAlgorithm.equals(lhs.mAuthenticatedEncryption, rhs.mAuthenticatedEncryption)
+                && IpSecAlgorithm.equals(lhs.mAuthentication, rhs.mAuthentication)
+                && lhs.mMarkValue == rhs.mMarkValue
+                && lhs.mMarkMask == rhs.mMarkMask
+                && lhs.mXfrmInterfaceId == rhs.mXfrmInterfaceId);
+    }
+}
diff --git a/android/net/IpSecManager.java b/android/net/IpSecManager.java
new file mode 100644
index 0000000..83813da
--- /dev/null
+++ b/android/net/IpSecManager.java
@@ -0,0 +1,990 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net;
+
+import static com.android.internal.util.Preconditions.checkNotNull;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.RequiresFeature;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.annotation.SystemService;
+import android.annotation.TestApi;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Binder;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.system.ErrnoException;
+import android.system.OsConstants;
+import android.util.AndroidException;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import dalvik.system.CloseGuard;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.net.Socket;
+
+/**
+ * This class contains methods for managing IPsec sessions. Once configured, the kernel will apply
+ * confidentiality (encryption) and integrity (authentication) to IP traffic.
+ *
+ * <p>Note that not all aspects of IPsec are permitted by this API. Applications may create
+ * transport mode security associations and apply them to individual sockets. Applications looking
+ * to create a VPN should use {@link VpnService}.
+ *
+ * @see <a href="https://tools.ietf.org/html/rfc4301">RFC 4301, Security Architecture for the
+ *     Internet Protocol</a>
+ */
+@SystemService(Context.IPSEC_SERVICE)
+public final class IpSecManager {
+    private static final String TAG = "IpSecManager";
+
+    /**
+     * Used when applying a transform to direct traffic through an {@link IpSecTransform}
+     * towards the host.
+     *
+     * <p>See {@link #applyTransportModeTransform(Socket, int, IpSecTransform)}.
+     */
+    public static final int DIRECTION_IN = 0;
+
+    /**
+     * Used when applying a transform to direct traffic through an {@link IpSecTransform}
+     * away from the host.
+     *
+     * <p>See {@link #applyTransportModeTransform(Socket, int, IpSecTransform)}.
+     */
+    public static final int DIRECTION_OUT = 1;
+
+    /** @hide */
+    @IntDef(value = {DIRECTION_IN, DIRECTION_OUT})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface PolicyDirection {}
+
+    /**
+     * The Security Parameter Index (SPI) 0 indicates an unknown or invalid index.
+     *
+     * <p>No IPsec packet may contain an SPI of 0.
+     *
+     * @hide
+     */
+    @TestApi public static final int INVALID_SECURITY_PARAMETER_INDEX = 0;
+
+    /** @hide */
+    public interface Status {
+        public static final int OK = 0;
+        public static final int RESOURCE_UNAVAILABLE = 1;
+        public static final int SPI_UNAVAILABLE = 2;
+    }
+
+    /** @hide */
+    public static final int INVALID_RESOURCE_ID = -1;
+
+    /**
+     * Thrown to indicate that a requested SPI is in use.
+     *
+     * <p>The combination of remote {@code InetAddress} and SPI must be unique across all apps on
+     * one device. If this error is encountered, a new SPI is required before a transform may be
+     * created. This error can be avoided by calling {@link
+     * IpSecManager#allocateSecurityParameterIndex}.
+     */
+    public static final class SpiUnavailableException extends AndroidException {
+        private final int mSpi;
+
+        /**
+         * Construct an exception indicating that a transform with the given SPI is already in use
+         * or otherwise unavailable.
+         *
+         * @param msg description indicating the colliding SPI
+         * @param spi the SPI that could not be used due to a collision
+         */
+        SpiUnavailableException(String msg, int spi) {
+            super(msg + " (spi: " + spi + ")");
+            mSpi = spi;
+        }
+
+        /** Get the SPI that caused a collision. */
+        public int getSpi() {
+            return mSpi;
+        }
+    }
+
+    /**
+     * Thrown to indicate that an IPsec resource is unavailable.
+     *
+     * <p>This could apply to resources such as sockets, {@link SecurityParameterIndex}, {@link
+     * IpSecTransform}, or other system resources. If this exception is thrown, users should release
+     * allocated objects of the type requested.
+     */
+    public static final class ResourceUnavailableException extends AndroidException {
+
+        ResourceUnavailableException(String msg) {
+            super(msg);
+        }
+    }
+
+    private final Context mContext;
+    private final IIpSecService mService;
+
+    /**
+     * This class represents a reserved SPI.
+     *
+     * <p>Objects of this type are used to track reserved security parameter indices. They can be
+     * obtained by calling {@link IpSecManager#allocateSecurityParameterIndex} and must be released
+     * by calling {@link #close()} when they are no longer needed.
+     */
+    public static final class SecurityParameterIndex implements AutoCloseable {
+        private final IIpSecService mService;
+        private final InetAddress mDestinationAddress;
+        private final CloseGuard mCloseGuard = CloseGuard.get();
+        private int mSpi = INVALID_SECURITY_PARAMETER_INDEX;
+        private int mResourceId = INVALID_RESOURCE_ID;
+
+        /** Get the underlying SPI held by this object. */
+        public int getSpi() {
+            return mSpi;
+        }
+
+        /**
+         * Release an SPI that was previously reserved.
+         *
+         * <p>Release an SPI for use by other users in the system. If a SecurityParameterIndex is
+         * applied to an IpSecTransform, it will become unusable for future transforms but should
+         * still be closed to ensure system resources are released.
+         */
+        @Override
+        public void close() {
+            try {
+                mService.releaseSecurityParameterIndex(mResourceId);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            } catch (Exception e) {
+                // On close we swallow all random exceptions since failure to close is not
+                // actionable by the user.
+                Log.e(TAG, "Failed to close " + this + ", Exception=" + e);
+            } finally {
+                mResourceId = INVALID_RESOURCE_ID;
+                mCloseGuard.close();
+            }
+        }
+
+        /** Check that the SPI was closed properly. */
+        @Override
+        protected void finalize() throws Throwable {
+            if (mCloseGuard != null) {
+                mCloseGuard.warnIfOpen();
+            }
+
+            close();
+        }
+
+        private SecurityParameterIndex(
+                @NonNull IIpSecService service, InetAddress destinationAddress, int spi)
+                throws ResourceUnavailableException, SpiUnavailableException {
+            mService = service;
+            mDestinationAddress = destinationAddress;
+            try {
+                IpSecSpiResponse result =
+                        mService.allocateSecurityParameterIndex(
+                                destinationAddress.getHostAddress(), spi, new Binder());
+
+                if (result == null) {
+                    throw new NullPointerException("Received null response from IpSecService");
+                }
+
+                int status = result.status;
+                switch (status) {
+                    case Status.OK:
+                        break;
+                    case Status.RESOURCE_UNAVAILABLE:
+                        throw new ResourceUnavailableException(
+                                "No more SPIs may be allocated by this requester.");
+                    case Status.SPI_UNAVAILABLE:
+                        throw new SpiUnavailableException("Requested SPI is unavailable", spi);
+                    default:
+                        throw new RuntimeException(
+                                "Unknown status returned by IpSecService: " + status);
+                }
+                mSpi = result.spi;
+                mResourceId = result.resourceId;
+
+                if (mSpi == INVALID_SECURITY_PARAMETER_INDEX) {
+                    throw new RuntimeException("Invalid SPI returned by IpSecService: " + status);
+                }
+
+                if (mResourceId == INVALID_RESOURCE_ID) {
+                    throw new RuntimeException(
+                            "Invalid Resource ID returned by IpSecService: " + status);
+                }
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+            mCloseGuard.open("open");
+        }
+
+        /** @hide */
+        @VisibleForTesting
+        public int getResourceId() {
+            return mResourceId;
+        }
+
+        @Override
+        public String toString() {
+            return new StringBuilder()
+                .append("SecurityParameterIndex{spi=")
+                .append(mSpi)
+                .append(",resourceId=")
+                .append(mResourceId)
+                .append("}")
+                .toString();
+        }
+    }
+
+    /**
+     * Reserve a random SPI for traffic bound to or from the specified destination address.
+     *
+     * <p>If successful, this SPI is guaranteed available until released by a call to {@link
+     * SecurityParameterIndex#close()}.
+     *
+     * @param destinationAddress the destination address for traffic bearing the requested SPI.
+     *     For inbound traffic, the destination should be an address currently assigned on-device.
+     * @return the reserved SecurityParameterIndex
+     * @throws {@link #ResourceUnavailableException} indicating that too many SPIs are
+     *     currently allocated for this user
+     */
+    @NonNull
+    public SecurityParameterIndex allocateSecurityParameterIndex(
+                @NonNull InetAddress destinationAddress) throws ResourceUnavailableException {
+        try {
+            return new SecurityParameterIndex(
+                    mService,
+                    destinationAddress,
+                    IpSecManager.INVALID_SECURITY_PARAMETER_INDEX);
+        } catch (ServiceSpecificException e) {
+            throw rethrowUncheckedExceptionFromServiceSpecificException(e);
+        } catch (SpiUnavailableException unlikely) {
+            // Because this function allocates a totally random SPI, it really shouldn't ever
+            // fail to allocate an SPI; we simply need this because the exception is checked.
+            throw new ResourceUnavailableException("No SPIs available");
+        }
+    }
+
+    /**
+     * Reserve the requested SPI for traffic bound to or from the specified destination address.
+     *
+     * <p>If successful, this SPI is guaranteed available until released by a call to {@link
+     * SecurityParameterIndex#close()}.
+     *
+     * @param destinationAddress the destination address for traffic bearing the requested SPI.
+     *     For inbound traffic, the destination should be an address currently assigned on-device.
+     * @param requestedSpi the requested SPI. The range 1-255 is reserved and may not be used. See
+     *     RFC 4303 Section 2.1.
+     * @return the reserved SecurityParameterIndex
+     * @throws {@link #ResourceUnavailableException} indicating that too many SPIs are
+     *     currently allocated for this user
+     * @throws {@link #SpiUnavailableException} indicating that the requested SPI could not be
+     *     reserved
+     */
+    @NonNull
+    public SecurityParameterIndex allocateSecurityParameterIndex(
+            @NonNull InetAddress destinationAddress, int requestedSpi)
+            throws SpiUnavailableException, ResourceUnavailableException {
+        if (requestedSpi == IpSecManager.INVALID_SECURITY_PARAMETER_INDEX) {
+            throw new IllegalArgumentException("Requested SPI must be a valid (non-zero) SPI");
+        }
+        try {
+            return new SecurityParameterIndex(mService, destinationAddress, requestedSpi);
+        } catch (ServiceSpecificException e) {
+            throw rethrowUncheckedExceptionFromServiceSpecificException(e);
+        }
+    }
+
+    /**
+     * Apply an IPsec transform to a stream socket.
+     *
+     * <p>This applies transport mode encapsulation to the given socket. Once applied, I/O on the
+     * socket will be encapsulated according to the parameters of the {@code IpSecTransform}. When
+     * the transform is removed from the socket by calling {@link #removeTransportModeTransforms},
+     * unprotected traffic can resume on that socket.
+     *
+     * <p>For security reasons, the destination address of any traffic on the socket must match the
+     * remote {@code InetAddress} of the {@code IpSecTransform}. Attempts to send traffic to any
+     * other IP address will result in an IOException. In addition, reads and writes on the socket
+     * will throw IOException if the user deactivates the transform (by calling {@link
+     * IpSecTransform#close()}) without calling {@link #removeTransportModeTransforms}.
+     *
+     * <p>Note that when applied to TCP sockets, calling {@link IpSecTransform#close()} on an
+     * applied transform before completion of graceful shutdown may result in the shutdown sequence
+     * failing to complete. As such, applications requiring graceful shutdown MUST close the socket
+     * prior to deactivating the applied transform. Socket closure may be performed asynchronously
+     * (in batches), so the returning of a close function does not guarantee shutdown of a socket.
+     * Setting an SO_LINGER timeout results in socket closure being performed synchronously, and is
+     * sufficient to ensure shutdown.
+     *
+     * Specifically, if the transform is deactivated (by calling {@link IpSecTransform#close()}),
+     * prior to the socket being closed, the standard [FIN - FIN/ACK - ACK], or the reset [RST]
+     * packets are dropped due to the lack of a valid Transform. Similarly, if a socket without the
+     * SO_LINGER option set is closed, the delayed/batched FIN packets may be dropped.
+     *
+     * <h4>Rekey Procedure</h4>
+     *
+     * <p>When applying a new tranform to a socket in the outbound direction, the previous transform
+     * will be removed and the new transform will take effect immediately, sending all traffic on
+     * the new transform; however, when applying a transform in the inbound direction, traffic
+     * on the old transform will continue to be decrypted and delivered until that transform is
+     * deallocated by calling {@link IpSecTransform#close()}. This overlap allows lossless rekey
+     * procedures where both transforms are valid until both endpoints are using the new transform
+     * and all in-flight packets have been received.
+     *
+     * @param socket a stream socket
+     * @param direction the direction in which the transform should be applied
+     * @param transform a transport mode {@code IpSecTransform}
+     * @throws IOException indicating that the transform could not be applied
+     */
+    public void applyTransportModeTransform(@NonNull Socket socket,
+            @PolicyDirection int direction, @NonNull IpSecTransform transform) throws IOException {
+        // Ensure creation of FD. See b/77548890 for more details.
+        socket.getSoLinger();
+
+        applyTransportModeTransform(socket.getFileDescriptor$(), direction, transform);
+    }
+
+    /**
+     * Apply an IPsec transform to a datagram socket.
+     *
+     * <p>This applies transport mode encapsulation to the given socket. Once applied, I/O on the
+     * socket will be encapsulated according to the parameters of the {@code IpSecTransform}. When
+     * the transform is removed from the socket by calling {@link #removeTransportModeTransforms},
+     * unprotected traffic can resume on that socket.
+     *
+     * <p>For security reasons, the destination address of any traffic on the socket must match the
+     * remote {@code InetAddress} of the {@code IpSecTransform}. Attempts to send traffic to any
+     * other IP address will result in an IOException. In addition, reads and writes on the socket
+     * will throw IOException if the user deactivates the transform (by calling {@link
+     * IpSecTransform#close()}) without calling {@link #removeTransportModeTransforms}.
+     *
+     * <h4>Rekey Procedure</h4>
+     *
+     * <p>When applying a new tranform to a socket in the outbound direction, the previous transform
+     * will be removed and the new transform will take effect immediately, sending all traffic on
+     * the new transform; however, when applying a transform in the inbound direction, traffic
+     * on the old transform will continue to be decrypted and delivered until that transform is
+     * deallocated by calling {@link IpSecTransform#close()}. This overlap allows lossless rekey
+     * procedures where both transforms are valid until both endpoints are using the new transform
+     * and all in-flight packets have been received.
+     *
+     * @param socket a datagram socket
+     * @param direction the direction in which the transform should be applied
+     * @param transform a transport mode {@code IpSecTransform}
+     * @throws IOException indicating that the transform could not be applied
+     */
+    public void applyTransportModeTransform(@NonNull DatagramSocket socket,
+            @PolicyDirection int direction, @NonNull IpSecTransform transform) throws IOException {
+        applyTransportModeTransform(socket.getFileDescriptor$(), direction, transform);
+    }
+
+    /**
+     * Apply an IPsec transform to a socket.
+     *
+     * <p>This applies transport mode encapsulation to the given socket. Once applied, I/O on the
+     * socket will be encapsulated according to the parameters of the {@code IpSecTransform}. When
+     * the transform is removed from the socket by calling {@link #removeTransportModeTransforms},
+     * unprotected traffic can resume on that socket.
+     *
+     * <p>For security reasons, the destination address of any traffic on the socket must match the
+     * remote {@code InetAddress} of the {@code IpSecTransform}. Attempts to send traffic to any
+     * other IP address will result in an IOException. In addition, reads and writes on the socket
+     * will throw IOException if the user deactivates the transform (by calling {@link
+     * IpSecTransform#close()}) without calling {@link #removeTransportModeTransforms}.
+     *
+     * <p>Note that when applied to TCP sockets, calling {@link IpSecTransform#close()} on an
+     * applied transform before completion of graceful shutdown may result in the shutdown sequence
+     * failing to complete. As such, applications requiring graceful shutdown MUST close the socket
+     * prior to deactivating the applied transform. Socket closure may be performed asynchronously
+     * (in batches), so the returning of a close function does not guarantee shutdown of a socket.
+     * Setting an SO_LINGER timeout results in socket closure being performed synchronously, and is
+     * sufficient to ensure shutdown.
+     *
+     * Specifically, if the transform is deactivated (by calling {@link IpSecTransform#close()}),
+     * prior to the socket being closed, the standard [FIN - FIN/ACK - ACK], or the reset [RST]
+     * packets are dropped due to the lack of a valid Transform. Similarly, if a socket without the
+     * SO_LINGER option set is closed, the delayed/batched FIN packets may be dropped.
+     *
+     * <h4>Rekey Procedure</h4>
+     *
+     * <p>When applying a new tranform to a socket in the outbound direction, the previous transform
+     * will be removed and the new transform will take effect immediately, sending all traffic on
+     * the new transform; however, when applying a transform in the inbound direction, traffic
+     * on the old transform will continue to be decrypted and delivered until that transform is
+     * deallocated by calling {@link IpSecTransform#close()}. This overlap allows lossless rekey
+     * procedures where both transforms are valid until both endpoints are using the new transform
+     * and all in-flight packets have been received.
+     *
+     * @param socket a socket file descriptor
+     * @param direction the direction in which the transform should be applied
+     * @param transform a transport mode {@code IpSecTransform}
+     * @throws IOException indicating that the transform could not be applied
+     */
+    public void applyTransportModeTransform(@NonNull FileDescriptor socket,
+            @PolicyDirection int direction, @NonNull IpSecTransform transform) throws IOException {
+        // We dup() the FileDescriptor here because if we don't, then the ParcelFileDescriptor()
+        // constructor takes control and closes the user's FD when we exit the method.
+        try (ParcelFileDescriptor pfd = ParcelFileDescriptor.dup(socket)) {
+            mService.applyTransportModeTransform(pfd, direction, transform.getResourceId());
+        } catch (ServiceSpecificException e) {
+            throw rethrowCheckedExceptionFromServiceSpecificException(e);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Remove an IPsec transform from a stream socket.
+     *
+     * <p>Once removed, traffic on the socket will not be encrypted. Removing transforms from a
+     * socket allows the socket to be reused for communication in the clear.
+     *
+     * <p>If an {@code IpSecTransform} object applied to this socket was deallocated by calling
+     * {@link IpSecTransform#close()}, then communication on the socket will fail until this method
+     * is called.
+     *
+     * @param socket a socket that previously had a transform applied to it
+     * @throws IOException indicating that the transform could not be removed from the socket
+     */
+    public void removeTransportModeTransforms(@NonNull Socket socket) throws IOException {
+        // Ensure creation of FD. See b/77548890 for more details.
+        socket.getSoLinger();
+
+        removeTransportModeTransforms(socket.getFileDescriptor$());
+    }
+
+    /**
+     * Remove an IPsec transform from a datagram socket.
+     *
+     * <p>Once removed, traffic on the socket will not be encrypted. Removing transforms from a
+     * socket allows the socket to be reused for communication in the clear.
+     *
+     * <p>If an {@code IpSecTransform} object applied to this socket was deallocated by calling
+     * {@link IpSecTransform#close()}, then communication on the socket will fail until this method
+     * is called.
+     *
+     * @param socket a socket that previously had a transform applied to it
+     * @throws IOException indicating that the transform could not be removed from the socket
+     */
+    public void removeTransportModeTransforms(@NonNull DatagramSocket socket) throws IOException {
+        removeTransportModeTransforms(socket.getFileDescriptor$());
+    }
+
+    /**
+     * Remove an IPsec transform from a socket.
+     *
+     * <p>Once removed, traffic on the socket will not be encrypted. Removing transforms from a
+     * socket allows the socket to be reused for communication in the clear.
+     *
+     * <p>If an {@code IpSecTransform} object applied to this socket was deallocated by calling
+     * {@link IpSecTransform#close()}, then communication on the socket will fail until this method
+     * is called.
+     *
+     * @param socket a socket that previously had a transform applied to it
+     * @throws IOException indicating that the transform could not be removed from the socket
+     */
+    public void removeTransportModeTransforms(@NonNull FileDescriptor socket) throws IOException {
+        try (ParcelFileDescriptor pfd = ParcelFileDescriptor.dup(socket)) {
+            mService.removeTransportModeTransforms(pfd);
+        } catch (ServiceSpecificException e) {
+            throw rethrowCheckedExceptionFromServiceSpecificException(e);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Remove a Tunnel Mode IPsec Transform from a {@link Network}. This must be used as part of
+     * cleanup if a tunneled Network experiences a change in default route. The Network will drop
+     * all traffic that cannot be routed to the Tunnel's outbound interface. If that interface is
+     * lost, all traffic will drop.
+     *
+     * <p>TODO: Update javadoc for tunnel mode APIs at the same time the APIs are re-worked.
+     *
+     * @param net a network that currently has transform applied to it.
+     * @param transform a Tunnel Mode IPsec Transform that has been previously applied to the given
+     *     network
+     * @hide
+     */
+    public void removeTunnelModeTransform(Network net, IpSecTransform transform) {}
+
+    /**
+     * This class provides access to a UDP encapsulation Socket.
+     *
+     * <p>{@code UdpEncapsulationSocket} wraps a system-provided datagram socket intended for IKEv2
+     * signalling and UDP encapsulated IPsec traffic. Instances can be obtained by calling {@link
+     * IpSecManager#openUdpEncapsulationSocket}. The provided socket cannot be re-bound by the
+     * caller. The caller should not close the {@code FileDescriptor} returned by {@link
+     * #getFileDescriptor}, but should use {@link #close} instead.
+     *
+     * <p>Allowing the user to close or unbind a UDP encapsulation socket could impact the traffic
+     * of the next user who binds to that port. To prevent this scenario, these sockets are held
+     * open by the system so that they may only be closed by calling {@link #close} or when the user
+     * process exits.
+     */
+    public static final class UdpEncapsulationSocket implements AutoCloseable {
+        private final ParcelFileDescriptor mPfd;
+        private final IIpSecService mService;
+        private int mResourceId = INVALID_RESOURCE_ID;
+        private final int mPort;
+        private final CloseGuard mCloseGuard = CloseGuard.get();
+
+        private UdpEncapsulationSocket(@NonNull IIpSecService service, int port)
+                throws ResourceUnavailableException, IOException {
+            mService = service;
+            try {
+                IpSecUdpEncapResponse result =
+                        mService.openUdpEncapsulationSocket(port, new Binder());
+                switch (result.status) {
+                    case Status.OK:
+                        break;
+                    case Status.RESOURCE_UNAVAILABLE:
+                        throw new ResourceUnavailableException(
+                                "No more Sockets may be allocated by this requester.");
+                    default:
+                        throw new RuntimeException(
+                                "Unknown status returned by IpSecService: " + result.status);
+                }
+                mResourceId = result.resourceId;
+                mPort = result.port;
+                mPfd = result.fileDescriptor;
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+            mCloseGuard.open("constructor");
+        }
+
+        /** Get the encapsulation socket's file descriptor. */
+        public FileDescriptor getFileDescriptor() {
+            if (mPfd == null) {
+                return null;
+            }
+            return mPfd.getFileDescriptor();
+        }
+
+        /** Get the bound port of the wrapped socket. */
+        public int getPort() {
+            return mPort;
+        }
+
+        /**
+         * Close this socket.
+         *
+         * <p>This closes the wrapped socket. Open encapsulation sockets count against a user's
+         * resource limits, and forgetting to close them eventually will result in {@link
+         * ResourceUnavailableException} being thrown.
+         */
+        @Override
+        public void close() throws IOException {
+            try {
+                mService.closeUdpEncapsulationSocket(mResourceId);
+                mResourceId = INVALID_RESOURCE_ID;
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            } catch (Exception e) {
+                // On close we swallow all random exceptions since failure to close is not
+                // actionable by the user.
+                Log.e(TAG, "Failed to close " + this + ", Exception=" + e);
+            } finally {
+                mResourceId = INVALID_RESOURCE_ID;
+                mCloseGuard.close();
+            }
+
+            try {
+                mPfd.close();
+            } catch (IOException e) {
+                Log.e(TAG, "Failed to close UDP Encapsulation Socket with Port= " + mPort);
+                throw e;
+            }
+        }
+
+        /** Check that the socket was closed properly. */
+        @Override
+        protected void finalize() throws Throwable {
+            if (mCloseGuard != null) {
+                mCloseGuard.warnIfOpen();
+            }
+            close();
+        }
+
+        /** @hide */
+        @VisibleForTesting
+        public int getResourceId() {
+            return mResourceId;
+        }
+
+        @Override
+        public String toString() {
+            return new StringBuilder()
+                .append("UdpEncapsulationSocket{port=")
+                .append(mPort)
+                .append(",resourceId=")
+                .append(mResourceId)
+                .append("}")
+                .toString();
+        }
+    };
+
+    /**
+     * Open a socket for UDP encapsulation and bind to the given port.
+     *
+     * <p>See {@link UdpEncapsulationSocket} for the proper way to close the returned socket.
+     *
+     * @param port a local UDP port
+     * @return a socket that is bound to the given port
+     * @throws IOException indicating that the socket could not be opened or bound
+     * @throws ResourceUnavailableException indicating that too many encapsulation sockets are open
+     */
+    // Returning a socket in this fashion that has been created and bound by the system
+    // is the only safe way to ensure that a socket is both accessible to the user and
+    // safely usable for Encapsulation without allowing a user to possibly unbind from/close
+    // the port, which could potentially impact the traffic of the next user who binds to that
+    // socket.
+    @NonNull
+    public UdpEncapsulationSocket openUdpEncapsulationSocket(int port)
+            throws IOException, ResourceUnavailableException {
+        /*
+         * Most range checking is done in the service, but this version of the constructor expects
+         * a valid port number, and zero cannot be checked after being passed to the service.
+         */
+        if (port == 0) {
+            throw new IllegalArgumentException("Specified port must be a valid port number!");
+        }
+        try {
+            return new UdpEncapsulationSocket(mService, port);
+        } catch (ServiceSpecificException e) {
+            throw rethrowCheckedExceptionFromServiceSpecificException(e);
+        }
+    }
+
+    /**
+     * Open a socket for UDP encapsulation.
+     *
+     * <p>See {@link UdpEncapsulationSocket} for the proper way to close the returned socket.
+     *
+     * <p>The local port of the returned socket can be obtained by calling {@link
+     * UdpEncapsulationSocket#getPort()}.
+     *
+     * @return a socket that is bound to a local port
+     * @throws IOException indicating that the socket could not be opened or bound
+     * @throws ResourceUnavailableException indicating that too many encapsulation sockets are open
+     */
+    // Returning a socket in this fashion that has been created and bound by the system
+    // is the only safe way to ensure that a socket is both accessible to the user and
+    // safely usable for Encapsulation without allowing a user to possibly unbind from/close
+    // the port, which could potentially impact the traffic of the next user who binds to that
+    // socket.
+    @NonNull
+    public UdpEncapsulationSocket openUdpEncapsulationSocket()
+            throws IOException, ResourceUnavailableException {
+        try {
+            return new UdpEncapsulationSocket(mService, 0);
+        } catch (ServiceSpecificException e) {
+            throw rethrowCheckedExceptionFromServiceSpecificException(e);
+        }
+    }
+
+    /**
+     * This class represents an IpSecTunnelInterface
+     *
+     * <p>IpSecTunnelInterface objects track tunnel interfaces that serve as
+     * local endpoints for IPsec tunnels.
+     *
+     * <p>Creating an IpSecTunnelInterface creates a device to which IpSecTransforms may be
+     * applied to provide IPsec security to packets sent through the tunnel. While a tunnel
+     * cannot be used in standalone mode within Android, the higher layers may use the tunnel
+     * to create Network objects which are accessible to the Android system.
+     * @hide
+     */
+    @SystemApi
+    public static final class IpSecTunnelInterface implements AutoCloseable {
+        private final String mOpPackageName;
+        private final IIpSecService mService;
+        private final InetAddress mRemoteAddress;
+        private final InetAddress mLocalAddress;
+        private final Network mUnderlyingNetwork;
+        private final CloseGuard mCloseGuard = CloseGuard.get();
+        private String mInterfaceName;
+        private int mResourceId = INVALID_RESOURCE_ID;
+
+        /** Get the underlying SPI held by this object. */
+        @NonNull
+        public String getInterfaceName() {
+            return mInterfaceName;
+        }
+
+        /**
+         * Add an address to the IpSecTunnelInterface
+         *
+         * <p>Add an address which may be used as the local inner address for
+         * tunneled traffic.
+         *
+         * @param address the local address for traffic inside the tunnel
+         * @param prefixLen length of the InetAddress prefix
+         * @hide
+         */
+        @SystemApi
+        @RequiresFeature(PackageManager.FEATURE_IPSEC_TUNNELS)
+        @RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS)
+        public void addAddress(@NonNull InetAddress address, int prefixLen) throws IOException {
+            try {
+                mService.addAddressToTunnelInterface(
+                        mResourceId, new LinkAddress(address, prefixLen), mOpPackageName);
+            } catch (ServiceSpecificException e) {
+                throw rethrowCheckedExceptionFromServiceSpecificException(e);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+
+        /**
+         * Remove an address from the IpSecTunnelInterface
+         *
+         * <p>Remove an address which was previously added to the IpSecTunnelInterface
+         *
+         * @param address to be removed
+         * @param prefixLen length of the InetAddress prefix
+         * @hide
+         */
+        @SystemApi
+        @RequiresFeature(PackageManager.FEATURE_IPSEC_TUNNELS)
+        @RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS)
+        public void removeAddress(@NonNull InetAddress address, int prefixLen) throws IOException {
+            try {
+                mService.removeAddressFromTunnelInterface(
+                        mResourceId, new LinkAddress(address, prefixLen), mOpPackageName);
+            } catch (ServiceSpecificException e) {
+                throw rethrowCheckedExceptionFromServiceSpecificException(e);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+
+        private IpSecTunnelInterface(@NonNull Context ctx, @NonNull IIpSecService service,
+                @NonNull InetAddress localAddress, @NonNull InetAddress remoteAddress,
+                @NonNull Network underlyingNetwork)
+                throws ResourceUnavailableException, IOException {
+            mOpPackageName = ctx.getOpPackageName();
+            mService = service;
+            mLocalAddress = localAddress;
+            mRemoteAddress = remoteAddress;
+            mUnderlyingNetwork = underlyingNetwork;
+
+            try {
+                IpSecTunnelInterfaceResponse result =
+                        mService.createTunnelInterface(
+                                localAddress.getHostAddress(),
+                                remoteAddress.getHostAddress(),
+                                underlyingNetwork,
+                                new Binder(),
+                                mOpPackageName);
+                switch (result.status) {
+                    case Status.OK:
+                        break;
+                    case Status.RESOURCE_UNAVAILABLE:
+                        throw new ResourceUnavailableException(
+                                "No more tunnel interfaces may be allocated by this requester.");
+                    default:
+                        throw new RuntimeException(
+                                "Unknown status returned by IpSecService: " + result.status);
+                }
+                mResourceId = result.resourceId;
+                mInterfaceName = result.interfaceName;
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+            mCloseGuard.open("constructor");
+        }
+
+        /**
+         * Delete an IpSecTunnelInterface
+         *
+         * <p>Calling close will deallocate the IpSecTunnelInterface and all of its system
+         * resources. Any packets bound for this interface either inbound or outbound will
+         * all be lost.
+         */
+        @Override
+        public void close() {
+            try {
+                mService.deleteTunnelInterface(mResourceId, mOpPackageName);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            } catch (Exception e) {
+                // On close we swallow all random exceptions since failure to close is not
+                // actionable by the user.
+                Log.e(TAG, "Failed to close " + this + ", Exception=" + e);
+            } finally {
+                mResourceId = INVALID_RESOURCE_ID;
+                mCloseGuard.close();
+            }
+        }
+
+        /** Check that the Interface was closed properly. */
+        @Override
+        protected void finalize() throws Throwable {
+            if (mCloseGuard != null) {
+                mCloseGuard.warnIfOpen();
+            }
+            close();
+        }
+
+        /** @hide */
+        @VisibleForTesting
+        public int getResourceId() {
+            return mResourceId;
+        }
+
+        @Override
+        public String toString() {
+            return new StringBuilder()
+                .append("IpSecTunnelInterface{ifname=")
+                .append(mInterfaceName)
+                .append(",resourceId=")
+                .append(mResourceId)
+                .append("}")
+                .toString();
+        }
+    }
+
+    /**
+     * Create a new IpSecTunnelInterface as a local endpoint for tunneled IPsec traffic.
+     *
+     * <p>An application that creates tunnels is responsible for cleaning up the tunnel when the
+     * underlying network goes away, and the onLost() callback is received.
+     *
+     * @param localAddress The local addres of the tunnel
+     * @param remoteAddress The local addres of the tunnel
+     * @param underlyingNetwork the {@link Network} that will carry traffic for this tunnel.
+     *        This network should almost certainly be a network such as WiFi with an L2 address.
+     * @return a new {@link IpSecManager#IpSecTunnelInterface} with the specified properties
+     * @throws IOException indicating that the socket could not be opened or bound
+     * @throws ResourceUnavailableException indicating that too many encapsulation sockets are open
+     * @hide
+     */
+    @SystemApi
+    @NonNull
+    @RequiresFeature(PackageManager.FEATURE_IPSEC_TUNNELS)
+    @RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS)
+    public IpSecTunnelInterface createIpSecTunnelInterface(@NonNull InetAddress localAddress,
+            @NonNull InetAddress remoteAddress, @NonNull Network underlyingNetwork)
+            throws ResourceUnavailableException, IOException {
+        try {
+            return new IpSecTunnelInterface(
+                    mContext, mService, localAddress, remoteAddress, underlyingNetwork);
+        } catch (ServiceSpecificException e) {
+            throw rethrowCheckedExceptionFromServiceSpecificException(e);
+        }
+    }
+
+    /**
+     * Apply an active Tunnel Mode IPsec Transform to a {@link IpSecTunnelInterface}, which will
+     * tunnel all traffic for the given direction through the underlying network's interface with
+     * IPsec (applies an outer IP header and IPsec Header to all traffic, and expects an additional
+     * IP header and IPsec Header on all inbound traffic).
+     * <p>Applications should probably not use this API directly.
+     *
+     *
+     * @param tunnel The {@link IpSecManager#IpSecTunnelInterface} that will use the supplied
+     *        transform.
+     * @param direction the direction, {@link DIRECTION_OUT} or {@link #DIRECTION_IN} in which
+     *        the transform will be used.
+     * @param transform an {@link IpSecTransform} created in tunnel mode
+     * @throws IOException indicating that the transform could not be applied due to a lower
+     *         layer failure.
+     * @hide
+     */
+    @SystemApi
+    @RequiresFeature(PackageManager.FEATURE_IPSEC_TUNNELS)
+    @RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS)
+    public void applyTunnelModeTransform(@NonNull IpSecTunnelInterface tunnel,
+            @PolicyDirection int direction, @NonNull IpSecTransform transform) throws IOException {
+        try {
+            mService.applyTunnelModeTransform(
+                    tunnel.getResourceId(), direction,
+                    transform.getResourceId(), mContext.getOpPackageName());
+        } catch (ServiceSpecificException e) {
+            throw rethrowCheckedExceptionFromServiceSpecificException(e);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Construct an instance of IpSecManager within an application context.
+     *
+     * @param context the application context for this manager
+     * @hide
+     */
+    public IpSecManager(Context ctx, IIpSecService service) {
+        mContext = ctx;
+        mService = checkNotNull(service, "missing service");
+    }
+
+    private static void maybeHandleServiceSpecificException(ServiceSpecificException sse) {
+        // OsConstants are late binding, so switch statements can't be used.
+        if (sse.errorCode == OsConstants.EINVAL) {
+            throw new IllegalArgumentException(sse);
+        } else if (sse.errorCode == OsConstants.EAGAIN) {
+            throw new IllegalStateException(sse);
+        } else if (sse.errorCode == OsConstants.EOPNOTSUPP
+                || sse.errorCode == OsConstants.EPROTONOSUPPORT) {
+            throw new UnsupportedOperationException(sse);
+        }
+    }
+
+    /**
+     * Convert an Errno SSE to the correct Unchecked exception type.
+     *
+     * This method never actually returns.
+     */
+    // package
+    static RuntimeException
+            rethrowUncheckedExceptionFromServiceSpecificException(ServiceSpecificException sse) {
+        maybeHandleServiceSpecificException(sse);
+        throw new RuntimeException(sse);
+    }
+
+    /**
+     * Convert an Errno SSE to the correct Checked or Unchecked exception type.
+     *
+     * This method may throw IOException, or it may throw an unchecked exception; it will never
+     * actually return.
+     */
+    // package
+    static IOException rethrowCheckedExceptionFromServiceSpecificException(
+            ServiceSpecificException sse) throws IOException {
+        // First see if this is an unchecked exception of a type we know.
+        // If so, then we prefer the unchecked (specific) type of exception.
+        maybeHandleServiceSpecificException(sse);
+        // If not, then all we can do is provide the SSE in the form of an IOException.
+        throw new ErrnoException(
+                "IpSec encountered errno=" + sse.errorCode, sse.errorCode).rethrowAsIOException();
+    }
+}
diff --git a/android/net/IpSecSpiResponse.java b/android/net/IpSecSpiResponse.java
new file mode 100644
index 0000000..f99e570
--- /dev/null
+++ b/android/net/IpSecSpiResponse.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * This class is used to return an SPI and corresponding status from the IpSecService to an
+ * IpSecManager.SecurityParameterIndex.
+ *
+ * @hide
+ */
+public final class IpSecSpiResponse implements Parcelable {
+    private static final String TAG = "IpSecSpiResponse";
+
+    public final int resourceId;
+    public final int status;
+    public final int spi;
+    // Parcelable Methods
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeInt(status);
+        out.writeInt(resourceId);
+        out.writeInt(spi);
+    }
+
+    public IpSecSpiResponse(int inStatus, int inResourceId, int inSpi) {
+        status = inStatus;
+        resourceId = inResourceId;
+        spi = inSpi;
+    }
+
+    public IpSecSpiResponse(int inStatus) {
+        if (inStatus == IpSecManager.Status.OK) {
+            throw new IllegalArgumentException("Valid status implies other args must be provided");
+        }
+        status = inStatus;
+        resourceId = IpSecManager.INVALID_RESOURCE_ID;
+        spi = IpSecManager.INVALID_SECURITY_PARAMETER_INDEX;
+    }
+
+    private IpSecSpiResponse(Parcel in) {
+        status = in.readInt();
+        resourceId = in.readInt();
+        spi = in.readInt();
+    }
+
+    public static final @android.annotation.NonNull Parcelable.Creator<IpSecSpiResponse> CREATOR =
+            new Parcelable.Creator<IpSecSpiResponse>() {
+                public IpSecSpiResponse createFromParcel(Parcel in) {
+                    return new IpSecSpiResponse(in);
+                }
+
+                public IpSecSpiResponse[] newArray(int size) {
+                    return new IpSecSpiResponse[size];
+                }
+            };
+}
diff --git a/android/net/IpSecTransform.java b/android/net/IpSecTransform.java
new file mode 100644
index 0000000..36111f2
--- /dev/null
+++ b/android/net/IpSecTransform.java
@@ -0,0 +1,526 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net;
+
+import static android.net.IpSecManager.INVALID_RESOURCE_ID;
+
+import static com.android.internal.util.Preconditions.checkNotNull;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.RequiresFeature;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.ServiceSpecificException;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Preconditions;
+
+import dalvik.system.CloseGuard;
+
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.net.InetAddress;
+
+/**
+ * This class represents a transform, which roughly corresponds to an IPsec Security Association.
+ *
+ * <p>Transforms are created using {@link IpSecTransform.Builder}. Each {@code IpSecTransform}
+ * object encapsulates the properties and state of an IPsec security association. That includes,
+ * but is not limited to, algorithm choice, key material, and allocated system resources.
+ *
+ * @see <a href="https://tools.ietf.org/html/rfc4301">RFC 4301, Security Architecture for the
+ *     Internet Protocol</a>
+ */
+public final class IpSecTransform implements AutoCloseable {
+    private static final String TAG = "IpSecTransform";
+
+    /** @hide */
+    public static final int MODE_TRANSPORT = 0;
+
+    /** @hide */
+    public static final int MODE_TUNNEL = 1;
+
+    /** @hide */
+    public static final int ENCAP_NONE = 0;
+
+    /**
+     * IPsec traffic will be encapsulated within UDP, but with 8 zero-value bytes between the UDP
+     * header and payload. This prevents traffic from being interpreted as ESP or IKEv2.
+     *
+     * @hide
+     */
+    public static final int ENCAP_ESPINUDP_NON_IKE = 1;
+
+    /**
+     * IPsec traffic will be encapsulated within UDP as per
+     * <a href="https://tools.ietf.org/html/rfc3948">RFC 3498</a>.
+     *
+     * @hide
+     */
+    public static final int ENCAP_ESPINUDP = 2;
+
+    /** @hide */
+    @IntDef(value = {ENCAP_NONE, ENCAP_ESPINUDP, ENCAP_ESPINUDP_NON_IKE})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface EncapType {}
+
+    /** @hide */
+    @VisibleForTesting
+    public IpSecTransform(Context context, IpSecConfig config) {
+        mContext = context;
+        mConfig = new IpSecConfig(config);
+        mResourceId = INVALID_RESOURCE_ID;
+    }
+
+    private IIpSecService getIpSecService() {
+        IBinder b = ServiceManager.getService(android.content.Context.IPSEC_SERVICE);
+        if (b == null) {
+            throw new RemoteException("Failed to connect to IpSecService")
+                    .rethrowAsRuntimeException();
+        }
+
+        return IIpSecService.Stub.asInterface(b);
+    }
+
+    /**
+     * Checks the result status and throws an appropriate exception if the status is not Status.OK.
+     */
+    private void checkResultStatus(int status)
+            throws IOException, IpSecManager.ResourceUnavailableException,
+                    IpSecManager.SpiUnavailableException {
+        switch (status) {
+            case IpSecManager.Status.OK:
+                return;
+                // TODO: Pass Error string back from bundle so that errors can be more specific
+            case IpSecManager.Status.RESOURCE_UNAVAILABLE:
+                throw new IpSecManager.ResourceUnavailableException(
+                        "Failed to allocate a new IpSecTransform");
+            case IpSecManager.Status.SPI_UNAVAILABLE:
+                Log.wtf(TAG, "Attempting to use an SPI that was somehow not reserved");
+                // Fall through
+            default:
+                throw new IllegalStateException(
+                        "Failed to Create a Transform with status code " + status);
+        }
+    }
+
+    private IpSecTransform activate()
+            throws IOException, IpSecManager.ResourceUnavailableException,
+                    IpSecManager.SpiUnavailableException {
+        synchronized (this) {
+            try {
+                IIpSecService svc = getIpSecService();
+                IpSecTransformResponse result = svc.createTransform(
+                        mConfig, new Binder(), mContext.getOpPackageName());
+                int status = result.status;
+                checkResultStatus(status);
+                mResourceId = result.resourceId;
+                Log.d(TAG, "Added Transform with Id " + mResourceId);
+                mCloseGuard.open("build");
+            } catch (ServiceSpecificException e) {
+                throw IpSecManager.rethrowUncheckedExceptionFromServiceSpecificException(e);
+            } catch (RemoteException e) {
+                throw e.rethrowAsRuntimeException();
+            }
+        }
+
+        return this;
+    }
+
+    /**
+     * Equals method used for testing
+     *
+     * @hide
+     */
+    @VisibleForTesting
+    public static boolean equals(IpSecTransform lhs, IpSecTransform rhs) {
+        if (lhs == null || rhs == null) return (lhs == rhs);
+        return IpSecConfig.equals(lhs.getConfig(), rhs.getConfig())
+                && lhs.mResourceId == rhs.mResourceId;
+    }
+
+    /**
+     * Deactivate this {@code IpSecTransform} and free allocated resources.
+     *
+     * <p>Deactivating a transform while it is still applied to a socket will result in errors on
+     * that socket. Make sure to remove transforms by calling {@link
+     * IpSecManager#removeTransportModeTransforms}. Note, removing an {@code IpSecTransform} from a
+     * socket will not deactivate it (because one transform may be applied to multiple sockets).
+     *
+     * <p>It is safe to call this method on a transform that has already been deactivated.
+     */
+    public void close() {
+        Log.d(TAG, "Removing Transform with Id " + mResourceId);
+
+        // Always safe to attempt cleanup
+        if (mResourceId == INVALID_RESOURCE_ID) {
+            mCloseGuard.close();
+            return;
+        }
+        try {
+            IIpSecService svc = getIpSecService();
+            svc.deleteTransform(mResourceId);
+            stopNattKeepalive();
+        } catch (RemoteException e) {
+            throw e.rethrowAsRuntimeException();
+        } catch (Exception e) {
+            // On close we swallow all random exceptions since failure to close is not
+            // actionable by the user.
+            Log.e(TAG, "Failed to close " + this + ", Exception=" + e);
+        } finally {
+            mResourceId = INVALID_RESOURCE_ID;
+            mCloseGuard.close();
+        }
+    }
+
+    /** Check that the transform was closed properly. */
+    @Override
+    protected void finalize() throws Throwable {
+        if (mCloseGuard != null) {
+            mCloseGuard.warnIfOpen();
+        }
+        close();
+    }
+
+    /* Package */
+    IpSecConfig getConfig() {
+        return mConfig;
+    }
+
+    private final IpSecConfig mConfig;
+    private int mResourceId;
+    private final Context mContext;
+    private final CloseGuard mCloseGuard = CloseGuard.get();
+    private ConnectivityManager.PacketKeepalive mKeepalive;
+    private Handler mCallbackHandler;
+    private final ConnectivityManager.PacketKeepaliveCallback mKeepaliveCallback =
+            new ConnectivityManager.PacketKeepaliveCallback() {
+
+                @Override
+                public void onStarted() {
+                    synchronized (this) {
+                        mCallbackHandler.post(() -> mUserKeepaliveCallback.onStarted());
+                    }
+                }
+
+                @Override
+                public void onStopped() {
+                    synchronized (this) {
+                        mKeepalive = null;
+                        mCallbackHandler.post(() -> mUserKeepaliveCallback.onStopped());
+                    }
+                }
+
+                @Override
+                public void onError(int error) {
+                    synchronized (this) {
+                        mKeepalive = null;
+                        mCallbackHandler.post(() -> mUserKeepaliveCallback.onError(error));
+                    }
+                }
+            };
+
+    private NattKeepaliveCallback mUserKeepaliveCallback;
+
+    /** @hide */
+    @VisibleForTesting
+    public int getResourceId() {
+        return mResourceId;
+    }
+
+    /**
+     * A callback class to provide status information regarding a NAT-T keepalive session
+     *
+     * <p>Use this callback to receive status information regarding a NAT-T keepalive session
+     * by registering it when calling {@link #startNattKeepalive}.
+     *
+     * @hide
+     */
+    public static class NattKeepaliveCallback {
+        /** The specified {@code Network} is not connected. */
+        public static final int ERROR_INVALID_NETWORK = 1;
+        /** The hardware does not support this request. */
+        public static final int ERROR_HARDWARE_UNSUPPORTED = 2;
+        /** The hardware returned an error. */
+        public static final int ERROR_HARDWARE_ERROR = 3;
+
+        /** The requested keepalive was successfully started. */
+        public void onStarted() {}
+        /** The keepalive was successfully stopped. */
+        public void onStopped() {}
+        /** An error occurred. */
+        public void onError(int error) {}
+    }
+
+    /**
+     * Start a NAT-T keepalive session for the current transform.
+     *
+     * For a transform that is using UDP encapsulated IPv4, NAT-T offloading provides
+     * a power efficient mechanism of sending NAT-T packets at a specified interval.
+     *
+     * @param userCallback a {@link #NattKeepaliveCallback} to receive asynchronous status
+     *      information about the requested NAT-T keepalive session.
+     * @param intervalSeconds the interval between NAT-T keepalives being sent. The
+     *      the allowed range is between 20 and 3600 seconds.
+     * @param handler a handler on which to post callbacks when received.
+     *
+     * @hide
+     */
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.MANAGE_IPSEC_TUNNELS,
+            android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD
+    })
+    public void startNattKeepalive(@NonNull NattKeepaliveCallback userCallback,
+            int intervalSeconds, @NonNull Handler handler) throws IOException {
+        checkNotNull(userCallback);
+        if (intervalSeconds < 20 || intervalSeconds > 3600) {
+            throw new IllegalArgumentException("Invalid NAT-T keepalive interval");
+        }
+        checkNotNull(handler);
+        if (mResourceId == INVALID_RESOURCE_ID) {
+            throw new IllegalStateException(
+                    "Packet keepalive cannot be started for an inactive transform");
+        }
+
+        synchronized (mKeepaliveCallback) {
+            if (mKeepaliveCallback != null) {
+                throw new IllegalStateException("Keepalive already active");
+            }
+
+            mUserKeepaliveCallback = userCallback;
+            ConnectivityManager cm = (ConnectivityManager) mContext.getSystemService(
+                    Context.CONNECTIVITY_SERVICE);
+            mKeepalive = cm.startNattKeepalive(
+                    mConfig.getNetwork(), intervalSeconds, mKeepaliveCallback,
+                    NetworkUtils.numericToInetAddress(mConfig.getSourceAddress()),
+                    4500, // FIXME urgently, we need to get the port number from the Encap socket
+                    NetworkUtils.numericToInetAddress(mConfig.getDestinationAddress()));
+            mCallbackHandler = handler;
+        }
+    }
+
+    /**
+     * Stop an ongoing NAT-T keepalive session.
+     *
+     * Calling this API will request that an ongoing NAT-T keepalive session be terminated.
+     * If this API is not called when a Transform is closed, the underlying NAT-T session will
+     * be terminated automatically.
+     *
+     * @hide
+     */
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.MANAGE_IPSEC_TUNNELS,
+            android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD
+    })
+    public void stopNattKeepalive() {
+        synchronized (mKeepaliveCallback) {
+            if (mKeepalive == null) {
+                Log.e(TAG, "No active keepalive to stop");
+                return;
+            }
+            mKeepalive.stop();
+        }
+    }
+
+    /** This class is used to build {@link IpSecTransform} objects. */
+    public static class Builder {
+        private Context mContext;
+        private IpSecConfig mConfig;
+
+        /**
+         * Set the encryption algorithm.
+         *
+         * <p>Encryption is mutually exclusive with authenticated encryption.
+         *
+         * @param algo {@link IpSecAlgorithm} specifying the encryption to be applied.
+         */
+        @NonNull
+        public IpSecTransform.Builder setEncryption(@NonNull IpSecAlgorithm algo) {
+            // TODO: throw IllegalArgumentException if algo is not an encryption algorithm.
+            Preconditions.checkNotNull(algo);
+            mConfig.setEncryption(algo);
+            return this;
+        }
+
+        /**
+         * Set the authentication (integrity) algorithm.
+         *
+         * <p>Authentication is mutually exclusive with authenticated encryption.
+         *
+         * @param algo {@link IpSecAlgorithm} specifying the authentication to be applied.
+         */
+        @NonNull
+        public IpSecTransform.Builder setAuthentication(@NonNull IpSecAlgorithm algo) {
+            // TODO: throw IllegalArgumentException if algo is not an authentication algorithm.
+            Preconditions.checkNotNull(algo);
+            mConfig.setAuthentication(algo);
+            return this;
+        }
+
+        /**
+         * Set the authenticated encryption algorithm.
+         *
+         * <p>The Authenticated Encryption (AE) class of algorithms are also known as
+         * Authenticated Encryption with Associated Data (AEAD) algorithms, or Combined mode
+         * algorithms (as referred to in
+         * <a href="https://tools.ietf.org/html/rfc4301">RFC 4301</a>).
+         *
+         * <p>Authenticated encryption is mutually exclusive with encryption and authentication.
+         *
+         * @param algo {@link IpSecAlgorithm} specifying the authenticated encryption algorithm to
+         *     be applied.
+         */
+        @NonNull
+        public IpSecTransform.Builder setAuthenticatedEncryption(@NonNull IpSecAlgorithm algo) {
+            Preconditions.checkNotNull(algo);
+            mConfig.setAuthenticatedEncryption(algo);
+            return this;
+        }
+
+        /**
+         * Add UDP encapsulation to an IPv4 transform.
+         *
+         * <p>This allows IPsec traffic to pass through a NAT.
+         *
+         * @see <a href="https://tools.ietf.org/html/rfc3948">RFC 3948, UDP Encapsulation of IPsec
+         *     ESP Packets</a>
+         * @see <a href="https://tools.ietf.org/html/rfc7296#section-2.23">RFC 7296 section 2.23,
+         *     NAT Traversal of IKEv2</a>
+         * @param localSocket a socket for sending and receiving encapsulated traffic
+         * @param remotePort the UDP port number of the remote host that will send and receive
+         *     encapsulated traffic. In the case of IKEv2, this should be port 4500.
+         */
+        @NonNull
+        public IpSecTransform.Builder setIpv4Encapsulation(
+                @NonNull IpSecManager.UdpEncapsulationSocket localSocket, int remotePort) {
+            Preconditions.checkNotNull(localSocket);
+            mConfig.setEncapType(ENCAP_ESPINUDP);
+            if (localSocket.getResourceId() == INVALID_RESOURCE_ID) {
+                throw new IllegalArgumentException("Invalid UdpEncapsulationSocket");
+            }
+            mConfig.setEncapSocketResourceId(localSocket.getResourceId());
+            mConfig.setEncapRemotePort(remotePort);
+            return this;
+        }
+
+        /**
+         * Build a transport mode {@link IpSecTransform}.
+         *
+         * <p>This builds and activates a transport mode transform. Note that an active transform
+         * will not affect any network traffic until it has been applied to one or more sockets.
+         *
+         * @see IpSecManager#applyTransportModeTransform
+         * @param sourceAddress the source {@code InetAddress} of traffic on sockets that will use
+         *     this transform; this address must belong to the Network used by all sockets that
+         *     utilize this transform; if provided, then only traffic originating from the
+         *     specified source address will be processed.
+         * @param spi a unique {@link IpSecManager.SecurityParameterIndex} to identify transformed
+         *     traffic
+         * @throws IllegalArgumentException indicating that a particular combination of transform
+         *     properties is invalid
+         * @throws IpSecManager.ResourceUnavailableException indicating that too many transforms
+         *     are active
+         * @throws IpSecManager.SpiUnavailableException indicating the rare case where an SPI
+         *     collides with an existing transform
+         * @throws IOException indicating other errors
+         */
+        @NonNull
+        public IpSecTransform buildTransportModeTransform(
+                @NonNull InetAddress sourceAddress,
+                @NonNull IpSecManager.SecurityParameterIndex spi)
+                throws IpSecManager.ResourceUnavailableException,
+                        IpSecManager.SpiUnavailableException, IOException {
+            Preconditions.checkNotNull(sourceAddress);
+            Preconditions.checkNotNull(spi);
+            if (spi.getResourceId() == INVALID_RESOURCE_ID) {
+                throw new IllegalArgumentException("Invalid SecurityParameterIndex");
+            }
+            mConfig.setMode(MODE_TRANSPORT);
+            mConfig.setSourceAddress(sourceAddress.getHostAddress());
+            mConfig.setSpiResourceId(spi.getResourceId());
+            // FIXME: modifying a builder after calling build can change the built transform.
+            return new IpSecTransform(mContext, mConfig).activate();
+        }
+
+        /**
+         * Build and return an {@link IpSecTransform} object as a Tunnel Mode Transform. Some
+         * parameters have interdependencies that are checked at build time.
+         *
+         * @param sourceAddress the {@link InetAddress} that provides the source address for this
+         *     IPsec tunnel. This is almost certainly an address belonging to the {@link Network}
+         *     that will originate the traffic, which is set as the {@link #setUnderlyingNetwork}.
+         * @param spi a unique {@link IpSecManager.SecurityParameterIndex} to identify transformed
+         *     traffic
+         * @throws IllegalArgumentException indicating that a particular combination of transform
+         *     properties is invalid.
+         * @throws IpSecManager.ResourceUnavailableException indicating that too many transforms
+         *     are active
+         * @throws IpSecManager.SpiUnavailableException indicating the rare case where an SPI
+         *     collides with an existing transform
+         * @throws IOException indicating other errors
+         * @hide
+         */
+        @SystemApi
+        @NonNull
+        @RequiresFeature(PackageManager.FEATURE_IPSEC_TUNNELS)
+        @RequiresPermission(android.Manifest.permission.MANAGE_IPSEC_TUNNELS)
+        public IpSecTransform buildTunnelModeTransform(
+                @NonNull InetAddress sourceAddress,
+                @NonNull IpSecManager.SecurityParameterIndex spi)
+                throws IpSecManager.ResourceUnavailableException,
+                        IpSecManager.SpiUnavailableException, IOException {
+            Preconditions.checkNotNull(sourceAddress);
+            Preconditions.checkNotNull(spi);
+            if (spi.getResourceId() == INVALID_RESOURCE_ID) {
+                throw new IllegalArgumentException("Invalid SecurityParameterIndex");
+            }
+            mConfig.setMode(MODE_TUNNEL);
+            mConfig.setSourceAddress(sourceAddress.getHostAddress());
+            mConfig.setSpiResourceId(spi.getResourceId());
+            return new IpSecTransform(mContext, mConfig).activate();
+        }
+
+        /**
+         * Create a new IpSecTransform.Builder.
+         *
+         * @param context current context
+         */
+        public Builder(@NonNull Context context) {
+            Preconditions.checkNotNull(context);
+            mContext = context;
+            mConfig = new IpSecConfig();
+        }
+    }
+
+    @Override
+    public String toString() {
+        return new StringBuilder()
+            .append("IpSecTransform{resourceId=")
+            .append(mResourceId)
+            .append("}")
+            .toString();
+    }
+}
diff --git a/android/net/IpSecTransformResponse.java b/android/net/IpSecTransformResponse.java
new file mode 100644
index 0000000..a384889
--- /dev/null
+++ b/android/net/IpSecTransformResponse.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * This class is used to return an IpSecTransform resource Id and and corresponding status from the
+ * IpSecService to an IpSecTransform object.
+ *
+ * @hide
+ */
+public final class IpSecTransformResponse implements Parcelable {
+    private static final String TAG = "IpSecTransformResponse";
+
+    public final int resourceId;
+    public final int status;
+    // Parcelable Methods
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeInt(status);
+        out.writeInt(resourceId);
+    }
+
+    public IpSecTransformResponse(int inStatus) {
+        if (inStatus == IpSecManager.Status.OK) {
+            throw new IllegalArgumentException("Valid status implies other args must be provided");
+        }
+        status = inStatus;
+        resourceId = IpSecManager.INVALID_RESOURCE_ID;
+    }
+
+    public IpSecTransformResponse(int inStatus, int inResourceId) {
+        status = inStatus;
+        resourceId = inResourceId;
+    }
+
+    private IpSecTransformResponse(Parcel in) {
+        status = in.readInt();
+        resourceId = in.readInt();
+    }
+
+    public static final @android.annotation.NonNull Parcelable.Creator<IpSecTransformResponse> CREATOR =
+            new Parcelable.Creator<IpSecTransformResponse>() {
+                public IpSecTransformResponse createFromParcel(Parcel in) {
+                    return new IpSecTransformResponse(in);
+                }
+
+                public IpSecTransformResponse[] newArray(int size) {
+                    return new IpSecTransformResponse[size];
+                }
+            };
+}
diff --git a/android/net/IpSecTunnelInterfaceResponse.java b/android/net/IpSecTunnelInterfaceResponse.java
new file mode 100644
index 0000000..e3411e0
--- /dev/null
+++ b/android/net/IpSecTunnelInterfaceResponse.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2018 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.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * This class is used to return an IpSecTunnelInterface resource Id and and corresponding status
+ * from the IpSecService to an IpSecTunnelInterface object.
+ *
+ * @hide
+ */
+public final class IpSecTunnelInterfaceResponse implements Parcelable {
+    private static final String TAG = "IpSecTunnelInterfaceResponse";
+
+    public final int resourceId;
+    public final String interfaceName;
+    public final int status;
+    // Parcelable Methods
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeInt(status);
+        out.writeInt(resourceId);
+        out.writeString(interfaceName);
+    }
+
+    public IpSecTunnelInterfaceResponse(int inStatus) {
+        if (inStatus == IpSecManager.Status.OK) {
+            throw new IllegalArgumentException("Valid status implies other args must be provided");
+        }
+        status = inStatus;
+        resourceId = IpSecManager.INVALID_RESOURCE_ID;
+        interfaceName = "";
+    }
+
+    public IpSecTunnelInterfaceResponse(int inStatus, int inResourceId, String inInterfaceName) {
+        status = inStatus;
+        resourceId = inResourceId;
+        interfaceName = inInterfaceName;
+    }
+
+    private IpSecTunnelInterfaceResponse(Parcel in) {
+        status = in.readInt();
+        resourceId = in.readInt();
+        interfaceName = in.readString();
+    }
+
+    public static final @android.annotation.NonNull Parcelable.Creator<IpSecTunnelInterfaceResponse> CREATOR =
+            new Parcelable.Creator<IpSecTunnelInterfaceResponse>() {
+                public IpSecTunnelInterfaceResponse createFromParcel(Parcel in) {
+                    return new IpSecTunnelInterfaceResponse(in);
+                }
+
+                public IpSecTunnelInterfaceResponse[] newArray(int size) {
+                    return new IpSecTunnelInterfaceResponse[size];
+                }
+            };
+}
diff --git a/android/net/IpSecUdpEncapResponse.java b/android/net/IpSecUdpEncapResponse.java
new file mode 100644
index 0000000..4e7ba9b
--- /dev/null
+++ b/android/net/IpSecUdpEncapResponse.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.net;
+
+import android.os.Parcel;
+import android.os.ParcelFileDescriptor;
+import android.os.Parcelable;
+import java.io.FileDescriptor;
+import java.io.IOException;
+
+/**
+ * This class is used to return a UDP Socket and corresponding status from the IpSecService to an
+ * IpSecManager.UdpEncapsulationSocket.
+ *
+ * @hide
+ */
+public final class IpSecUdpEncapResponse implements Parcelable {
+    private static final String TAG = "IpSecUdpEncapResponse";
+
+    public final int resourceId;
+    public final int port;
+    public final int status;
+    // There is a weird asymmetry with FileDescriptor: you can write a FileDescriptor
+    // but you read a ParcelFileDescriptor. To circumvent this, when we receive a FD
+    // from the user, we immediately create a ParcelFileDescriptor DUP, which we invalidate
+    // on writeParcel() by setting the flag to do close-on-write.
+    // TODO: tests to ensure this doesn't leak
+    public final ParcelFileDescriptor fileDescriptor;
+
+    // Parcelable Methods
+
+    @Override
+    public int describeContents() {
+        return (fileDescriptor != null) ? Parcelable.CONTENTS_FILE_DESCRIPTOR : 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeInt(status);
+        out.writeInt(resourceId);
+        out.writeInt(port);
+        out.writeParcelable(fileDescriptor, Parcelable.PARCELABLE_WRITE_RETURN_VALUE);
+    }
+
+    public IpSecUdpEncapResponse(int inStatus) {
+        if (inStatus == IpSecManager.Status.OK) {
+            throw new IllegalArgumentException("Valid status implies other args must be provided");
+        }
+        status = inStatus;
+        resourceId = IpSecManager.INVALID_RESOURCE_ID;
+        port = -1;
+        fileDescriptor = null; // yes I know it's redundant, but readability
+    }
+
+    public IpSecUdpEncapResponse(int inStatus, int inResourceId, int inPort, FileDescriptor inFd)
+            throws IOException {
+        if (inStatus == IpSecManager.Status.OK && inFd == null) {
+            throw new IllegalArgumentException("Valid status implies FD must be non-null");
+        }
+        status = inStatus;
+        resourceId = inResourceId;
+        port = inPort;
+        fileDescriptor = (status == IpSecManager.Status.OK) ? ParcelFileDescriptor.dup(inFd) : null;
+    }
+
+    private IpSecUdpEncapResponse(Parcel in) {
+        status = in.readInt();
+        resourceId = in.readInt();
+        port = in.readInt();
+        fileDescriptor = in.readParcelable(ParcelFileDescriptor.class.getClassLoader());
+    }
+
+    public static final @android.annotation.NonNull Parcelable.Creator<IpSecUdpEncapResponse> CREATOR =
+            new Parcelable.Creator<IpSecUdpEncapResponse>() {
+                public IpSecUdpEncapResponse createFromParcel(Parcel in) {
+                    return new IpSecUdpEncapResponse(in);
+                }
+
+                public IpSecUdpEncapResponse[] newArray(int size) {
+                    return new IpSecUdpEncapResponse[size];
+                }
+            };
+}
diff --git a/android/net/KeepalivePacketData.java b/android/net/KeepalivePacketData.java
new file mode 100644
index 0000000..9b8b732
--- /dev/null
+++ b/android/net/KeepalivePacketData.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2015 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 static android.net.SocketKeepalive.ERROR_INVALID_IP_ADDRESS;
+import static android.net.SocketKeepalive.ERROR_INVALID_PORT;
+
+import android.net.SocketKeepalive.InvalidPacketException;
+import android.net.util.IpUtils;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+
+import java.net.InetAddress;
+
+/**
+ * Represents the actual packets that are sent by the
+ * {@link android.net.SocketKeepalive} API.
+ *
+ * @hide
+ */
+public class KeepalivePacketData implements Parcelable {
+    private static final String TAG = "KeepalivePacketData";
+
+    /** Source IP address */
+    public final InetAddress srcAddress;
+
+    /** Destination IP address */
+    public final InetAddress dstAddress;
+
+    /** Source port */
+    public final int srcPort;
+
+    /** Destination port */
+    public final int dstPort;
+
+    /** Packet data. A raw byte string of packet data, not including the link-layer header. */
+    private final byte[] mPacket;
+
+    protected static final int IPV4_HEADER_LENGTH = 20;
+    protected static final int UDP_HEADER_LENGTH = 8;
+
+    // This should only be constructed via static factory methods, such as
+    // nattKeepalivePacket
+    protected KeepalivePacketData(InetAddress srcAddress, int srcPort,
+            InetAddress dstAddress, int dstPort, byte[] data) throws InvalidPacketException {
+        this.srcAddress = srcAddress;
+        this.dstAddress = dstAddress;
+        this.srcPort = srcPort;
+        this.dstPort = dstPort;
+        this.mPacket = data;
+
+        // Check we have two IP addresses of the same family.
+        if (srcAddress == null || dstAddress == null || !srcAddress.getClass().getName()
+                .equals(dstAddress.getClass().getName())) {
+            Log.e(TAG, "Invalid or mismatched InetAddresses in KeepalivePacketData");
+            throw new InvalidPacketException(ERROR_INVALID_IP_ADDRESS);
+        }
+
+        // Check the ports.
+        if (!IpUtils.isValidUdpOrTcpPort(srcPort) || !IpUtils.isValidUdpOrTcpPort(dstPort)) {
+            Log.e(TAG, "Invalid ports in KeepalivePacketData");
+            throw new InvalidPacketException(ERROR_INVALID_PORT);
+        }
+    }
+
+    public byte[] getPacket() {
+        return mPacket.clone();
+    }
+
+    /* Parcelable Implementation */
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Write to parcel */
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeString(srcAddress.getHostAddress());
+        out.writeString(dstAddress.getHostAddress());
+        out.writeInt(srcPort);
+        out.writeInt(dstPort);
+        out.writeByteArray(mPacket);
+    }
+
+    protected KeepalivePacketData(Parcel in) {
+        srcAddress = NetworkUtils.numericToInetAddress(in.readString());
+        dstAddress = NetworkUtils.numericToInetAddress(in.readString());
+        srcPort = in.readInt();
+        dstPort = in.readInt();
+        mPacket = in.createByteArray();
+    }
+
+    /** Parcelable Creator */
+    public static final @android.annotation.NonNull Parcelable.Creator<KeepalivePacketData> CREATOR =
+            new Parcelable.Creator<KeepalivePacketData>() {
+                public KeepalivePacketData createFromParcel(Parcel in) {
+                    return new KeepalivePacketData(in);
+                }
+
+                public KeepalivePacketData[] newArray(int size) {
+                    return new KeepalivePacketData[size];
+                }
+            };
+
+}
diff --git a/android/net/LinkAddress.java b/android/net/LinkAddress.java
new file mode 100644
index 0000000..93dd2e4
--- /dev/null
+++ b/android/net/LinkAddress.java
@@ -0,0 +1,402 @@
+/*
+ * Copyright (C) 2010 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 static android.system.OsConstants.IFA_F_DADFAILED;
+import static android.system.OsConstants.IFA_F_DEPRECATED;
+import static android.system.OsConstants.IFA_F_OPTIMISTIC;
+import static android.system.OsConstants.IFA_F_TENTATIVE;
+import static android.system.OsConstants.RT_SCOPE_HOST;
+import static android.system.OsConstants.RT_SCOPE_LINK;
+import static android.system.OsConstants.RT_SCOPE_SITE;
+import static android.system.OsConstants.RT_SCOPE_UNIVERSE;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.annotation.UnsupportedAppUsage;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Pair;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.InterfaceAddress;
+import java.net.UnknownHostException;
+
+/**
+ * Identifies an IP address on a network link.
+ *
+ * A {@code LinkAddress} consists of:
+ * <ul>
+ * <li>An IP address and prefix length (e.g., {@code 2001:db8::1/64} or {@code 192.0.2.1/24}).
+ * The address must be unicast, as multicast addresses cannot be assigned to interfaces.
+ * <li>Address flags: A bitmask of {@code OsConstants.IFA_F_*} values representing properties
+ * of the address (e.g., {@code android.system.OsConstants.IFA_F_OPTIMISTIC}).
+ * <li>Address scope: One of the {@code OsConstants.IFA_F_*} values; defines the scope in which
+ * the address is unique (e.g.,
+ * {@code android.system.OsConstants.RT_SCOPE_LINK} or
+ * {@code android.system.OsConstants.RT_SCOPE_UNIVERSE}).
+ * </ul>
+ */
+public class LinkAddress implements Parcelable {
+    /**
+     * IPv4 or IPv6 address.
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    private InetAddress address;
+
+    /**
+     * Prefix length.
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    private int prefixLength;
+
+    /**
+     * Address flags. A bitmask of IFA_F_* values.
+     */
+    private int flags;
+
+    /**
+     * Address scope. One of the RT_SCOPE_* constants.
+     */
+    private int scope;
+
+    /**
+     * Utility function to determines the scope of a unicast address. Per RFC 4291 section 2.5 and
+     * RFC 6724 section 3.2.
+     * @hide
+     */
+    private static int scopeForUnicastAddress(InetAddress addr) {
+        if (addr.isAnyLocalAddress()) {
+            return RT_SCOPE_HOST;
+        }
+
+        if (addr.isLoopbackAddress() || addr.isLinkLocalAddress()) {
+            return RT_SCOPE_LINK;
+        }
+
+        // isSiteLocalAddress() returns true for private IPv4 addresses, but RFC 6724 section 3.2
+        // says that they are assigned global scope.
+        if (!(addr instanceof Inet4Address) && addr.isSiteLocalAddress()) {
+            return RT_SCOPE_SITE;
+        }
+
+        return RT_SCOPE_UNIVERSE;
+    }
+
+    /**
+     * Utility function to check if |address| is a Unique Local IPv6 Unicast Address
+     * (a.k.a. "ULA"; RFC 4193).
+     *
+     * Per RFC 4193 section 8, fc00::/7 identifies these addresses.
+     */
+    private boolean isIpv6ULA() {
+        if (isIpv6()) {
+            byte[] bytes = address.getAddress();
+            return ((bytes[0] & (byte)0xfe) == (byte)0xfc);
+        }
+        return false;
+    }
+
+    /**
+     * @return true if the address is IPv6.
+     * @hide
+     */
+    @TestApi
+    @SystemApi
+    public boolean isIpv6() {
+        return address instanceof Inet6Address;
+    }
+
+    /**
+     * For backward compatibility.
+     * This was annotated with @UnsupportedAppUsage in P, so we can't remove the method completely
+     * just yet.
+     * @return true if the address is IPv6.
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+    public boolean isIPv6() {
+        return isIpv6();
+    }
+
+    /**
+     * @return true if the address is IPv4 or is a mapped IPv4 address.
+     * @hide
+     */
+    @TestApi
+    @SystemApi
+    public boolean isIpv4() {
+        return address instanceof Inet4Address;
+    }
+
+    /**
+     * Utility function for the constructors.
+     */
+    private void init(InetAddress address, int prefixLength, int flags, int scope) {
+        if (address == null ||
+                address.isMulticastAddress() ||
+                prefixLength < 0 ||
+                (address instanceof Inet4Address && prefixLength > 32) ||
+                (prefixLength > 128)) {
+            throw new IllegalArgumentException("Bad LinkAddress params " + address +
+                    "/" + prefixLength);
+        }
+        this.address = address;
+        this.prefixLength = prefixLength;
+        this.flags = flags;
+        this.scope = scope;
+    }
+
+    /**
+     * Constructs a new {@code LinkAddress} from an {@code InetAddress} and prefix length, with
+     * the specified flags and scope. Flags and scope are not checked for validity.
+     * @param address The IP address.
+     * @param prefixLength The prefix length. Must be &gt;= 0 and &lt;= (32 or 128) (IPv4 or IPv6).
+     * @param flags A bitmask of {@code IFA_F_*} values representing properties of the address.
+     * @param scope An integer defining the scope in which the address is unique (e.g.,
+     *              {@link OsConstants#RT_SCOPE_LINK} or {@link OsConstants#RT_SCOPE_SITE}).
+     * @hide
+     */
+    @SystemApi
+    @TestApi
+    public LinkAddress(@NonNull InetAddress address, @IntRange(from = 0, to = 128) int prefixLength,
+            int flags, int scope) {
+        init(address, prefixLength, flags, scope);
+    }
+
+    /**
+     * Constructs a new {@code LinkAddress} from an {@code InetAddress} and a prefix length.
+     * The flags are set to zero and the scope is determined from the address.
+     * @param address The IP address.
+     * @param prefixLength The prefix length. Must be &gt;= 0 and &lt;= (32 or 128) (IPv4 or IPv6).
+     * @hide
+     */
+    @SystemApi
+    @TestApi
+    public LinkAddress(@NonNull InetAddress address,
+            @IntRange(from = 0, to = 128) int prefixLength) {
+        this(address, prefixLength, 0, 0);
+        this.scope = scopeForUnicastAddress(address);
+    }
+
+    /**
+     * Constructs a new {@code LinkAddress} from an {@code InterfaceAddress}.
+     * The flags are set to zero and the scope is determined from the address.
+     * @param interfaceAddress The interface address.
+     * @hide
+     */
+    public LinkAddress(@NonNull InterfaceAddress interfaceAddress) {
+        this(interfaceAddress.getAddress(),
+             interfaceAddress.getNetworkPrefixLength());
+    }
+
+    /**
+     * Constructs a new {@code LinkAddress} from a string such as "192.0.2.5/24" or
+     * "2001:db8::1/64". The flags are set to zero and the scope is determined from the address.
+     * @param address The string to parse.
+     * @hide
+     */
+    @SystemApi
+    @TestApi
+    public LinkAddress(@NonNull String address) {
+        this(address, 0, 0);
+        this.scope = scopeForUnicastAddress(this.address);
+    }
+
+    /**
+     * Constructs a new {@code LinkAddress} from a string such as "192.0.2.5/24" or
+     * "2001:db8::1/64", with the specified flags and scope.
+     * @param address The string to parse.
+     * @param flags The address flags.
+     * @param scope The address scope.
+     * @hide
+     */
+    @SystemApi
+    @TestApi
+    public LinkAddress(@NonNull String address, int flags, int scope) {
+        // This may throw an IllegalArgumentException; catching it is the caller's responsibility.
+        // TODO: consider rejecting mapped IPv4 addresses such as "::ffff:192.0.2.5/24".
+        Pair<InetAddress, Integer> ipAndMask = NetworkUtils.parseIpAndMask(address);
+        init(ipAndMask.first, ipAndMask.second, flags, scope);
+    }
+
+    /**
+     * Returns a string representation of this address, such as "192.0.2.1/24" or "2001:db8::1/64".
+     * The string representation does not contain the flags and scope, just the address and prefix
+     * length.
+     */
+    @Override
+    public String toString() {
+        return address.getHostAddress() + "/" + prefixLength;
+    }
+
+    /**
+     * Compares this {@code LinkAddress} instance against {@code obj}. Two addresses are equal if
+     * their address, prefix length, flags and scope are equal. Thus, for example, two addresses
+     * that have the same address and prefix length are not equal if one of them is deprecated and
+     * the other is not.
+     *
+     * @param obj the object to be tested for equality.
+     * @return {@code true} if both objects are equal, {@code false} otherwise.
+     */
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof LinkAddress)) {
+            return false;
+        }
+        LinkAddress linkAddress = (LinkAddress) obj;
+        return this.address.equals(linkAddress.address) &&
+            this.prefixLength == linkAddress.prefixLength &&
+            this.flags == linkAddress.flags &&
+            this.scope == linkAddress.scope;
+    }
+
+    /**
+     * Returns a hashcode for this address.
+     */
+    @Override
+    public int hashCode() {
+        return address.hashCode() + 11 * prefixLength + 19 * flags + 43 * scope;
+    }
+
+    /**
+     * Determines whether this {@code LinkAddress} and the provided {@code LinkAddress}
+     * represent the same address. Two {@code LinkAddresses} represent the same address
+     * if they have the same IP address and prefix length, even if their properties are
+     * different.
+     *
+     * @param other the {@code LinkAddress} to compare to.
+     * @return {@code true} if both objects have the same address and prefix length, {@code false}
+     * otherwise.
+     * @hide
+     */
+    @TestApi
+    @SystemApi
+    public boolean isSameAddressAs(@Nullable LinkAddress other) {
+        if (other == null) {
+            return false;
+        }
+        return address.equals(other.address) && prefixLength == other.prefixLength;
+    }
+
+    /**
+     * Returns the {@link InetAddress} of this {@code LinkAddress}.
+     */
+    public InetAddress getAddress() {
+        return address;
+    }
+
+    /**
+     * Returns the prefix length of this {@code LinkAddress}.
+     */
+    @IntRange(from = 0, to = 128)
+    public int getPrefixLength() {
+        return prefixLength;
+    }
+
+    /**
+     * Returns the prefix length of this {@code LinkAddress}.
+     * TODO: Delete all callers and remove in favour of getPrefixLength().
+     * @hide
+     */
+    @UnsupportedAppUsage
+    @IntRange(from = 0, to = 128)
+    public int getNetworkPrefixLength() {
+        return getPrefixLength();
+    }
+
+    /**
+     * Returns the flags of this {@code LinkAddress}.
+     */
+    public int getFlags() {
+        return flags;
+    }
+
+    /**
+     * Returns the scope of this {@code LinkAddress}.
+     */
+    public int getScope() {
+        return scope;
+    }
+
+    /**
+     * Returns true if this {@code LinkAddress} is global scope and preferred.
+     * @hide
+     */
+    @TestApi
+    @SystemApi
+    public boolean isGlobalPreferred() {
+        /**
+         * Note that addresses flagged as IFA_F_OPTIMISTIC are
+         * simultaneously flagged as IFA_F_TENTATIVE (when the tentative
+         * state has cleared either DAD has succeeded or failed, and both
+         * flags are cleared regardless).
+         */
+        return (scope == RT_SCOPE_UNIVERSE
+                && !isIpv6ULA()
+                && (flags & (IFA_F_DADFAILED | IFA_F_DEPRECATED)) == 0L
+                && ((flags & IFA_F_TENTATIVE) == 0L || (flags & IFA_F_OPTIMISTIC) != 0L));
+    }
+
+    /**
+     * Implement the Parcelable interface.
+     */
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Implement the Parcelable interface.
+     */
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeByteArray(address.getAddress());
+        dest.writeInt(prefixLength);
+        dest.writeInt(this.flags);
+        dest.writeInt(scope);
+    }
+
+    /**
+     * Implement the Parcelable interface.
+     */
+    public static final @android.annotation.NonNull Creator<LinkAddress> CREATOR =
+        new Creator<LinkAddress>() {
+            public LinkAddress createFromParcel(Parcel in) {
+                InetAddress address = null;
+                try {
+                    address = InetAddress.getByAddress(in.createByteArray());
+                } catch (UnknownHostException e) {
+                    // Nothing we can do here. When we call the constructor, we'll throw an
+                    // IllegalArgumentException, because a LinkAddress can't have a null
+                    // InetAddress.
+                }
+                int prefixLength = in.readInt();
+                int flags = in.readInt();
+                int scope = in.readInt();
+                return new LinkAddress(address, prefixLength, flags, scope);
+            }
+
+            public LinkAddress[] newArray(int size) {
+                return new LinkAddress[size];
+            }
+        };
+}
diff --git a/android/net/LinkProperties.java b/android/net/LinkProperties.java
new file mode 100644
index 0000000..d3f48ac
--- /dev/null
+++ b/android/net/LinkProperties.java
@@ -0,0 +1,1699 @@
+/*
+ * Copyright (C) 2010 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.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.annotation.UnsupportedAppUsage;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Objects;
+import java.util.StringJoiner;
+
+/**
+ * Describes the properties of a network link.
+ *
+ * A link represents a connection to a network.
+ * It may have multiple addresses and multiple gateways,
+ * multiple dns servers but only one http proxy and one
+ * network interface.
+ *
+ * Note that this is just a holder of data.  Modifying it
+ * does not affect live networks.
+ *
+ */
+public final class LinkProperties implements Parcelable {
+    // The interface described by the network link.
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    private String mIfaceName;
+    private final ArrayList<LinkAddress> mLinkAddresses = new ArrayList<>();
+    private final ArrayList<InetAddress> mDnses = new ArrayList<>();
+    // PCSCF addresses are addresses of SIP proxies that only exist for the IMS core service.
+    private final ArrayList<InetAddress> mPcscfs = new ArrayList<InetAddress>();
+    private final ArrayList<InetAddress> mValidatedPrivateDnses = new ArrayList<>();
+    private boolean mUsePrivateDns;
+    private String mPrivateDnsServerName;
+    private String mDomains;
+    private ArrayList<RouteInfo> mRoutes = new ArrayList<>();
+    private ProxyInfo mHttpProxy;
+    private int mMtu;
+    // in the format "rmem_min,rmem_def,rmem_max,wmem_min,wmem_def,wmem_max"
+    private String mTcpBufferSizes;
+    private IpPrefix mNat64Prefix;
+
+    private static final int MIN_MTU    = 68;
+    private static final int MIN_MTU_V6 = 1280;
+    private static final int MAX_MTU    = 10000;
+
+    // Stores the properties of links that are "stacked" above this link.
+    // Indexed by interface name to allow modification and to prevent duplicates being added.
+    private Hashtable<String, LinkProperties> mStackedLinks = new Hashtable<>();
+
+    /**
+     * @hide
+     */
+    public static class CompareResult<T> {
+        public final List<T> removed = new ArrayList<>();
+        public final List<T> added = new ArrayList<>();
+
+        public CompareResult() {}
+
+        public CompareResult(Collection<T> oldItems, Collection<T> newItems) {
+            if (oldItems != null) {
+                removed.addAll(oldItems);
+            }
+            if (newItems != null) {
+                for (T newItem : newItems) {
+                    if (!removed.remove(newItem)) {
+                        added.add(newItem);
+                    }
+                }
+            }
+        }
+
+        @Override
+        public String toString() {
+            return "removed=[" + TextUtils.join(",", removed)
+                    + "] added=[" + TextUtils.join(",", added)
+                    + "]";
+        }
+    }
+
+    /**
+     * @hide
+     */
+    public enum ProvisioningChange {
+        @UnsupportedAppUsage
+        STILL_NOT_PROVISIONED,
+        @UnsupportedAppUsage
+        LOST_PROVISIONING,
+        @UnsupportedAppUsage
+        GAINED_PROVISIONING,
+        @UnsupportedAppUsage
+        STILL_PROVISIONED,
+    }
+
+    /**
+     * Compare the provisioning states of two LinkProperties instances.
+     *
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public static ProvisioningChange compareProvisioning(
+            LinkProperties before, LinkProperties after) {
+        if (before.isProvisioned() && after.isProvisioned()) {
+            // On dual-stack networks, DHCPv4 renewals can occasionally fail.
+            // When this happens, IPv6-reachable services continue to function
+            // normally but IPv4-only services (naturally) fail.
+            //
+            // When an application using an IPv4-only service reports a bad
+            // network condition to the framework, attempts to re-validate
+            // the network succeed (since we support IPv6-only networks) and
+            // nothing is changed.
+            //
+            // For users, this is confusing and unexpected behaviour, and is
+            // not necessarily easy to diagnose.  Therefore, we treat changing
+            // from a dual-stack network to an IPv6-only network equivalent to
+            // a total loss of provisioning.
+            //
+            // For one such example of this, see b/18867306.
+            //
+            // Additionally, losing IPv6 provisioning can result in TCP
+            // connections getting stuck until timeouts fire and other
+            // baffling failures. Therefore, loss of either IPv4 or IPv6 on a
+            // previously dual-stack network is deemed a lost of provisioning.
+            if ((before.isIpv4Provisioned() && !after.isIpv4Provisioned())
+                    || (before.isIpv6Provisioned() && !after.isIpv6Provisioned())) {
+                return ProvisioningChange.LOST_PROVISIONING;
+            }
+            return ProvisioningChange.STILL_PROVISIONED;
+        } else if (before.isProvisioned() && !after.isProvisioned()) {
+            return ProvisioningChange.LOST_PROVISIONING;
+        } else if (!before.isProvisioned() && after.isProvisioned()) {
+            return ProvisioningChange.GAINED_PROVISIONING;
+        } else {  // !before.isProvisioned() && !after.isProvisioned()
+            return ProvisioningChange.STILL_NOT_PROVISIONED;
+        }
+    }
+
+    /**
+     * Constructs a new {@code LinkProperties} with default values.
+     */
+    public LinkProperties() {
+    }
+
+    /**
+     * @hide
+     */
+    @SystemApi
+    @TestApi
+    public LinkProperties(@Nullable LinkProperties source) {
+        if (source != null) {
+            mIfaceName = source.mIfaceName;
+            mLinkAddresses.addAll(source.mLinkAddresses);
+            mDnses.addAll(source.mDnses);
+            mValidatedPrivateDnses.addAll(source.mValidatedPrivateDnses);
+            mUsePrivateDns = source.mUsePrivateDns;
+            mPrivateDnsServerName = source.mPrivateDnsServerName;
+            mPcscfs.addAll(source.mPcscfs);
+            mDomains = source.mDomains;
+            mRoutes.addAll(source.mRoutes);
+            mHttpProxy = (source.mHttpProxy == null) ? null : new ProxyInfo(source.mHttpProxy);
+            for (LinkProperties l: source.mStackedLinks.values()) {
+                addStackedLink(l);
+            }
+            setMtu(source.mMtu);
+            mTcpBufferSizes = source.mTcpBufferSizes;
+            mNat64Prefix = source.mNat64Prefix;
+        }
+    }
+
+    /**
+     * Sets the interface name for this link.  All {@link RouteInfo} already set for this
+     * will have their interface changed to match this new value.
+     *
+     * @param iface The name of the network interface used for this link.
+     */
+    public void setInterfaceName(@Nullable String iface) {
+        mIfaceName = iface;
+        ArrayList<RouteInfo> newRoutes = new ArrayList<>(mRoutes.size());
+        for (RouteInfo route : mRoutes) {
+            newRoutes.add(routeWithInterface(route));
+        }
+        mRoutes = newRoutes;
+    }
+
+    /**
+     * Gets the interface name for this link.  May be {@code null} if not set.
+     *
+     * @return The interface name set for this link or {@code null}.
+     */
+    public @Nullable String getInterfaceName() {
+        return mIfaceName;
+    }
+
+    /**
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public @NonNull List<String> getAllInterfaceNames() {
+        List<String> interfaceNames = new ArrayList<>(mStackedLinks.size() + 1);
+        if (mIfaceName != null) interfaceNames.add(mIfaceName);
+        for (LinkProperties stacked: mStackedLinks.values()) {
+            interfaceNames.addAll(stacked.getAllInterfaceNames());
+        }
+        return interfaceNames;
+    }
+
+    /**
+     * Returns all the addresses on this link.  We often think of a link having a single address,
+     * however, particularly with Ipv6 several addresses are typical.  Note that the
+     * {@code LinkProperties} actually contains {@link LinkAddress} objects which also include
+     * prefix lengths for each address.  This is a simplified utility alternative to
+     * {@link LinkProperties#getLinkAddresses}.
+     *
+     * @return An unmodifiable {@link List} of {@link InetAddress} for this link.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public @NonNull List<InetAddress> getAddresses() {
+        final List<InetAddress> addresses = new ArrayList<>();
+        for (LinkAddress linkAddress : mLinkAddresses) {
+            addresses.add(linkAddress.getAddress());
+        }
+        return Collections.unmodifiableList(addresses);
+    }
+
+    /**
+     * Returns all the addresses on this link and all the links stacked above it.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public @NonNull List<InetAddress> getAllAddresses() {
+        List<InetAddress> addresses = new ArrayList<>();
+        for (LinkAddress linkAddress : mLinkAddresses) {
+            addresses.add(linkAddress.getAddress());
+        }
+        for (LinkProperties stacked: mStackedLinks.values()) {
+            addresses.addAll(stacked.getAllAddresses());
+        }
+        return addresses;
+    }
+
+    private int findLinkAddressIndex(LinkAddress address) {
+        for (int i = 0; i < mLinkAddresses.size(); i++) {
+            if (mLinkAddresses.get(i).isSameAddressAs(address)) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * Adds a {@link LinkAddress} to this {@code LinkProperties} if a {@link LinkAddress} of the
+     * same address/prefix does not already exist.  If it does exist it is replaced.
+     * @param address The {@code LinkAddress} to add.
+     * @return true if {@code address} was added or updated, false otherwise.
+     * @hide
+     */
+    @SystemApi
+    @TestApi
+    public boolean addLinkAddress(@NonNull LinkAddress address) {
+        if (address == null) {
+            return false;
+        }
+        int i = findLinkAddressIndex(address);
+        if (i < 0) {
+            // Address was not present. Add it.
+            mLinkAddresses.add(address);
+            return true;
+        } else if (mLinkAddresses.get(i).equals(address)) {
+            // Address was present and has same properties. Do nothing.
+            return false;
+        } else {
+            // Address was present and has different properties. Update it.
+            mLinkAddresses.set(i, address);
+            return true;
+        }
+    }
+
+    /**
+     * Removes a {@link LinkAddress} from this {@code LinkProperties}.  Specifically, matches
+     * and {@link LinkAddress} with the same address and prefix.
+     *
+     * @param toRemove A {@link LinkAddress} specifying the address to remove.
+     * @return true if the address was removed, false if it did not exist.
+     * @hide
+     */
+    @SystemApi
+    @TestApi
+    public boolean removeLinkAddress(@NonNull LinkAddress toRemove) {
+        int i = findLinkAddressIndex(toRemove);
+        if (i >= 0) {
+            mLinkAddresses.remove(i);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Returns all the {@link LinkAddress} on this link.  Typically a link will have
+     * one IPv4 address and one or more IPv6 addresses.
+     *
+     * @return An unmodifiable {@link List} of {@link LinkAddress} for this link.
+     */
+    public @NonNull List<LinkAddress> getLinkAddresses() {
+        return Collections.unmodifiableList(mLinkAddresses);
+    }
+
+    /**
+     * Returns all the addresses on this link and all the links stacked above it.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public List<LinkAddress> getAllLinkAddresses() {
+        List<LinkAddress> addresses = new ArrayList<>(mLinkAddresses);
+        for (LinkProperties stacked: mStackedLinks.values()) {
+            addresses.addAll(stacked.getAllLinkAddresses());
+        }
+        return addresses;
+    }
+
+    /**
+     * Replaces the {@link LinkAddress} in this {@code LinkProperties} with
+     * the given {@link Collection} of {@link LinkAddress}.
+     *
+     * @param addresses The {@link Collection} of {@link LinkAddress} to set in this
+     *                  object.
+     */
+    public void setLinkAddresses(@NonNull Collection<LinkAddress> addresses) {
+        mLinkAddresses.clear();
+        for (LinkAddress address: addresses) {
+            addLinkAddress(address);
+        }
+    }
+
+    /**
+     * Adds the given {@link InetAddress} to the list of DNS servers, if not present.
+     *
+     * @param dnsServer The {@link InetAddress} to add to the list of DNS servers.
+     * @return true if the DNS server was added, false if it was already present.
+     * @hide
+     */
+    @TestApi
+    @SystemApi
+    public boolean addDnsServer(@NonNull InetAddress dnsServer) {
+        if (dnsServer != null && !mDnses.contains(dnsServer)) {
+            mDnses.add(dnsServer);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Removes the given {@link InetAddress} from the list of DNS servers.
+     *
+     * @param dnsServer The {@link InetAddress} to remove from the list of DNS servers.
+     * @return true if the DNS server was removed, false if it did not exist.
+     * @hide
+     */
+    @TestApi
+    @SystemApi
+    public boolean removeDnsServer(@NonNull InetAddress dnsServer) {
+        return mDnses.remove(dnsServer);
+    }
+
+    /**
+     * Replaces the DNS servers in this {@code LinkProperties} with
+     * the given {@link Collection} of {@link InetAddress} objects.
+     *
+     * @param dnsServers The {@link Collection} of DNS servers to set in this object.
+     */
+    public void setDnsServers(@NonNull Collection<InetAddress> dnsServers) {
+        mDnses.clear();
+        for (InetAddress dnsServer: dnsServers) {
+            addDnsServer(dnsServer);
+        }
+    }
+
+    /**
+     * Returns all the {@link InetAddress} for DNS servers on this link.
+     *
+     * @return An unmodifiable {@link List} of {@link InetAddress} for DNS servers on
+     *         this link.
+     */
+    public @NonNull List<InetAddress> getDnsServers() {
+        return Collections.unmodifiableList(mDnses);
+    }
+
+    /**
+     * Set whether private DNS is currently in use on this network.
+     *
+     * @param usePrivateDns The private DNS state.
+     * @hide
+     */
+    @TestApi
+    @SystemApi
+    public void setUsePrivateDns(boolean usePrivateDns) {
+        mUsePrivateDns = usePrivateDns;
+    }
+
+    /**
+     * Returns whether private DNS is currently in use on this network. When
+     * private DNS is in use, applications must not send unencrypted DNS
+     * queries as doing so could reveal private user information. Furthermore,
+     * if private DNS is in use and {@link #getPrivateDnsServerName} is not
+     * {@code null}, DNS queries must be sent to the specified DNS server.
+     *
+     * @return {@code true} if private DNS is in use, {@code false} otherwise.
+     */
+    public boolean isPrivateDnsActive() {
+        return mUsePrivateDns;
+    }
+
+    /**
+     * Set the name of the private DNS server to which private DNS queries
+     * should be sent when in strict mode. This value should be {@code null}
+     * when private DNS is off or in opportunistic mode.
+     *
+     * @param privateDnsServerName The private DNS server name.
+     * @hide
+     */
+    @TestApi
+    @SystemApi
+    public void setPrivateDnsServerName(@Nullable String privateDnsServerName) {
+        mPrivateDnsServerName = privateDnsServerName;
+    }
+
+    /**
+     * Returns the private DNS server name that is in use. If not {@code null},
+     * private DNS is in strict mode. In this mode, applications should ensure
+     * that all DNS queries are encrypted and sent to this hostname and that
+     * queries are only sent if the hostname's certificate is valid. If
+     * {@code null} and {@link #isPrivateDnsActive} is {@code true}, private
+     * DNS is in opportunistic mode, and applications should ensure that DNS
+     * queries are encrypted and sent to a DNS server returned by
+     * {@link #getDnsServers}. System DNS will handle each of these cases
+     * correctly, but applications implementing their own DNS lookups must make
+     * sure to follow these requirements.
+     *
+     * @return The private DNS server name.
+     */
+    public @Nullable String getPrivateDnsServerName() {
+        return mPrivateDnsServerName;
+    }
+
+    /**
+     * Adds the given {@link InetAddress} to the list of validated private DNS servers,
+     * if not present. This is distinct from the server name in that these are actually
+     * resolved addresses.
+     *
+     * @param dnsServer The {@link InetAddress} to add to the list of validated private DNS servers.
+     * @return true if the DNS server was added, false if it was already present.
+     * @hide
+     */
+    public boolean addValidatedPrivateDnsServer(@NonNull InetAddress dnsServer) {
+        if (dnsServer != null && !mValidatedPrivateDnses.contains(dnsServer)) {
+            mValidatedPrivateDnses.add(dnsServer);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Removes the given {@link InetAddress} from the list of validated private DNS servers.
+     *
+     * @param dnsServer The {@link InetAddress} to remove from the list of validated private DNS
+     *        servers.
+     * @return true if the DNS server was removed, false if it did not exist.
+     * @hide
+     */
+    public boolean removeValidatedPrivateDnsServer(@NonNull InetAddress dnsServer) {
+        return mValidatedPrivateDnses.remove(dnsServer);
+    }
+
+    /**
+     * Replaces the validated private DNS servers in this {@code LinkProperties} with
+     * the given {@link Collection} of {@link InetAddress} objects.
+     *
+     * @param dnsServers The {@link Collection} of validated private DNS servers to set in this
+     *        object.
+     * @hide
+     */
+    @TestApi
+    @SystemApi
+    public void setValidatedPrivateDnsServers(@NonNull Collection<InetAddress> dnsServers) {
+        mValidatedPrivateDnses.clear();
+        for (InetAddress dnsServer: dnsServers) {
+            addValidatedPrivateDnsServer(dnsServer);
+        }
+    }
+
+    /**
+     * Returns all the {@link InetAddress} for validated private DNS servers on this link.
+     * These are resolved from the private DNS server name.
+     *
+     * @return An unmodifiable {@link List} of {@link InetAddress} for validated private
+     *         DNS servers on this link.
+     * @hide
+     */
+    @TestApi
+    @SystemApi
+    public @NonNull List<InetAddress> getValidatedPrivateDnsServers() {
+        return Collections.unmodifiableList(mValidatedPrivateDnses);
+    }
+
+    /**
+     * Adds the given {@link InetAddress} to the list of PCSCF servers, if not present.
+     *
+     * @param pcscfServer The {@link InetAddress} to add to the list of PCSCF servers.
+     * @return true if the PCSCF server was added, false otherwise.
+     * @hide
+     */
+    public boolean addPcscfServer(@NonNull InetAddress pcscfServer) {
+        if (pcscfServer != null && !mPcscfs.contains(pcscfServer)) {
+            mPcscfs.add(pcscfServer);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Removes the given {@link InetAddress} from the list of PCSCF servers.
+     *
+     * @param pcscfServer The {@link InetAddress} to remove from the list of PCSCF servers.
+     * @return true if the PCSCF server was removed, false otherwise.
+     * @hide
+     */
+    public boolean removePcscfServer(@NonNull InetAddress pcscfServer) {
+        return mPcscfs.remove(pcscfServer);
+    }
+
+    /**
+     * Replaces the PCSCF servers in this {@code LinkProperties} with
+     * the given {@link Collection} of {@link InetAddress} objects.
+     *
+     * @param pcscfServers The {@link Collection} of PCSCF servers to set in this object.
+     * @hide
+     */
+    @SystemApi
+    @TestApi
+    public void setPcscfServers(@NonNull Collection<InetAddress> pcscfServers) {
+        mPcscfs.clear();
+        for (InetAddress pcscfServer: pcscfServers) {
+            addPcscfServer(pcscfServer);
+        }
+    }
+
+    /**
+     * Returns all the {@link InetAddress} for PCSCF servers on this link.
+     *
+     * @return An unmodifiable {@link List} of {@link InetAddress} for PCSCF servers on
+     *         this link.
+     * @hide
+     */
+    @SystemApi
+    @TestApi
+    public @NonNull List<InetAddress> getPcscfServers() {
+        return Collections.unmodifiableList(mPcscfs);
+    }
+
+    /**
+     * Sets the DNS domain search path used on this link.
+     *
+     * @param domains A {@link String} listing in priority order the comma separated
+     *                domains to search when resolving host names on this link.
+     */
+    public void setDomains(@Nullable String domains) {
+        mDomains = domains;
+    }
+
+    /**
+     * Get the DNS domains search path set for this link. May be {@code null} if not set.
+     *
+     * @return A {@link String} containing the comma separated domains to search when resolving host
+     *         names on this link or {@code null}.
+     */
+    public @Nullable String getDomains() {
+        return mDomains;
+    }
+
+    /**
+     * Sets the Maximum Transmission Unit size to use on this link.  This should not be used
+     * unless the system default (1500) is incorrect.  Values less than 68 or greater than
+     * 10000 will be ignored.
+     *
+     * @param mtu The MTU to use for this link.
+     */
+    public void setMtu(int mtu) {
+        mMtu = mtu;
+    }
+
+    /**
+     * Gets any non-default MTU size set for this link.  Note that if the default is being used
+     * this will return 0.
+     *
+     * @return The mtu value set for this link.
+     */
+    public int getMtu() {
+        return mMtu;
+    }
+
+    /**
+     * Sets the tcp buffers sizes to be used when this link is the system default.
+     * Should be of the form "rmem_min,rmem_def,rmem_max,wmem_min,wmem_def,wmem_max".
+     *
+     * @param tcpBufferSizes The tcp buffers sizes to use.
+     *
+     * @hide
+     */
+    @TestApi
+    @SystemApi
+    public void setTcpBufferSizes(@Nullable String tcpBufferSizes) {
+        mTcpBufferSizes = tcpBufferSizes;
+    }
+
+    /**
+     * Gets the tcp buffer sizes. May be {@code null} if not set.
+     *
+     * @return the tcp buffer sizes to use when this link is the system default or {@code null}.
+     *
+     * @hide
+     */
+    @TestApi
+    @SystemApi
+    public @Nullable String getTcpBufferSizes() {
+        return mTcpBufferSizes;
+    }
+
+    private RouteInfo routeWithInterface(RouteInfo route) {
+        return new RouteInfo(
+            route.getDestination(),
+            route.getGateway(),
+            mIfaceName,
+            route.getType());
+    }
+
+    /**
+     * Adds a {@link RouteInfo} to this {@code LinkProperties}, if not present. If the
+     * {@link RouteInfo} had an interface name set and that differs from the interface set for this
+     * {@code LinkProperties} an {@link IllegalArgumentException} will be thrown.  The proper
+     * course is to add either un-named or properly named {@link RouteInfo}.
+     *
+     * @param route A {@link RouteInfo} to add to this object.
+     * @return {@code false} if the route was already present, {@code true} if it was added.
+     */
+    public boolean addRoute(@NonNull RouteInfo route) {
+        String routeIface = route.getInterface();
+        if (routeIface != null && !routeIface.equals(mIfaceName)) {
+            throw new IllegalArgumentException(
+                    "Route added with non-matching interface: " + routeIface
+                            + " vs. " + mIfaceName);
+        }
+        route = routeWithInterface(route);
+        if (!mRoutes.contains(route)) {
+            mRoutes.add(route);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Removes a {@link RouteInfo} from this {@code LinkProperties}, if present. The route must
+     * specify an interface and the interface must match the interface of this
+     * {@code LinkProperties}, or it will not be removed.
+     *
+     * @return {@code true} if the route was removed, {@code false} if it was not present.
+     *
+     * @hide
+     */
+    @TestApi
+    @SystemApi
+    public boolean removeRoute(@NonNull RouteInfo route) {
+        return Objects.equals(mIfaceName, route.getInterface()) && mRoutes.remove(route);
+    }
+
+    /**
+     * Returns all the {@link RouteInfo} set on this link.
+     *
+     * @return An unmodifiable {@link List} of {@link RouteInfo} for this link.
+     */
+    public @NonNull List<RouteInfo> getRoutes() {
+        return Collections.unmodifiableList(mRoutes);
+    }
+
+    /**
+     * Make sure this LinkProperties instance contains routes that cover the local subnet
+     * of its link addresses. Add any route that is missing.
+     * @hide
+     */
+    public void ensureDirectlyConnectedRoutes() {
+        for (LinkAddress addr : mLinkAddresses) {
+            addRoute(new RouteInfo(addr, null, mIfaceName));
+        }
+    }
+
+    /**
+     * Returns all the routes on this link and all the links stacked above it.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public @NonNull List<RouteInfo> getAllRoutes() {
+        List<RouteInfo> routes = new ArrayList<>(mRoutes);
+        for (LinkProperties stacked: mStackedLinks.values()) {
+            routes.addAll(stacked.getAllRoutes());
+        }
+        return routes;
+    }
+
+    /**
+     * Sets the recommended {@link ProxyInfo} to use on this link, or {@code null} for none.
+     * Note that Http Proxies are only a hint - the system recommends their use, but it does
+     * not enforce it and applications may ignore them.
+     *
+     * @param proxy A {@link ProxyInfo} defining the HTTP Proxy to use on this link.
+     */
+    public void setHttpProxy(@Nullable ProxyInfo proxy) {
+        mHttpProxy = proxy;
+    }
+
+    /**
+     * Gets the recommended {@link ProxyInfo} (or {@code null}) set on this link.
+     *
+     * @return The {@link ProxyInfo} set on this link or {@code null}.
+     */
+    public @Nullable ProxyInfo getHttpProxy() {
+        return mHttpProxy;
+    }
+
+    /**
+     * Returns the NAT64 prefix in use on this link, if any.
+     *
+     * @return the NAT64 prefix or {@code null}.
+     * @hide
+     */
+    @SystemApi
+    @TestApi
+    public @Nullable IpPrefix getNat64Prefix() {
+        return mNat64Prefix;
+    }
+
+    /**
+     * Sets the NAT64 prefix in use on this link.
+     *
+     * Currently, only 96-bit prefixes (i.e., where the 32-bit IPv4 address is at the end of the
+     * 128-bit IPv6 address) are supported or {@code null} for no prefix.
+     *
+     * @param prefix the NAT64 prefix.
+     * @hide
+     */
+    @SystemApi
+    @TestApi
+    public void setNat64Prefix(@Nullable IpPrefix prefix) {
+        if (prefix != null && prefix.getPrefixLength() != 96) {
+            throw new IllegalArgumentException("Only 96-bit prefixes are supported: " + prefix);
+        }
+        mNat64Prefix = prefix;  // IpPrefix objects are immutable.
+    }
+
+    /**
+     * Adds a stacked link.
+     *
+     * If there is already a stacked link with the same interface name as link,
+     * that link is replaced with link. Otherwise, link is added to the list
+     * of stacked links.
+     *
+     * @param link The link to add.
+     * @return true if the link was stacked, false otherwise.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public boolean addStackedLink(@NonNull LinkProperties link) {
+        if (link.getInterfaceName() != null) {
+            mStackedLinks.put(link.getInterfaceName(), link);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Removes a stacked link.
+     *
+     * If there is a stacked link with the given interface name, it is
+     * removed. Otherwise, nothing changes.
+     *
+     * @param iface The interface name of the link to remove.
+     * @return true if the link was removed, false otherwise.
+     * @hide
+     */
+    public boolean removeStackedLink(@NonNull String iface) {
+        LinkProperties removed = mStackedLinks.remove(iface);
+        return removed != null;
+    }
+
+    /**
+     * Returns all the links stacked on top of this link.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public @NonNull List<LinkProperties> getStackedLinks() {
+        if (mStackedLinks.isEmpty()) {
+            return Collections.emptyList();
+        }
+        final List<LinkProperties> stacked = new ArrayList<>();
+        for (LinkProperties link : mStackedLinks.values()) {
+            stacked.add(new LinkProperties(link));
+        }
+        return Collections.unmodifiableList(stacked);
+    }
+
+    /**
+     * Clears this object to its initial state.
+     */
+    public void clear() {
+        mIfaceName = null;
+        mLinkAddresses.clear();
+        mDnses.clear();
+        mUsePrivateDns = false;
+        mPrivateDnsServerName = null;
+        mPcscfs.clear();
+        mDomains = null;
+        mRoutes.clear();
+        mHttpProxy = null;
+        mStackedLinks.clear();
+        mMtu = 0;
+        mTcpBufferSizes = null;
+        mNat64Prefix = null;
+    }
+
+    /**
+     * Implement the Parcelable interface
+     */
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public String toString() {
+        // Space as a separator, so no need for spaces at start/end of the individual fragments.
+        final StringJoiner resultJoiner = new StringJoiner(" ", "{", "}");
+
+        if (mIfaceName != null) {
+            resultJoiner.add("InterfaceName:");
+            resultJoiner.add(mIfaceName);
+        }
+
+        resultJoiner.add("LinkAddresses: [");
+        if (!mLinkAddresses.isEmpty()) {
+            resultJoiner.add(TextUtils.join(",", mLinkAddresses));
+        }
+        resultJoiner.add("]");
+
+        resultJoiner.add("DnsAddresses: [");
+        if (!mDnses.isEmpty()) {
+            resultJoiner.add(TextUtils.join(",", mDnses));
+        }
+        resultJoiner.add("]");
+
+        if (mUsePrivateDns) {
+            resultJoiner.add("UsePrivateDns: true");
+        }
+
+        if (mPrivateDnsServerName != null) {
+            resultJoiner.add("PrivateDnsServerName:");
+            resultJoiner.add(mPrivateDnsServerName);
+        }
+
+        if (!mPcscfs.isEmpty()) {
+            resultJoiner.add("PcscfAddresses: [");
+            resultJoiner.add(TextUtils.join(",", mPcscfs));
+            resultJoiner.add("]");
+        }
+
+        if (!mValidatedPrivateDnses.isEmpty()) {
+            final StringJoiner validatedPrivateDnsesJoiner =
+                    new StringJoiner(",", "ValidatedPrivateDnsAddresses: [", "]");
+            for (final InetAddress addr : mValidatedPrivateDnses) {
+                validatedPrivateDnsesJoiner.add(addr.getHostAddress());
+            }
+            resultJoiner.add(validatedPrivateDnsesJoiner.toString());
+        }
+
+        resultJoiner.add("Domains:");
+        resultJoiner.add(mDomains);
+
+        resultJoiner.add("MTU:");
+        resultJoiner.add(Integer.toString(mMtu));
+
+        if (mTcpBufferSizes != null) {
+            resultJoiner.add("TcpBufferSizes:");
+            resultJoiner.add(mTcpBufferSizes);
+        }
+
+        resultJoiner.add("Routes: [");
+        if (!mRoutes.isEmpty()) {
+            resultJoiner.add(TextUtils.join(",", mRoutes));
+        }
+        resultJoiner.add("]");
+
+        if (mHttpProxy != null) {
+            resultJoiner.add("HttpProxy:");
+            resultJoiner.add(mHttpProxy.toString());
+        }
+
+        if (mNat64Prefix != null) {
+            resultJoiner.add("Nat64Prefix:");
+            resultJoiner.add(mNat64Prefix.toString());
+        }
+
+        final Collection<LinkProperties> stackedLinksValues = mStackedLinks.values();
+        if (!stackedLinksValues.isEmpty()) {
+            final StringJoiner stackedLinksJoiner = new StringJoiner(",", "Stacked: [", "]");
+            for (final LinkProperties lp : stackedLinksValues) {
+                stackedLinksJoiner.add("[ " + lp + " ]");
+            }
+            resultJoiner.add(stackedLinksJoiner.toString());
+        }
+
+        return resultJoiner.toString();
+    }
+
+    /**
+     * Returns true if this link has an IPv4 address.
+     *
+     * @return {@code true} if there is an IPv4 address, {@code false} otherwise.
+     * @hide
+     */
+    @TestApi
+    @SystemApi
+    public boolean hasIpv4Address() {
+        for (LinkAddress address : mLinkAddresses) {
+            if (address.getAddress() instanceof Inet4Address) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * For backward compatibility.
+     * This was annotated with @UnsupportedAppUsage in P, so we can't remove the method completely
+     * just yet.
+     * @return {@code true} if there is an IPv4 address, {@code false} otherwise.
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+    public boolean hasIPv4Address() {
+        return hasIpv4Address();
+    }
+
+    /**
+     * Returns true if this link or any of its stacked interfaces has an IPv4 address.
+     *
+     * @return {@code true} if there is an IPv4 address, {@code false} otherwise.
+     */
+    private boolean hasIpv4AddressOnInterface(String iface) {
+        // mIfaceName can be null.
+        return (Objects.equals(iface, mIfaceName) && hasIpv4Address())
+                || (iface != null && mStackedLinks.containsKey(iface)
+                        && mStackedLinks.get(iface).hasIpv4Address());
+    }
+
+    /**
+     * Returns true if this link has a global preferred IPv6 address.
+     *
+     * @return {@code true} if there is a global preferred IPv6 address, {@code false} otherwise.
+     * @hide
+     */
+    @TestApi
+    @SystemApi
+    public boolean hasGlobalIpv6Address() {
+        for (LinkAddress address : mLinkAddresses) {
+          if (address.getAddress() instanceof Inet6Address && address.isGlobalPreferred()) {
+            return true;
+          }
+        }
+        return false;
+    }
+
+    /**
+     * For backward compatibility.
+     * This was annotated with @UnsupportedAppUsage in P, so we can't remove the method completely
+     * just yet.
+     * @return {@code true} if there is a global preferred IPv6 address, {@code false} otherwise.
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+    public boolean hasGlobalIPv6Address() {
+        return hasGlobalIpv6Address();
+    }
+
+    /**
+     * Returns true if this link has an IPv4 default route.
+     *
+     * @return {@code true} if there is an IPv4 default route, {@code false} otherwise.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public boolean hasIpv4DefaultRoute() {
+        for (RouteInfo r : mRoutes) {
+            if (r.isIPv4Default()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * For backward compatibility.
+     * This was annotated with @UnsupportedAppUsage in P, so we can't remove the method completely
+     * just yet.
+     * @return {@code true} if there is an IPv4 default route, {@code false} otherwise.
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+    public boolean hasIPv4DefaultRoute() {
+        return hasIpv4DefaultRoute();
+    }
+
+    /**
+     * Returns true if this link has an IPv6 default route.
+     *
+     * @return {@code true} if there is an IPv6 default route, {@code false} otherwise.
+     * @hide
+     */
+    @TestApi
+    @SystemApi
+    public boolean hasIpv6DefaultRoute() {
+        for (RouteInfo r : mRoutes) {
+            if (r.isIPv6Default()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * For backward compatibility.
+     * This was annotated with @UnsupportedAppUsage in P, so we can't remove the method completely
+     * just yet.
+     * @return {@code true} if there is an IPv6 default route, {@code false} otherwise.
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+    public boolean hasIPv6DefaultRoute() {
+        return hasIpv6DefaultRoute();
+    }
+
+    /**
+     * Returns true if this link has an IPv4 DNS server.
+     *
+     * @return {@code true} if there is an IPv4 DNS server, {@code false} otherwise.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public boolean hasIpv4DnsServer() {
+        for (InetAddress ia : mDnses) {
+            if (ia instanceof Inet4Address) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * For backward compatibility.
+     * This was annotated with @UnsupportedAppUsage in P, so we can't remove the method completely
+     * just yet.
+     * @return {@code true} if there is an IPv4 DNS server, {@code false} otherwise.
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+    public boolean hasIPv4DnsServer() {
+        return hasIpv4DnsServer();
+    }
+
+    /**
+     * Returns true if this link has an IPv6 DNS server.
+     *
+     * @return {@code true} if there is an IPv6 DNS server, {@code false} otherwise.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public boolean hasIpv6DnsServer() {
+        for (InetAddress ia : mDnses) {
+            if (ia instanceof Inet6Address) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * For backward compatibility.
+     * This was annotated with @UnsupportedAppUsage in P, so we can't remove the method completely
+     * just yet.
+     * @return {@code true} if there is an IPv6 DNS server, {@code false} otherwise.
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+    public boolean hasIPv6DnsServer() {
+        return hasIpv6DnsServer();
+    }
+
+    /**
+     * Returns true if this link has an IPv4 PCSCF server.
+     *
+     * @return {@code true} if there is an IPv4 PCSCF server, {@code false} otherwise.
+     * @hide
+     */
+    public boolean hasIpv4PcscfServer() {
+        for (InetAddress ia : mPcscfs) {
+          if (ia instanceof Inet4Address) {
+            return true;
+          }
+        }
+        return false;
+    }
+
+    /**
+     * Returns true if this link has an IPv6 PCSCF server.
+     *
+     * @return {@code true} if there is an IPv6 PCSCF server, {@code false} otherwise.
+     * @hide
+     */
+    public boolean hasIpv6PcscfServer() {
+        for (InetAddress ia : mPcscfs) {
+          if (ia instanceof Inet6Address) {
+            return true;
+          }
+        }
+        return false;
+    }
+
+    /**
+     * Returns true if this link is provisioned for global IPv4 connectivity.
+     * This requires an IP address, default route, and DNS server.
+     *
+     * @return {@code true} if the link is provisioned, {@code false} otherwise.
+     * @hide
+     */
+    @TestApi
+    @SystemApi
+    public boolean isIpv4Provisioned() {
+        return (hasIpv4Address()
+                && hasIpv4DefaultRoute()
+                && hasIpv4DnsServer());
+    }
+
+    /**
+     * Returns true if this link is provisioned for global IPv6 connectivity.
+     * This requires an IP address, default route, and DNS server.
+     *
+     * @return {@code true} if the link is provisioned, {@code false} otherwise.
+     * @hide
+     */
+    @TestApi
+    @SystemApi
+    public boolean isIpv6Provisioned() {
+        return (hasGlobalIpv6Address()
+                && hasIpv6DefaultRoute()
+                && hasIpv6DnsServer());
+    }
+
+    /**
+     * For backward compatibility.
+     * This was annotated with @UnsupportedAppUsage in P, so we can't remove the method completely
+     * just yet.
+     * @return {@code true} if the link is provisioned, {@code false} otherwise.
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+    public boolean isIPv6Provisioned() {
+        return isIpv6Provisioned();
+    }
+
+
+    /**
+     * Returns true if this link is provisioned for global connectivity,
+     * for at least one Internet Protocol family.
+     *
+     * @return {@code true} if the link is provisioned, {@code false} otherwise.
+     * @hide
+     */
+    @TestApi
+    @SystemApi
+    public boolean isProvisioned() {
+        return (isIpv4Provisioned() || isIpv6Provisioned());
+    }
+
+    /**
+     * Evaluate whether the {@link InetAddress} is considered reachable.
+     *
+     * @return {@code true} if the given {@link InetAddress} is considered reachable,
+     *         {@code false} otherwise.
+     * @hide
+     */
+    @TestApi
+    @SystemApi
+    public boolean isReachable(@NonNull InetAddress ip) {
+        final List<RouteInfo> allRoutes = getAllRoutes();
+        // If we don't have a route to this IP address, it's not reachable.
+        final RouteInfo bestRoute = RouteInfo.selectBestRoute(allRoutes, ip);
+        if (bestRoute == null) {
+            return false;
+        }
+
+        // TODO: better source address evaluation for destination addresses.
+
+        if (ip instanceof Inet4Address) {
+            // For IPv4, it suffices for now to simply have any address.
+            return hasIpv4AddressOnInterface(bestRoute.getInterface());
+        } else if (ip instanceof Inet6Address) {
+            if (ip.isLinkLocalAddress()) {
+                // For now, just make sure link-local destinations have
+                // scopedIds set, since transmits will generally fail otherwise.
+                // TODO: verify it matches the ifindex of one of the interfaces.
+                return (((Inet6Address)ip).getScopeId() != 0);
+            }  else {
+                // For non-link-local destinations check that either the best route
+                // is directly connected or that some global preferred address exists.
+                // TODO: reconsider all cases (disconnected ULA networks, ...).
+                return (!bestRoute.hasGateway() || hasGlobalIpv6Address());
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Compares this {@code LinkProperties} interface name against the target
+     *
+     * @param target LinkProperties to compare.
+     * @return {@code true} if both are identical, {@code false} otherwise.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public boolean isIdenticalInterfaceName(@NonNull LinkProperties target) {
+        return TextUtils.equals(getInterfaceName(), target.getInterfaceName());
+    }
+
+    /**
+     * Compares this {@code LinkProperties} interface addresses against the target
+     *
+     * @param target LinkProperties to compare.
+     * @return {@code true} if both are identical, {@code false} otherwise.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public boolean isIdenticalAddresses(@NonNull LinkProperties target) {
+        Collection<InetAddress> targetAddresses = target.getAddresses();
+        Collection<InetAddress> sourceAddresses = getAddresses();
+        return (sourceAddresses.size() == targetAddresses.size()) ?
+                    sourceAddresses.containsAll(targetAddresses) : false;
+    }
+
+    /**
+     * Compares this {@code LinkProperties} DNS addresses against the target
+     *
+     * @param target LinkProperties to compare.
+     * @return {@code true} if both are identical, {@code false} otherwise.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public boolean isIdenticalDnses(@NonNull LinkProperties target) {
+        Collection<InetAddress> targetDnses = target.getDnsServers();
+        String targetDomains = target.getDomains();
+        if (mDomains == null) {
+            if (targetDomains != null) return false;
+        } else {
+            if (!mDomains.equals(targetDomains)) return false;
+        }
+        return (mDnses.size() == targetDnses.size()) ?
+                mDnses.containsAll(targetDnses) : false;
+    }
+
+    /**
+     * Compares this {@code LinkProperties} private DNS settings against the
+     * target.
+     *
+     * @param target LinkProperties to compare.
+     * @return {@code true} if both are identical, {@code false} otherwise.
+     * @hide
+     */
+    public boolean isIdenticalPrivateDns(@NonNull LinkProperties target) {
+        return (isPrivateDnsActive() == target.isPrivateDnsActive()
+                && TextUtils.equals(getPrivateDnsServerName(),
+                target.getPrivateDnsServerName()));
+    }
+
+    /**
+     * Compares this {@code LinkProperties} validated private DNS addresses against
+     * the target
+     *
+     * @param target LinkProperties to compare.
+     * @return {@code true} if both are identical, {@code false} otherwise.
+     * @hide
+     */
+    public boolean isIdenticalValidatedPrivateDnses(@NonNull LinkProperties target) {
+        Collection<InetAddress> targetDnses = target.getValidatedPrivateDnsServers();
+        return (mValidatedPrivateDnses.size() == targetDnses.size())
+                ? mValidatedPrivateDnses.containsAll(targetDnses) : false;
+    }
+
+    /**
+     * Compares this {@code LinkProperties} PCSCF addresses against the target
+     *
+     * @param target LinkProperties to compare.
+     * @return {@code true} if both are identical, {@code false} otherwise.
+     * @hide
+     */
+    public boolean isIdenticalPcscfs(@NonNull LinkProperties target) {
+        Collection<InetAddress> targetPcscfs = target.getPcscfServers();
+        return (mPcscfs.size() == targetPcscfs.size()) ?
+                    mPcscfs.containsAll(targetPcscfs) : false;
+    }
+
+    /**
+     * Compares this {@code LinkProperties} Routes against the target
+     *
+     * @param target LinkProperties to compare.
+     * @return {@code true} if both are identical, {@code false} otherwise.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public boolean isIdenticalRoutes(@NonNull LinkProperties target) {
+        Collection<RouteInfo> targetRoutes = target.getRoutes();
+        return (mRoutes.size() == targetRoutes.size()) ?
+                mRoutes.containsAll(targetRoutes) : false;
+    }
+
+    /**
+     * Compares this {@code LinkProperties} HttpProxy against the target
+     *
+     * @param target LinkProperties to compare.
+     * @return {@code true} if both are identical, {@code false} otherwise.
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    public boolean isIdenticalHttpProxy(@NonNull LinkProperties target) {
+        return getHttpProxy() == null ? target.getHttpProxy() == null :
+                getHttpProxy().equals(target.getHttpProxy());
+    }
+
+    /**
+     * Compares this {@code LinkProperties} stacked links against the target
+     *
+     * @param target LinkProperties to compare.
+     * @return {@code true} if both are identical, {@code false} otherwise.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public boolean isIdenticalStackedLinks(@NonNull LinkProperties target) {
+        if (!mStackedLinks.keySet().equals(target.mStackedLinks.keySet())) {
+            return false;
+        }
+        for (LinkProperties stacked : mStackedLinks.values()) {
+            // Hashtable values can never be null.
+            String iface = stacked.getInterfaceName();
+            if (!stacked.equals(target.mStackedLinks.get(iface))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Compares this {@code LinkProperties} MTU against the target
+     *
+     * @param target LinkProperties to compare.
+     * @return {@code true} if both are identical, {@code false} otherwise.
+     * @hide
+     */
+    public boolean isIdenticalMtu(@NonNull LinkProperties target) {
+        return getMtu() == target.getMtu();
+    }
+
+    /**
+     * Compares this {@code LinkProperties} Tcp buffer sizes against the target.
+     *
+     * @param target LinkProperties to compare.
+     * @return {@code true} if both are identical, {@code false} otherwise.
+     * @hide
+     */
+    public boolean isIdenticalTcpBufferSizes(@NonNull LinkProperties target) {
+        return Objects.equals(mTcpBufferSizes, target.mTcpBufferSizes);
+    }
+
+    /**
+     * Compares this {@code LinkProperties} NAT64 prefix against the target.
+     *
+     * @param target LinkProperties to compare.
+     * @return {@code true} if both are identical, {@code false} otherwise.
+     * @hide
+     */
+    public boolean isIdenticalNat64Prefix(@NonNull LinkProperties target) {
+        return Objects.equals(mNat64Prefix, target.mNat64Prefix);
+    }
+
+    /**
+     * Compares this {@code LinkProperties} instance against the target
+     * LinkProperties in {@code obj}. Two LinkPropertieses are equal if
+     * all their fields are equal in values.
+     *
+     * For collection fields, such as mDnses, containsAll() is used to check
+     * if two collections contains the same elements, independent of order.
+     * There are two thoughts regarding containsAll()
+     * 1. Duplicated elements. eg, (A, B, B) and (A, A, B) are equal.
+     * 2. Worst case performance is O(n^2).
+     *
+     * @param obj the object to be tested for equality.
+     * @return {@code true} if both objects are equal, {@code false} otherwise.
+     */
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) return true;
+
+        if (!(obj instanceof LinkProperties)) return false;
+
+        LinkProperties target = (LinkProperties) obj;
+        /*
+         * This method does not check that stacked interfaces are equal, because
+         * stacked interfaces are not so much a property of the link as a
+         * description of connections between links.
+         */
+        return isIdenticalInterfaceName(target)
+                && isIdenticalAddresses(target)
+                && isIdenticalDnses(target)
+                && isIdenticalPrivateDns(target)
+                && isIdenticalValidatedPrivateDnses(target)
+                && isIdenticalPcscfs(target)
+                && isIdenticalRoutes(target)
+                && isIdenticalHttpProxy(target)
+                && isIdenticalStackedLinks(target)
+                && isIdenticalMtu(target)
+                && isIdenticalTcpBufferSizes(target)
+                && isIdenticalNat64Prefix(target);
+    }
+
+    /**
+     * Compares the addresses in this LinkProperties with another
+     * LinkProperties, examining only addresses on the base link.
+     *
+     * @param target a LinkProperties with the new list of addresses
+     * @return the differences between the addresses.
+     * @hide
+     */
+    public @NonNull CompareResult<LinkAddress> compareAddresses(@Nullable LinkProperties target) {
+        /*
+         * Duplicate the LinkAddresses into removed, we will be removing
+         * address which are common between mLinkAddresses and target
+         * leaving the addresses that are different. And address which
+         * are in target but not in mLinkAddresses are placed in the
+         * addedAddresses.
+         */
+        return new CompareResult<>(mLinkAddresses,
+                target != null ? target.getLinkAddresses() : null);
+    }
+
+    /**
+     * Compares the DNS addresses in this LinkProperties with another
+     * LinkProperties, examining only DNS addresses on the base link.
+     *
+     * @param target a LinkProperties with the new list of dns addresses
+     * @return the differences between the DNS addresses.
+     * @hide
+     */
+    public @NonNull CompareResult<InetAddress> compareDnses(@Nullable LinkProperties target) {
+        /*
+         * Duplicate the InetAddresses into removed, we will be removing
+         * dns address which are common between mDnses and target
+         * leaving the addresses that are different. And dns address which
+         * are in target but not in mDnses are placed in the
+         * addedAddresses.
+         */
+        return new CompareResult<>(mDnses, target != null ? target.getDnsServers() : null);
+    }
+
+    /**
+     * Compares the validated private DNS addresses in this LinkProperties with another
+     * LinkProperties.
+     *
+     * @param target a LinkProperties with the new list of validated private dns addresses
+     * @return the differences between the DNS addresses.
+     * @hide
+     */
+    public @NonNull CompareResult<InetAddress> compareValidatedPrivateDnses(
+            @Nullable LinkProperties target) {
+        return new CompareResult<>(mValidatedPrivateDnses,
+                target != null ? target.getValidatedPrivateDnsServers() : null);
+    }
+
+    /**
+     * Compares all routes in this LinkProperties with another LinkProperties,
+     * examining both the the base link and all stacked links.
+     *
+     * @param target a LinkProperties with the new list of routes
+     * @return the differences between the routes.
+     * @hide
+     */
+    public @NonNull CompareResult<RouteInfo> compareAllRoutes(@Nullable LinkProperties target) {
+        /*
+         * Duplicate the RouteInfos into removed, we will be removing
+         * routes which are common between mRoutes and target
+         * leaving the routes that are different. And route address which
+         * are in target but not in mRoutes are placed in added.
+         */
+        return new CompareResult<>(getAllRoutes(), target != null ? target.getAllRoutes() : null);
+    }
+
+    /**
+     * Compares all interface names in this LinkProperties with another
+     * LinkProperties, examining both the the base link and all stacked links.
+     *
+     * @param target a LinkProperties with the new list of interface names
+     * @return the differences between the interface names.
+     * @hide
+     */
+    public @NonNull CompareResult<String> compareAllInterfaceNames(
+            @Nullable LinkProperties target) {
+        /*
+         * Duplicate the interface names into removed, we will be removing
+         * interface names which are common between this and target
+         * leaving the interface names that are different. And interface names which
+         * are in target but not in this are placed in added.
+         */
+        return new CompareResult<>(getAllInterfaceNames(),
+                target != null ? target.getAllInterfaceNames() : null);
+    }
+
+
+    /**
+     * Generate hashcode based on significant fields
+     *
+     * Equal objects must produce the same hash code, while unequal objects
+     * may have the same hash codes.
+     */
+    @Override
+    public int hashCode() {
+        return ((null == mIfaceName) ? 0 : mIfaceName.hashCode()
+                + mLinkAddresses.size() * 31
+                + mDnses.size() * 37
+                + mValidatedPrivateDnses.size() * 61
+                + ((null == mDomains) ? 0 : mDomains.hashCode())
+                + mRoutes.size() * 41
+                + ((null == mHttpProxy) ? 0 : mHttpProxy.hashCode())
+                + mStackedLinks.hashCode() * 47)
+                + mMtu * 51
+                + ((null == mTcpBufferSizes) ? 0 : mTcpBufferSizes.hashCode())
+                + (mUsePrivateDns ? 57 : 0)
+                + mPcscfs.size() * 67
+                + ((null == mPrivateDnsServerName) ? 0 : mPrivateDnsServerName.hashCode())
+                + Objects.hash(mNat64Prefix);
+    }
+
+    /**
+     * Implement the Parcelable interface.
+     */
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeString(getInterfaceName());
+        dest.writeInt(mLinkAddresses.size());
+        for (LinkAddress linkAddress : mLinkAddresses) {
+            dest.writeParcelable(linkAddress, flags);
+        }
+
+        dest.writeInt(mDnses.size());
+        for (InetAddress d : mDnses) {
+            dest.writeByteArray(d.getAddress());
+        }
+        dest.writeInt(mValidatedPrivateDnses.size());
+        for (InetAddress d : mValidatedPrivateDnses) {
+            dest.writeByteArray(d.getAddress());
+        }
+        dest.writeBoolean(mUsePrivateDns);
+        dest.writeString(mPrivateDnsServerName);
+        dest.writeInt(mPcscfs.size());
+        for (InetAddress d : mPcscfs) {
+            dest.writeByteArray(d.getAddress());
+        }
+        dest.writeString(mDomains);
+        dest.writeInt(mMtu);
+        dest.writeString(mTcpBufferSizes);
+        dest.writeInt(mRoutes.size());
+        for (RouteInfo route : mRoutes) {
+            dest.writeParcelable(route, flags);
+        }
+
+        if (mHttpProxy != null) {
+            dest.writeByte((byte)1);
+            dest.writeParcelable(mHttpProxy, flags);
+        } else {
+            dest.writeByte((byte)0);
+        }
+        dest.writeParcelable(mNat64Prefix, 0);
+
+        ArrayList<LinkProperties> stackedLinks = new ArrayList<>(mStackedLinks.values());
+        dest.writeList(stackedLinks);
+    }
+
+    /**
+     * Implement the Parcelable interface.
+     */
+    public static final @android.annotation.NonNull Creator<LinkProperties> CREATOR =
+        new Creator<LinkProperties>() {
+            public LinkProperties createFromParcel(Parcel in) {
+                LinkProperties netProp = new LinkProperties();
+
+                String iface = in.readString();
+                if (iface != null) {
+                    netProp.setInterfaceName(iface);
+                }
+                int addressCount = in.readInt();
+                for (int i = 0; i < addressCount; i++) {
+                    netProp.addLinkAddress(in.readParcelable(null));
+                }
+                addressCount = in.readInt();
+                for (int i = 0; i < addressCount; i++) {
+                    try {
+                        netProp.addDnsServer(InetAddress.getByAddress(in.createByteArray()));
+                    } catch (UnknownHostException e) { }
+                }
+                addressCount = in.readInt();
+                for (int i = 0; i < addressCount; i++) {
+                    try {
+                        netProp.addValidatedPrivateDnsServer(
+                                InetAddress.getByAddress(in.createByteArray()));
+                    } catch (UnknownHostException e) { }
+                }
+                netProp.setUsePrivateDns(in.readBoolean());
+                netProp.setPrivateDnsServerName(in.readString());
+                addressCount = in.readInt();
+                for (int i = 0; i < addressCount; i++) {
+                    try {
+                        netProp.addPcscfServer(InetAddress.getByAddress(in.createByteArray()));
+                    } catch (UnknownHostException e) { }
+                }
+                netProp.setDomains(in.readString());
+                netProp.setMtu(in.readInt());
+                netProp.setTcpBufferSizes(in.readString());
+                addressCount = in.readInt();
+                for (int i = 0; i < addressCount; i++) {
+                    netProp.addRoute(in.readParcelable(null));
+                }
+                if (in.readByte() == 1) {
+                    netProp.setHttpProxy(in.readParcelable(null));
+                }
+                netProp.setNat64Prefix(in.readParcelable(null));
+                ArrayList<LinkProperties> stackedLinks = new ArrayList<LinkProperties>();
+                in.readList(stackedLinks, LinkProperties.class.getClassLoader());
+                for (LinkProperties stackedLink: stackedLinks) {
+                    netProp.addStackedLink(stackedLink);
+                }
+                return netProp;
+            }
+
+            public LinkProperties[] newArray(int size) {
+                return new LinkProperties[size];
+            }
+        };
+
+    /**
+     * Check the valid MTU range based on IPv4 or IPv6.
+     * @hide
+     */
+    public static boolean isValidMtu(int mtu, boolean ipv6) {
+        if (ipv6) {
+            return mtu >= MIN_MTU_V6 && mtu <= MAX_MTU;
+        } else {
+            return mtu >= MIN_MTU && mtu <= MAX_MTU;
+        }
+    }
+}
diff --git a/android/net/LinkQualityInfo.java b/android/net/LinkQualityInfo.java
new file mode 100644
index 0000000..3c5d474
--- /dev/null
+++ b/android/net/LinkQualityInfo.java
@@ -0,0 +1,291 @@
+/*
+ * Copyright (C) 2013 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.UnsupportedAppUsage;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ *  Class that represents useful attributes of generic network links
+ *  such as the upload/download throughput or packet error rate.
+ *  Generally speaking, you should be dealing with instances of
+ *  LinkQualityInfo subclasses, such as {@link android.net.#WifiLinkQualityInfo}
+ *  or {@link android.net.#MobileLinkQualityInfo} which provide additional
+ *  information.
+ *  @hide
+ */
+public class LinkQualityInfo implements Parcelable {
+
+    /**
+     * Represents a value that you can use to test if an integer field is set to a good value
+     */
+    public static final int UNKNOWN_INT = Integer.MAX_VALUE;
+
+    /**
+     * Represents a value that you can use to test if a long field is set to a good value
+     */
+    public static final long UNKNOWN_LONG = Long.MAX_VALUE;
+
+    public static final int NORMALIZED_MIN_SIGNAL_STRENGTH = 0;
+
+    public static final int NORMALIZED_MAX_SIGNAL_STRENGTH = 99;
+
+    public static final int NORMALIZED_SIGNAL_STRENGTH_RANGE =
+            NORMALIZED_MAX_SIGNAL_STRENGTH - NORMALIZED_MIN_SIGNAL_STRENGTH + 1;
+
+    /* Network type as defined by ConnectivityManager */
+    private int mNetworkType = ConnectivityManager.TYPE_NONE;
+
+    private int mNormalizedSignalStrength = UNKNOWN_INT;
+
+    private long mPacketCount = UNKNOWN_LONG;
+    private long mPacketErrorCount = UNKNOWN_LONG;
+    private int mTheoreticalTxBandwidth = UNKNOWN_INT;
+    private int mTheoreticalRxBandwidth = UNKNOWN_INT;
+    private int mTheoreticalLatency = UNKNOWN_INT;
+
+    /* Timestamp when last sample was made available */
+    private long mLastDataSampleTime = UNKNOWN_LONG;
+
+    /* Sample duration in millisecond */
+    private int mDataSampleDuration = UNKNOWN_INT;
+
+    public LinkQualityInfo() {
+
+    }
+
+    /**
+     * Implement the Parcelable interface
+     * @hide
+     */
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Implement the Parcelable interface.
+     */
+
+    protected static final int OBJECT_TYPE_LINK_QUALITY_INFO = 1;
+    protected static final int OBJECT_TYPE_WIFI_LINK_QUALITY_INFO = 2;
+    protected static final int OBJECT_TYPE_MOBILE_LINK_QUALITY_INFO = 3;
+
+    /**
+     * @hide
+     */
+    public void writeToParcel(Parcel dest, int flags) {
+        writeToParcel(dest, flags, OBJECT_TYPE_LINK_QUALITY_INFO);
+    }
+
+    /**
+     * @hide
+     */
+    public void writeToParcel(Parcel dest, int flags, int objectType) {
+        dest.writeInt(objectType);
+        dest.writeInt(mNetworkType);
+        dest.writeInt(mNormalizedSignalStrength);
+        dest.writeLong(mPacketCount);
+        dest.writeLong(mPacketErrorCount);
+        dest.writeInt(mTheoreticalTxBandwidth);
+        dest.writeInt(mTheoreticalRxBandwidth);
+        dest.writeInt(mTheoreticalLatency);
+        dest.writeLong(mLastDataSampleTime);
+        dest.writeInt(mDataSampleDuration);
+    }
+
+    /**
+     * @hide
+     */
+    public static final @android.annotation.NonNull Creator<LinkQualityInfo> CREATOR =
+            new Creator<LinkQualityInfo>() {
+                public LinkQualityInfo createFromParcel(Parcel in) {
+                    int objectType = in.readInt();
+                    if (objectType == OBJECT_TYPE_LINK_QUALITY_INFO) {
+                        LinkQualityInfo li = new LinkQualityInfo();
+                        li.initializeFromParcel(in);
+                        return li;
+                    } else if (objectType == OBJECT_TYPE_WIFI_LINK_QUALITY_INFO) {
+                        return WifiLinkQualityInfo.createFromParcelBody(in);
+                    } else if (objectType == OBJECT_TYPE_MOBILE_LINK_QUALITY_INFO) {
+                        return MobileLinkQualityInfo.createFromParcelBody(in);
+                    } else {
+                        return null;
+                    }
+                }
+
+                public LinkQualityInfo[] newArray(int size) {
+                    return new LinkQualityInfo[size];
+                }
+            };
+
+    /**
+     * @hide
+     */
+    protected void initializeFromParcel(Parcel in) {
+        mNetworkType = in.readInt();
+        mNormalizedSignalStrength = in.readInt();
+        mPacketCount = in.readLong();
+        mPacketErrorCount = in.readLong();
+        mTheoreticalTxBandwidth = in.readInt();
+        mTheoreticalRxBandwidth = in.readInt();
+        mTheoreticalLatency = in.readInt();
+        mLastDataSampleTime = in.readLong();
+        mDataSampleDuration = in.readInt();
+    }
+
+    /**
+     * returns the type of network this link is connected to
+     * @return network type as defined by {@link android.net.ConnectivityManager} or
+     * {@link android.net.LinkQualityInfo#UNKNOWN_INT}
+     */
+    public int getNetworkType() {
+        return mNetworkType;
+    }
+
+    /**
+     * @hide
+     */
+    public void setNetworkType(int networkType) {
+        mNetworkType = networkType;
+    }
+
+    /**
+     * returns the signal strength normalized across multiple types of networks
+     * @return an integer value from 0 - 99 or {@link android.net.LinkQualityInfo#UNKNOWN_INT}
+     */
+    public int getNormalizedSignalStrength() {
+        return mNormalizedSignalStrength;
+    }
+
+    /**
+     * @hide
+     */
+    public void setNormalizedSignalStrength(int normalizedSignalStrength) {
+        mNormalizedSignalStrength = normalizedSignalStrength;
+    }
+
+    /**
+     * returns the total number of packets sent or received in sample duration
+     * @return number of packets or {@link android.net.LinkQualityInfo#UNKNOWN_LONG}
+     */
+    public long getPacketCount() {
+        return mPacketCount;
+    }
+
+    /**
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public void setPacketCount(long packetCount) {
+        mPacketCount = packetCount;
+    }
+
+    /**
+     * returns the total number of packets errors encountered in sample duration
+     * @return number of errors or {@link android.net.LinkQualityInfo#UNKNOWN_LONG}
+     */
+    public long getPacketErrorCount() {
+        return mPacketErrorCount;
+    }
+
+    /**
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public void setPacketErrorCount(long packetErrorCount) {
+        mPacketErrorCount = packetErrorCount;
+    }
+
+    /**
+     * returns the theoretical upload bandwidth of this network
+     * @return bandwidth in Kbps or {@link android.net.LinkQualityInfo#UNKNOWN_INT}
+     */
+    public int getTheoreticalTxBandwidth() {
+        return mTheoreticalTxBandwidth;
+    }
+
+    /**
+     * @hide
+     */
+    public void setTheoreticalTxBandwidth(int theoreticalTxBandwidth) {
+        mTheoreticalTxBandwidth = theoreticalTxBandwidth;
+    }
+
+    /**
+     * returns the theoretical download bandwidth of this network
+     * @return bandwidth in Kbps or {@link android.net.LinkQualityInfo#UNKNOWN_INT}
+     */
+    public int getTheoreticalRxBandwidth() {
+        return mTheoreticalRxBandwidth;
+    }
+
+    /**
+     * @hide
+     */
+    public void setTheoreticalRxBandwidth(int theoreticalRxBandwidth) {
+        mTheoreticalRxBandwidth = theoreticalRxBandwidth;
+    }
+
+    /**
+     * returns the theoretical latency of this network
+     * @return latency in milliseconds or {@link android.net.LinkQualityInfo#UNKNOWN_INT}
+     */
+    public int getTheoreticalLatency() {
+        return mTheoreticalLatency;
+    }
+
+    /**
+     * @hide
+     */
+    public void setTheoreticalLatency(int theoreticalLatency) {
+        mTheoreticalLatency = theoreticalLatency;
+    }
+
+    /**
+     * returns the time stamp of the last sample
+     * @return milliseconds elapsed since start and sample time or
+     * {@link android.net.LinkQualityInfo#UNKNOWN_LONG}
+     */
+    public long getLastDataSampleTime() {
+        return mLastDataSampleTime;
+    }
+
+    /**
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public void setLastDataSampleTime(long lastDataSampleTime) {
+        mLastDataSampleTime = lastDataSampleTime;
+    }
+
+    /**
+     * returns the sample duration used
+     * @return duration in milliseconds or {@link android.net.LinkQualityInfo#UNKNOWN_INT}
+     */
+    public int getDataSampleDuration() {
+        return mDataSampleDuration;
+    }
+
+    /**
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public void setDataSampleDuration(int dataSampleDuration) {
+        mDataSampleDuration = dataSampleDuration;
+    }
+}
diff --git a/android/net/LocalServerSocket.java b/android/net/LocalServerSocket.java
new file mode 100644
index 0000000..d1f49d2
--- /dev/null
+++ b/android/net/LocalServerSocket.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2007 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 java.io.Closeable;
+import java.io.FileDescriptor;
+import java.io.IOException;
+
+/**
+ * Non-standard class for creating an inbound UNIX-domain socket
+ * in the Linux abstract namespace.
+ */
+public class LocalServerSocket implements Closeable {
+    private final LocalSocketImpl impl;
+    private final LocalSocketAddress localAddress;
+
+    /** 50 seems a bit much, but it's what was here */
+    private static final int LISTEN_BACKLOG = 50;
+
+    /**
+     * Creates a new server socket listening at specified name.
+     * On the Android platform, the name is created in the Linux
+     * abstract namespace (instead of on the filesystem).
+     * 
+     * @param name address for socket
+     * @throws IOException
+     */
+    public LocalServerSocket(String name) throws IOException
+    {
+        impl = new LocalSocketImpl();
+
+        impl.create(LocalSocket.SOCKET_STREAM);
+
+        localAddress = new LocalSocketAddress(name);
+        impl.bind(localAddress);
+
+        impl.listen(LISTEN_BACKLOG);
+    }
+
+    /**
+     * Create a LocalServerSocket from a file descriptor that's already
+     * been created and bound. listen() will be called immediately on it.
+     * Used for cases where file descriptors are passed in via environment
+     * variables
+     *
+     * @param fd bound file descriptor
+     * @throws IOException
+     */
+    public LocalServerSocket(FileDescriptor fd) throws IOException
+    {
+        impl = new LocalSocketImpl(fd);
+        impl.listen(LISTEN_BACKLOG);
+        localAddress = impl.getSockAddress();
+    }
+
+    /**
+     * Obtains the socket's local address
+     *
+     * @return local address
+     */
+    public LocalSocketAddress getLocalSocketAddress()
+    {
+        return localAddress;
+    }
+
+    /**
+     * Accepts a new connection to the socket. Blocks until a new
+     * connection arrives.
+     *
+     * @return a socket representing the new connection.
+     * @throws IOException
+     */
+    public LocalSocket accept() throws IOException
+    {
+        LocalSocketImpl acceptedImpl = new LocalSocketImpl();
+
+        impl.accept(acceptedImpl);
+
+        return LocalSocket.createLocalSocketForAccept(acceptedImpl);
+    }
+
+    /**
+     * Returns file descriptor or null if not yet open/already closed
+     *
+     * @return fd or null
+     */
+    public FileDescriptor getFileDescriptor() {
+        return impl.getFileDescriptor();
+    }
+
+    /**
+     * Closes server socket.
+     * 
+     * @throws IOException
+     */
+    @Override public void close() throws IOException
+    {
+        impl.close();
+    }
+}
diff --git a/android/net/LocalSocket.java b/android/net/LocalSocket.java
new file mode 100644
index 0000000..6a2031b
--- /dev/null
+++ b/android/net/LocalSocket.java
@@ -0,0 +1,341 @@
+/*
+ * Copyright (C) 2007 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.UnsupportedAppUsage;
+import java.io.Closeable;
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.SocketOptions;
+
+/**
+ * Creates a (non-server) socket in the UNIX-domain namespace. The interface
+ * here is not entirely unlike that of java.net.Socket. This class and the streams
+ * returned from it may be used from multiple threads.
+ */
+public class LocalSocket implements Closeable {
+
+    @UnsupportedAppUsage
+    private final LocalSocketImpl impl;
+    /** false if impl.create() needs to be called */
+    private volatile boolean implCreated;
+    private LocalSocketAddress localAddress;
+    private boolean isBound;
+    private boolean isConnected;
+    private final int sockType;
+
+    /** unknown socket type (used for constructor with existing file descriptor) */
+    /* package */ static final int SOCKET_UNKNOWN = 0;
+    /** Datagram socket type */
+    public static final int SOCKET_DGRAM = 1;
+    /** Stream socket type */
+    public static final int SOCKET_STREAM = 2;
+    /** Sequential packet socket type */
+    public static final int SOCKET_SEQPACKET = 3;
+
+    /**
+     * Creates a AF_LOCAL/UNIX domain stream socket.
+     */
+    public LocalSocket() {
+        this(SOCKET_STREAM);
+    }
+
+    /**
+     * Creates a AF_LOCAL/UNIX domain stream socket with given socket type
+     *
+     * @param sockType either {@link #SOCKET_DGRAM}, {@link #SOCKET_STREAM}
+     * or {@link #SOCKET_SEQPACKET}
+     */
+    public LocalSocket(int sockType) {
+        this(new LocalSocketImpl(), sockType);
+    }
+
+    private LocalSocket(LocalSocketImpl impl, int sockType) {
+        this.impl = impl;
+        this.sockType = sockType;
+        this.isConnected = false;
+        this.isBound = false;
+    }
+
+    /**
+     * Creates a LocalSocket instances using the FileDescriptor for an already-connected
+     * AF_LOCAL/UNIX domain stream socket. Note: the FileDescriptor must be closed by the caller:
+     * closing the LocalSocket will not close it.
+     *
+     * @hide - used by BluetoothSocket.
+     */
+    public static LocalSocket createConnectedLocalSocket(FileDescriptor fd) {
+        return createConnectedLocalSocket(new LocalSocketImpl(fd), SOCKET_UNKNOWN);
+    }
+
+    /**
+     * for use with LocalServerSocket.accept()
+     */
+    static LocalSocket createLocalSocketForAccept(LocalSocketImpl impl) {
+        return createConnectedLocalSocket(impl, SOCKET_UNKNOWN);
+    }
+
+    /**
+     * Creates a LocalSocket from an existing LocalSocketImpl that is already connected.
+     */
+    private static LocalSocket createConnectedLocalSocket(LocalSocketImpl impl, int sockType) {
+        LocalSocket socket = new LocalSocket(impl, sockType);
+        socket.isConnected = true;
+        socket.isBound = true;
+        socket.implCreated = true;
+        return socket;
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public String toString() {
+        return super.toString() + " impl:" + impl;
+    }
+
+    /**
+     * It's difficult to discern from the spec when impl.create() should be
+     * called, but it seems like a reasonable rule is "as soon as possible,
+     * but not in a context where IOException cannot be thrown"
+     *
+     * @throws IOException from SocketImpl.create()
+     */
+    private void implCreateIfNeeded() throws IOException {
+        if (!implCreated) {
+            synchronized (this) {
+                if (!implCreated) {
+                    try {
+                        impl.create(sockType);
+                    } finally {
+                        implCreated = true;
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Connects this socket to an endpoint. May only be called on an instance
+     * that has not yet been connected.
+     *
+     * @param endpoint endpoint address
+     * @throws IOException if socket is in invalid state or the address does
+     * not exist.
+     */
+    public void connect(LocalSocketAddress endpoint) throws IOException {
+        synchronized (this) {
+            if (isConnected) {
+                throw new IOException("already connected");
+            }
+
+            implCreateIfNeeded();
+            impl.connect(endpoint, 0);
+            isConnected = true;
+            isBound = true;
+        }
+    }
+
+    /**
+     * Binds this socket to an endpoint name. May only be called on an instance
+     * that has not yet been bound.
+     *
+     * @param bindpoint endpoint address
+     * @throws IOException
+     */
+    public void bind(LocalSocketAddress bindpoint) throws IOException {
+        implCreateIfNeeded();
+
+        synchronized (this) {
+            if (isBound) {
+                throw new IOException("already bound");
+            }
+
+            localAddress = bindpoint;
+            impl.bind(localAddress);
+            isBound = true;
+        }
+    }
+
+    /**
+     * Retrieves the name that this socket is bound to, if any.
+     *
+     * @return Local address or null if anonymous
+     */
+    public LocalSocketAddress getLocalSocketAddress() {
+        return localAddress;
+    }
+
+    /**
+     * Retrieves the input stream for this instance.
+     *
+     * @return input stream
+     * @throws IOException if socket has been closed or cannot be created.
+     */
+    public InputStream getInputStream() throws IOException {
+        implCreateIfNeeded();
+        return impl.getInputStream();
+    }
+
+    /**
+     * Retrieves the output stream for this instance.
+     *
+     * @return output stream
+     * @throws IOException if socket has been closed or cannot be created.
+     */
+    public OutputStream getOutputStream() throws IOException {
+        implCreateIfNeeded();
+        return impl.getOutputStream();
+    }
+
+    /**
+     * Closes the socket.
+     *
+     * @throws IOException
+     */
+    @Override
+    public void close() throws IOException {
+        implCreateIfNeeded();
+        impl.close();
+    }
+
+    /**
+     * Shuts down the input side of the socket.
+     *
+     * @throws IOException
+     */
+    public void shutdownInput() throws IOException {
+        implCreateIfNeeded();
+        impl.shutdownInput();
+    }
+
+    /**
+     * Shuts down the output side of the socket.
+     *
+     * @throws IOException
+     */
+    public void shutdownOutput() throws IOException {
+        implCreateIfNeeded();
+        impl.shutdownOutput();
+    }
+
+    public void setReceiveBufferSize(int size) throws IOException {
+        impl.setOption(SocketOptions.SO_RCVBUF, Integer.valueOf(size));
+    }
+
+    public int getReceiveBufferSize() throws IOException {
+        return ((Integer) impl.getOption(SocketOptions.SO_RCVBUF)).intValue();
+    }
+
+    public void setSoTimeout(int n) throws IOException {
+        impl.setOption(SocketOptions.SO_TIMEOUT, Integer.valueOf(n));
+    }
+
+    public int getSoTimeout() throws IOException {
+        return ((Integer) impl.getOption(SocketOptions.SO_TIMEOUT)).intValue();
+    }
+
+    public void setSendBufferSize(int n) throws IOException {
+        impl.setOption(SocketOptions.SO_SNDBUF, Integer.valueOf(n));
+    }
+
+    public int getSendBufferSize() throws IOException {
+        return ((Integer) impl.getOption(SocketOptions.SO_SNDBUF)).intValue();
+    }
+
+    //???SEC
+    public LocalSocketAddress getRemoteSocketAddress() {
+        throw new UnsupportedOperationException();
+    }
+
+    //???SEC
+    public synchronized boolean isConnected() {
+        return isConnected;
+    }
+
+    //???SEC
+    public boolean isClosed() {
+        throw new UnsupportedOperationException();
+    }
+
+    //???SEC
+    public synchronized boolean isBound() {
+        return isBound;
+    }
+
+    //???SEC
+    public boolean isOutputShutdown() {
+        throw new UnsupportedOperationException();
+    }
+
+    //???SEC
+    public boolean isInputShutdown() {
+        throw new UnsupportedOperationException();
+    }
+
+    //???SEC
+    public void connect(LocalSocketAddress endpoint, int timeout)
+            throws IOException {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * Enqueues a set of file descriptors to send to the peer. The queue
+     * is one deep. The file descriptors will be sent with the next write
+     * of normal data, and will be delivered in a single ancillary message.
+     * See "man 7 unix" SCM_RIGHTS on a desktop Linux machine.
+     *
+     * @param fds non-null; file descriptors to send.
+     */
+    public void setFileDescriptorsForSend(FileDescriptor[] fds) {
+        impl.setFileDescriptorsForSend(fds);
+    }
+
+    /**
+     * Retrieves a set of file descriptors that a peer has sent through
+     * an ancillary message. This method retrieves the most recent set sent,
+     * and then returns null until a new set arrives.
+     * File descriptors may only be passed along with regular data, so this
+     * method can only return a non-null after a read operation.
+     *
+     * @return null or file descriptor array
+     * @throws IOException
+     */
+    public FileDescriptor[] getAncillaryFileDescriptors() throws IOException {
+        return impl.getAncillaryFileDescriptors();
+    }
+
+    /**
+     * Retrieves the credentials of this socket's peer. Only valid on
+     * connected sockets.
+     *
+     * @return non-null; peer credentials
+     * @throws IOException
+     */
+    public Credentials getPeerCredentials() throws IOException {
+        return impl.getPeerCredentials();
+    }
+
+    /**
+     * Returns file descriptor or null if not yet open/already closed
+     *
+     * @return fd or null
+     */
+    public FileDescriptor getFileDescriptor() {
+        return impl.getFileDescriptor();
+    }
+}
diff --git a/android/net/LocalSocketAddress.java b/android/net/LocalSocketAddress.java
new file mode 100644
index 0000000..8265b85
--- /dev/null
+++ b/android/net/LocalSocketAddress.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2007 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;
+
+/**
+ * A UNIX-domain (AF_LOCAL) socket address. For use with
+ * android.net.LocalSocket and android.net.LocalServerSocket.
+ *
+ * On the Android system, these names refer to names in the Linux
+ * abstract (non-filesystem) UNIX domain namespace.
+ */
+public class LocalSocketAddress
+{
+    /**
+     * The namespace that this address exists in. See also
+     * include/cutils/sockets.h ANDROID_SOCKET_NAMESPACE_*
+     */
+    public enum Namespace {
+        /** A socket in the Linux abstract namespace */
+        ABSTRACT(0),
+        /**
+         * A socket in the Android reserved namespace in /dev/socket.
+         * Only the init process may create a socket here.
+         */
+        RESERVED(1),
+        /**
+         * A socket named with a normal filesystem path.
+         */
+        FILESYSTEM(2);
+
+        /** The id matches with a #define in include/cutils/sockets.h */
+        private int id;
+        Namespace (int id) {
+            this.id = id;
+        }
+
+        /**
+         * @return int constant shared with native code
+         */
+        /*package*/ int getId() {
+            return id;
+        }
+    }
+
+    private final String name;
+    private final Namespace namespace;
+
+    /**
+     * Creates an instance with a given name.
+     *
+     * @param name non-null name
+     * @param namespace namespace the name should be created in.
+     */
+    public LocalSocketAddress(String name, Namespace namespace) {
+        this.name = name;
+        this.namespace = namespace;
+    }
+
+    /**
+     * Creates an instance with a given name in the {@link Namespace#ABSTRACT}
+     * namespace
+     *
+     * @param name non-null name
+     */
+    public LocalSocketAddress(String name) {
+        this(name,Namespace.ABSTRACT);
+    }
+
+    /**
+     * Retrieves the string name of this address
+     * @return string name
+     */
+    public String getName()
+    {
+        return name;
+    }
+
+    /**
+     * Returns the namespace used by this address.
+     *
+     * @return non-null a namespace
+     */
+    public Namespace getNamespace() {
+        return namespace;
+    }
+}
diff --git a/android/net/LocalSocketImpl.java b/android/net/LocalSocketImpl.java
new file mode 100644
index 0000000..fe7632c
--- /dev/null
+++ b/android/net/LocalSocketImpl.java
@@ -0,0 +1,622 @@
+/*
+ * Copyright (C) 2007 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.UnsupportedAppUsage;
+import android.system.ErrnoException;
+import android.system.Int32Ref;
+import android.system.Os;
+import android.system.OsConstants;
+import android.system.StructLinger;
+import android.system.StructTimeval;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.SocketOptions;
+
+/**
+ * Socket implementation used for android.net.LocalSocket and
+ * android.net.LocalServerSocket. Supports only AF_LOCAL sockets.
+ */
+class LocalSocketImpl
+{
+    private SocketInputStream fis;
+    private SocketOutputStream fos;
+    private Object readMonitor = new Object();
+    private Object writeMonitor = new Object();
+
+    /** null if closed or not yet created */
+    private FileDescriptor fd;
+    /** whether fd is created internally */
+    private boolean mFdCreatedInternally;
+
+    // These fields are accessed by native code;
+    /** file descriptor array received during a previous read */
+    @UnsupportedAppUsage
+    FileDescriptor[] inboundFileDescriptors;
+    /** file descriptor array that should be written during next write */
+    @UnsupportedAppUsage
+    FileDescriptor[] outboundFileDescriptors;
+
+    /**
+     * An input stream for local sockets. Needed because we may
+     * need to read ancillary data.
+     */
+    class SocketInputStream extends InputStream {
+        /** {@inheritDoc} */
+        @Override
+        public int available() throws IOException {
+            FileDescriptor myFd = fd;
+            if (myFd == null) throw new IOException("socket closed");
+
+            Int32Ref avail = new Int32Ref(0);
+            try {
+                Os.ioctlInt(myFd, OsConstants.FIONREAD, avail);
+            } catch (ErrnoException e) {
+                throw e.rethrowAsIOException();
+            }
+            return avail.value;
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public void close() throws IOException {
+            LocalSocketImpl.this.close();
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public int read() throws IOException {
+            int ret;
+            synchronized (readMonitor) {
+                FileDescriptor myFd = fd;
+                if (myFd == null) throw new IOException("socket closed");
+
+                ret = read_native(myFd);
+                return ret;
+            }
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public int read(byte[] b) throws IOException {
+            return read(b, 0, b.length);
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public int read(byte[] b, int off, int len) throws IOException {
+            synchronized (readMonitor) {
+                FileDescriptor myFd = fd;
+                if (myFd == null) throw new IOException("socket closed");
+
+                if (off < 0 || len < 0 || (off + len) > b.length ) {
+                    throw new ArrayIndexOutOfBoundsException();
+                }
+
+                int ret = readba_native(b, off, len, myFd);
+
+                return ret;
+            }
+        }
+    }
+
+    /**
+     * An output stream for local sockets. Needed because we may
+     * need to read ancillary data.
+     */
+    class SocketOutputStream extends OutputStream {
+        /** {@inheritDoc} */
+        @Override
+        public void close() throws IOException {
+            LocalSocketImpl.this.close();
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public void write (byte[] b) throws IOException {
+            write(b, 0, b.length);
+        }
+        
+        /** {@inheritDoc} */
+        @Override
+        public void write (byte[] b, int off, int len) throws IOException {
+            synchronized (writeMonitor) {
+                FileDescriptor myFd = fd;
+                if (myFd == null) throw new IOException("socket closed");
+
+                if (off < 0 || len < 0 || (off + len) > b.length ) {
+                    throw new ArrayIndexOutOfBoundsException();
+                }
+                writeba_native(b, off, len, myFd);
+            }
+        }
+
+        /** {@inheritDoc} */
+        @Override
+        public void write (int b) throws IOException {
+            synchronized (writeMonitor) {
+                FileDescriptor myFd = fd;
+                if (myFd == null) throw new IOException("socket closed");
+                write_native(b, myFd);
+            }
+        }
+
+        /**
+         * Wait until the data in sending queue is emptied. A polling version
+         * for flush implementation.
+         * @throws IOException
+         *             if an i/o error occurs.
+         */
+        @Override
+        public void flush() throws IOException {
+            FileDescriptor myFd = fd;
+            if (myFd == null) throw new IOException("socket closed");
+
+            // Loop until the output buffer is empty.
+            Int32Ref pending = new Int32Ref(0);
+            while (true) {
+                try {
+                    // See linux/net/unix/af_unix.c
+                    Os.ioctlInt(myFd, OsConstants.TIOCOUTQ, pending);
+                } catch (ErrnoException e) {
+                    throw e.rethrowAsIOException();
+                }
+
+                if (pending.value <= 0) {
+                    // The output buffer is empty.
+                    break;
+                }
+
+                try {
+                    Thread.sleep(10);
+                } catch (InterruptedException ie) {
+                    break;
+                }
+            }
+        }
+    }
+
+    private native int read_native(FileDescriptor fd) throws IOException;
+    private native int readba_native(byte[] b, int off, int len,
+            FileDescriptor fd) throws IOException;
+    private native void writeba_native(byte[] b, int off, int len,
+            FileDescriptor fd) throws IOException;
+    private native void write_native(int b, FileDescriptor fd)
+            throws IOException;
+    private native void connectLocal(FileDescriptor fd, String name,
+            int namespace) throws IOException;
+    private native void bindLocal(FileDescriptor fd, String name, int namespace)
+            throws IOException;
+    private native Credentials getPeerCredentials_native(
+            FileDescriptor fd) throws IOException;
+
+    /**
+     * Create a new instance.
+     */
+    @UnsupportedAppUsage
+    /*package*/ LocalSocketImpl()
+    {
+    }
+
+    /**
+     * Create a new instance from a file descriptor representing
+     * a bound socket. The state of the file descriptor is not checked here
+     *  but the caller can verify socket state by calling listen().
+     *
+     * @param fd non-null; bound file descriptor
+     */
+    /*package*/ LocalSocketImpl(FileDescriptor fd)
+    {
+        this.fd = fd;
+    }
+
+    public String toString() {
+        return super.toString() + " fd:" + fd;
+    }
+
+    /**
+     * Creates a socket in the underlying OS.
+     *
+     * @param sockType either {@link LocalSocket#SOCKET_DGRAM}, {@link LocalSocket#SOCKET_STREAM}
+     * or {@link LocalSocket#SOCKET_SEQPACKET}
+     * @throws IOException
+     */
+    public void create(int sockType) throws IOException {
+        if (fd != null) {
+            throw new IOException("LocalSocketImpl already has an fd");
+        }
+
+        int osType;
+        switch (sockType) {
+            case LocalSocket.SOCKET_DGRAM:
+                osType = OsConstants.SOCK_DGRAM;
+                break;
+            case LocalSocket.SOCKET_STREAM:
+                osType = OsConstants.SOCK_STREAM;
+                break;
+            case LocalSocket.SOCKET_SEQPACKET:
+                osType = OsConstants.SOCK_SEQPACKET;
+                break;
+            default:
+                throw new IllegalStateException("unknown sockType");
+        }
+        try {
+            fd = Os.socket(OsConstants.AF_UNIX, osType, 0);
+            mFdCreatedInternally = true;
+        } catch (ErrnoException e) {
+            e.rethrowAsIOException();
+        }
+    }
+
+    /**
+     * Closes the socket.
+     *
+     * @throws IOException
+     */
+    public void close() throws IOException {
+        synchronized (LocalSocketImpl.this) {
+            if ((fd == null) || (mFdCreatedInternally == false)) {
+                fd = null;
+                return;
+            }
+            try {
+                Os.close(fd);
+            } catch (ErrnoException e) {
+                e.rethrowAsIOException();
+            }
+            fd = null;
+        }
+    }
+
+    /** note timeout presently ignored */
+    protected void connect(LocalSocketAddress address, int timeout)
+                        throws IOException
+    {        
+        if (fd == null) {
+            throw new IOException("socket not created");
+        }
+
+        connectLocal(fd, address.getName(), address.getNamespace().getId());
+    }
+
+    /**
+     * Binds this socket to an endpoint name. May only be called on an instance
+     * that has not yet been bound.
+     *
+     * @param endpoint endpoint address
+     * @throws IOException
+     */
+    public void bind(LocalSocketAddress endpoint) throws IOException
+    {
+        if (fd == null) {
+            throw new IOException("socket not created");
+        }
+
+        bindLocal(fd, endpoint.getName(), endpoint.getNamespace().getId());
+    }
+
+    protected void listen(int backlog) throws IOException
+    {
+        if (fd == null) {
+            throw new IOException("socket not created");
+        }
+        try {
+            Os.listen(fd, backlog);
+        } catch (ErrnoException e) {
+            throw e.rethrowAsIOException();
+        }
+    }
+
+    /**
+     * Accepts a new connection to the socket. Blocks until a new
+     * connection arrives.
+     *
+     * @param s a socket that will be used to represent the new connection.
+     * @throws IOException
+     */
+    protected void accept(LocalSocketImpl s) throws IOException {
+        if (fd == null) {
+            throw new IOException("socket not created");
+        }
+
+        try {
+            s.fd = Os.accept(fd, null /* address */);
+            s.mFdCreatedInternally = true;
+        } catch (ErrnoException e) {
+            throw e.rethrowAsIOException();
+        }
+    }
+
+    /**
+     * Retrieves the input stream for this instance.
+     *
+     * @return input stream
+     * @throws IOException if socket has been closed or cannot be created.
+     */
+    protected InputStream getInputStream() throws IOException
+    {
+        if (fd == null) {
+            throw new IOException("socket not created");
+        }
+
+        synchronized (this) {
+            if (fis == null) {
+                fis = new SocketInputStream();
+            }
+
+            return fis;
+        }
+    }
+
+    /**
+     * Retrieves the output stream for this instance.
+     *
+     * @return output stream
+     * @throws IOException if socket has been closed or cannot be created.
+     */
+    protected OutputStream getOutputStream() throws IOException
+    { 
+        if (fd == null) {
+            throw new IOException("socket not created");
+        }
+
+        synchronized (this) {
+            if (fos == null) {
+                fos = new SocketOutputStream();
+            }
+
+            return fos;
+        }
+    }
+
+    /**
+     * Returns the number of bytes available for reading without blocking.
+     *
+     * @return >= 0 count bytes available
+     * @throws IOException
+     */
+    protected int available() throws IOException
+    {
+        return getInputStream().available();
+    }
+
+    /**
+     * Shuts down the input side of the socket.
+     *
+     * @throws IOException
+     */
+    protected void shutdownInput() throws IOException
+    {
+        if (fd == null) {
+            throw new IOException("socket not created");
+        }
+
+        try {
+            Os.shutdown(fd, OsConstants.SHUT_RD);
+        } catch (ErrnoException e) {
+            throw e.rethrowAsIOException();
+        }
+    }
+
+    /**
+     * Shuts down the output side of the socket.
+     *
+     * @throws IOException
+     */
+    protected void shutdownOutput() throws IOException
+    {
+        if (fd == null) {
+            throw new IOException("socket not created");
+        }
+
+        try {
+            Os.shutdown(fd, OsConstants.SHUT_WR);
+        } catch (ErrnoException e) {
+            throw e.rethrowAsIOException();
+        }
+    }
+
+    protected FileDescriptor getFileDescriptor()
+    {
+        return fd;
+    }
+
+    protected boolean supportsUrgentData()
+    {
+        return false;
+    }
+
+    protected void sendUrgentData(int data) throws IOException
+    {
+        throw new RuntimeException ("not impled");
+    }
+
+    public Object getOption(int optID) throws IOException
+    {
+        if (fd == null) {
+            throw new IOException("socket not created");
+        }
+
+        try {
+            Object toReturn;
+            switch (optID) {
+                case SocketOptions.SO_TIMEOUT:
+                    StructTimeval timeval = Os.getsockoptTimeval(fd, OsConstants.SOL_SOCKET,
+                            OsConstants.SO_SNDTIMEO);
+                    toReturn = (int) timeval.toMillis();
+                    break;
+                case SocketOptions.SO_RCVBUF:
+                case SocketOptions.SO_SNDBUF:
+                case SocketOptions.SO_REUSEADDR:
+                    int osOpt = javaSoToOsOpt(optID);
+                    toReturn = Os.getsockoptInt(fd, OsConstants.SOL_SOCKET, osOpt);
+                    break;
+                case SocketOptions.SO_LINGER:
+                    StructLinger linger=
+                            Os.getsockoptLinger(fd, OsConstants.SOL_SOCKET, OsConstants.SO_LINGER);
+                    if (!linger.isOn()) {
+                        toReturn = -1;
+                    } else {
+                        toReturn = linger.l_linger;
+                    }
+                    break;
+                case SocketOptions.TCP_NODELAY:
+                    toReturn = Os.getsockoptInt(fd, OsConstants.IPPROTO_TCP,
+                            OsConstants.TCP_NODELAY);
+                    break;
+                default:
+                    throw new IOException("Unknown option: " + optID);
+            }
+            return toReturn;
+        } catch (ErrnoException e) {
+            throw e.rethrowAsIOException();
+        }
+    }
+
+    public void setOption(int optID, Object value)
+            throws IOException {
+
+        if (fd == null) {
+            throw new IOException("socket not created");
+        }
+
+        /*
+         * Boolean.FALSE is used to disable some options, so it
+         * is important to distinguish between FALSE and unset.
+         * We define it here that -1 is unset, 0 is FALSE, and 1
+         * is TRUE.
+         */
+        int boolValue = -1;
+        int intValue = 0;
+        if (value instanceof Integer) {
+            intValue = (Integer)value;
+        } else if (value instanceof Boolean) {
+            boolValue = ((Boolean) value)? 1 : 0;
+        } else {
+            throw new IOException("bad value: " + value);
+        }
+
+        try {
+            switch (optID) {
+                case SocketOptions.SO_LINGER:
+                    StructLinger linger = new StructLinger(boolValue, intValue);
+                    Os.setsockoptLinger(fd, OsConstants.SOL_SOCKET, OsConstants.SO_LINGER, linger);
+                    break;
+                case SocketOptions.SO_TIMEOUT:
+                    // The option must set both send and receive timeouts.
+                    // Note: The incoming timeout value is in milliseconds.
+                    StructTimeval timeval = StructTimeval.fromMillis(intValue);
+                    Os.setsockoptTimeval(fd, OsConstants.SOL_SOCKET, OsConstants.SO_RCVTIMEO,
+                            timeval);
+                    Os.setsockoptTimeval(fd, OsConstants.SOL_SOCKET, OsConstants.SO_SNDTIMEO,
+                            timeval);
+                    break;
+                case SocketOptions.SO_RCVBUF:
+                case SocketOptions.SO_SNDBUF:
+                case SocketOptions.SO_REUSEADDR:
+                    int osOpt = javaSoToOsOpt(optID);
+                    Os.setsockoptInt(fd, OsConstants.SOL_SOCKET, osOpt, intValue);
+                    break;
+                case SocketOptions.TCP_NODELAY:
+                    Os.setsockoptInt(fd, OsConstants.IPPROTO_TCP, OsConstants.TCP_NODELAY,
+                            intValue);
+                    break;
+                default:
+                    throw new IOException("Unknown option: " + optID);
+            }
+        } catch (ErrnoException e) {
+            throw e.rethrowAsIOException();
+        }
+    }
+
+    /**
+     * Enqueues a set of file descriptors to send to the peer. The queue
+     * is one deep. The file descriptors will be sent with the next write
+     * of normal data, and will be delivered in a single ancillary message.
+     * See "man 7 unix" SCM_RIGHTS on a desktop Linux machine.
+     *
+     * @param fds non-null; file descriptors to send.
+     * @throws IOException
+     */
+    public void setFileDescriptorsForSend(FileDescriptor[] fds) {
+        synchronized(writeMonitor) {
+            outboundFileDescriptors = fds;
+        }
+    }
+
+    /**
+     * Retrieves a set of file descriptors that a peer has sent through
+     * an ancillary message. This method retrieves the most recent set sent,
+     * and then returns null until a new set arrives.
+     * File descriptors may only be passed along with regular data, so this
+     * method can only return a non-null after a read operation.
+     *
+     * @return null or file descriptor array
+     * @throws IOException
+     */
+    public FileDescriptor[] getAncillaryFileDescriptors() throws IOException {
+        synchronized(readMonitor) {
+            FileDescriptor[] result = inboundFileDescriptors;
+
+            inboundFileDescriptors = null;
+            return result;
+        }
+    }
+
+    /**
+     * Retrieves the credentials of this socket's peer. Only valid on
+     * connected sockets.
+     *
+     * @return non-null; peer credentials
+     * @throws IOException
+     */
+    public Credentials getPeerCredentials() throws IOException {
+        return getPeerCredentials_native(fd);
+    }
+
+    /**
+     * Retrieves the socket name from the OS.
+     *
+     * @return non-null; socket name
+     * @throws IOException on failure
+     */
+    public LocalSocketAddress getSockAddress() throws IOException {
+        // This method has never been implemented.
+        return null;
+    }
+
+    @Override
+    protected void finalize() throws IOException {
+        close();
+    }
+
+    private static int javaSoToOsOpt(int optID) {
+        switch (optID) {
+            case SocketOptions.SO_SNDBUF:
+                return OsConstants.SO_SNDBUF;
+            case SocketOptions.SO_RCVBUF:
+                return OsConstants.SO_RCVBUF;
+            case SocketOptions.SO_REUSEADDR:
+                return OsConstants.SO_REUSEADDR;
+            default:
+                throw new UnsupportedOperationException("Unknown option: " + optID);
+        }
+    }
+}
diff --git a/android/net/MacAddress.java b/android/net/MacAddress.java
new file mode 100644
index 0000000..aa8e010
--- /dev/null
+++ b/android/net/MacAddress.java
@@ -0,0 +1,446 @@
+/*
+ * Copyright 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UnsupportedAppUsage;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.BitUtils;
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.net.Inet6Address;
+import java.net.UnknownHostException;
+import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.Random;
+
+/**
+ * Representation of a MAC address.
+ *
+ * This class only supports 48 bits long addresses and does not support 64 bits long addresses.
+ * Instances of this class are immutable.
+ */
+public final class MacAddress implements Parcelable {
+
+    private static final int ETHER_ADDR_LEN = 6;
+    private static final byte[] ETHER_ADDR_BROADCAST = addr(0xff, 0xff, 0xff, 0xff, 0xff, 0xff);
+
+    /**
+     * The MacAddress representing the unique broadcast MAC address.
+     */
+    public static final MacAddress BROADCAST_ADDRESS = MacAddress.fromBytes(ETHER_ADDR_BROADCAST);
+
+    /**
+     * The MacAddress zero MAC address.
+     *
+     * <p>Not publicly exposed or treated specially since the OUI 00:00:00 is registered.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public static final MacAddress ALL_ZEROS_ADDRESS = new MacAddress(0);
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = { "TYPE_" }, value = {
+            TYPE_UNKNOWN,
+            TYPE_UNICAST,
+            TYPE_MULTICAST,
+            TYPE_BROADCAST,
+    })
+    public @interface MacAddressType { }
+
+    /** @hide Indicates a MAC address of unknown type. */
+    public static final int TYPE_UNKNOWN = 0;
+    /** Indicates a MAC address is a unicast address. */
+    public static final int TYPE_UNICAST = 1;
+    /** Indicates a MAC address is a multicast address. */
+    public static final int TYPE_MULTICAST = 2;
+    /** Indicates a MAC address is the broadcast address. */
+    public static final int TYPE_BROADCAST = 3;
+
+    private static final long VALID_LONG_MASK = (1L << 48) - 1;
+    private static final long LOCALLY_ASSIGNED_MASK = MacAddress.fromString("2:0:0:0:0:0").mAddr;
+    private static final long MULTICAST_MASK = MacAddress.fromString("1:0:0:0:0:0").mAddr;
+    private static final long OUI_MASK = MacAddress.fromString("ff:ff:ff:0:0:0").mAddr;
+    private static final long NIC_MASK = MacAddress.fromString("0:0:0:ff:ff:ff").mAddr;
+    private static final MacAddress BASE_GOOGLE_MAC = MacAddress.fromString("da:a1:19:0:0:0");
+
+    // Internal representation of the MAC address as a single 8 byte long.
+    // The encoding scheme sets the two most significant bytes to 0. The 6 bytes of the
+    // MAC address are encoded in the 6 least significant bytes of the long, where the first
+    // byte of the array is mapped to the 3rd highest logical byte of the long, the second
+    // byte of the array is mapped to the 4th highest logical byte of the long, and so on.
+    private final long mAddr;
+
+    private MacAddress(long addr) {
+        mAddr = (VALID_LONG_MASK & addr);
+    }
+
+    /**
+     * Returns the type of this address.
+     *
+     * @return the int constant representing the MAC address type of this MacAddress.
+     */
+    public @MacAddressType int getAddressType() {
+        if (equals(BROADCAST_ADDRESS)) {
+            return TYPE_BROADCAST;
+        }
+        if (isMulticastAddress()) {
+            return TYPE_MULTICAST;
+        }
+        return TYPE_UNICAST;
+    }
+
+    /**
+     * @return true if this MacAddress is a multicast address.
+     * @hide
+     */
+    public boolean isMulticastAddress() {
+        return (mAddr & MULTICAST_MASK) != 0;
+    }
+
+    /**
+     * @return true if this MacAddress is a locally assigned address.
+     */
+    public boolean isLocallyAssigned() {
+        return (mAddr & LOCALLY_ASSIGNED_MASK) != 0;
+    }
+
+    /**
+     * @return a byte array representation of this MacAddress.
+     */
+    public @NonNull byte[] toByteArray() {
+        return byteAddrFromLongAddr(mAddr);
+    }
+
+    @Override
+    public @NonNull String toString() {
+        return stringAddrFromLongAddr(mAddr);
+    }
+
+    /**
+     * @return a String representation of the OUI part of this MacAddress made of 3 hexadecimal
+     * numbers in [0,ff] joined by ':' characters.
+     */
+    public @NonNull String toOuiString() {
+        return String.format(
+                "%02x:%02x:%02x", (mAddr >> 40) & 0xff, (mAddr >> 32) & 0xff, (mAddr >> 24) & 0xff);
+    }
+
+    @Override
+    public int hashCode() {
+        return (int) ((mAddr >> 32) ^ mAddr);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        return (o instanceof MacAddress) && ((MacAddress) o).mAddr == mAddr;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeLong(mAddr);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public static final @android.annotation.NonNull Parcelable.Creator<MacAddress> CREATOR =
+            new Parcelable.Creator<MacAddress>() {
+                public MacAddress createFromParcel(Parcel in) {
+                    return new MacAddress(in.readLong());
+                }
+
+                public MacAddress[] newArray(int size) {
+                    return new MacAddress[size];
+                }
+            };
+
+    /**
+     * Returns true if the given byte array is an valid MAC address.
+     * A valid byte array representation for a MacAddress is a non-null array of length 6.
+     *
+     * @param addr a byte array.
+     * @return true if the given byte array is not null and has the length of a MAC address.
+     *
+     * @hide
+     */
+    public static boolean isMacAddress(byte[] addr) {
+        return addr != null && addr.length == ETHER_ADDR_LEN;
+    }
+
+    /**
+     * Returns the MAC address type of the MAC address represented by the given byte array,
+     * or null if the given byte array does not represent a MAC address.
+     * A valid byte array representation for a MacAddress is a non-null array of length 6.
+     *
+     * @param addr a byte array representing a MAC address.
+     * @return the int constant representing the MAC address type of the MAC address represented
+     * by the given byte array, or type UNKNOWN if the byte array is not a valid MAC address.
+     *
+     * @hide
+     */
+    public static int macAddressType(byte[] addr) {
+        if (!isMacAddress(addr)) {
+            return TYPE_UNKNOWN;
+        }
+        return MacAddress.fromBytes(addr).getAddressType();
+    }
+
+    /**
+     * Converts a String representation of a MAC address to a byte array representation.
+     * A valid String representation for a MacAddress is a series of 6 values in the
+     * range [0,ff] printed in hexadecimal and joined by ':' characters.
+     *
+     * @param addr a String representation of a MAC address.
+     * @return the byte representation of the MAC address.
+     * @throws IllegalArgumentException if the given String is not a valid representation.
+     *
+     * @hide
+     */
+    public static @NonNull byte[] byteAddrFromStringAddr(String addr) {
+        Preconditions.checkNotNull(addr);
+        String[] parts = addr.split(":");
+        if (parts.length != ETHER_ADDR_LEN) {
+            throw new IllegalArgumentException(addr + " was not a valid MAC address");
+        }
+        byte[] bytes = new byte[ETHER_ADDR_LEN];
+        for (int i = 0; i < ETHER_ADDR_LEN; i++) {
+            int x = Integer.valueOf(parts[i], 16);
+            if (x < 0 || 0xff < x) {
+                throw new IllegalArgumentException(addr + "was not a valid MAC address");
+            }
+            bytes[i] = (byte) x;
+        }
+        return bytes;
+    }
+
+    /**
+     * Converts a byte array representation of a MAC address to a String representation made
+     * of 6 hexadecimal numbers in [0,ff] joined by ':' characters.
+     * A valid byte array representation for a MacAddress is a non-null array of length 6.
+     *
+     * @param addr a byte array representation of a MAC address.
+     * @return the String representation of the MAC address.
+     * @throws IllegalArgumentException if the given byte array is not a valid representation.
+     *
+     * @hide
+     */
+    public static @NonNull String stringAddrFromByteAddr(byte[] addr) {
+        if (!isMacAddress(addr)) {
+            return null;
+        }
+        return String.format("%02x:%02x:%02x:%02x:%02x:%02x",
+                addr[0], addr[1], addr[2], addr[3], addr[4], addr[5]);
+    }
+
+    private static byte[] byteAddrFromLongAddr(long addr) {
+        byte[] bytes = new byte[ETHER_ADDR_LEN];
+        int index = ETHER_ADDR_LEN;
+        while (index-- > 0) {
+            bytes[index] = (byte) addr;
+            addr = addr >> 8;
+        }
+        return bytes;
+    }
+
+    private static long longAddrFromByteAddr(byte[] addr) {
+        Preconditions.checkNotNull(addr);
+        if (!isMacAddress(addr)) {
+            throw new IllegalArgumentException(
+                    Arrays.toString(addr) + " was not a valid MAC address");
+        }
+        long longAddr = 0;
+        for (byte b : addr) {
+            longAddr = (longAddr << 8) + BitUtils.uint8(b);
+        }
+        return longAddr;
+    }
+
+    // Internal conversion function equivalent to longAddrFromByteAddr(byteAddrFromStringAddr(addr))
+    // that avoids the allocation of an intermediary byte[].
+    private static long longAddrFromStringAddr(String addr) {
+        Preconditions.checkNotNull(addr);
+        String[] parts = addr.split(":");
+        if (parts.length != ETHER_ADDR_LEN) {
+            throw new IllegalArgumentException(addr + " was not a valid MAC address");
+        }
+        long longAddr = 0;
+        for (int i = 0; i < parts.length; i++) {
+            int x = Integer.valueOf(parts[i], 16);
+            if (x < 0 || 0xff < x) {
+                throw new IllegalArgumentException(addr + "was not a valid MAC address");
+            }
+            longAddr = x + (longAddr << 8);
+        }
+        return longAddr;
+    }
+
+    // Internal conversion function equivalent to stringAddrFromByteAddr(byteAddrFromLongAddr(addr))
+    // that avoids the allocation of an intermediary byte[].
+    private static @NonNull String stringAddrFromLongAddr(long addr) {
+        return String.format("%02x:%02x:%02x:%02x:%02x:%02x",
+                (addr >> 40) & 0xff,
+                (addr >> 32) & 0xff,
+                (addr >> 24) & 0xff,
+                (addr >> 16) & 0xff,
+                (addr >> 8) & 0xff,
+                addr & 0xff);
+    }
+
+    /**
+     * Creates a MacAddress from the given String representation. A valid String representation
+     * for a MacAddress is a series of 6 values in the range [0,ff] printed in hexadecimal
+     * and joined by ':' characters.
+     *
+     * @param addr a String representation of a MAC address.
+     * @return the MacAddress corresponding to the given String representation.
+     * @throws IllegalArgumentException if the given String is not a valid representation.
+     */
+    public static @NonNull MacAddress fromString(@NonNull String addr) {
+        return new MacAddress(longAddrFromStringAddr(addr));
+    }
+
+    /**
+     * Creates a MacAddress from the given byte array representation.
+     * A valid byte array representation for a MacAddress is a non-null array of length 6.
+     *
+     * @param addr a byte array representation of a MAC address.
+     * @return the MacAddress corresponding to the given byte array representation.
+     * @throws IllegalArgumentException if the given byte array is not a valid representation.
+     */
+    public static @NonNull MacAddress fromBytes(@NonNull byte[] addr) {
+        return new MacAddress(longAddrFromByteAddr(addr));
+    }
+
+    /**
+     * Returns a generated MAC address whose 24 least significant bits constituting the
+     * NIC part of the address are randomly selected and has Google OUI base.
+     *
+     * The locally assigned bit is always set to 1. The multicast bit is always set to 0.
+     *
+     * @return a random locally assigned, unicast MacAddress with Google OUI.
+     *
+     * @hide
+     */
+    public static @NonNull MacAddress createRandomUnicastAddressWithGoogleBase() {
+        return createRandomUnicastAddress(BASE_GOOGLE_MAC, new SecureRandom());
+    }
+
+    /**
+     * Returns a generated MAC address whose 46 bits, excluding the locally assigned bit and the
+     * unicast bit, are randomly selected.
+     *
+     * The locally assigned bit is always set to 1. The multicast bit is always set to 0.
+     *
+     * @return a random locally assigned, unicast MacAddress.
+     *
+     * @hide
+     */
+    public static @NonNull MacAddress createRandomUnicastAddress() {
+        SecureRandom r = new SecureRandom();
+        long addr = r.nextLong() & VALID_LONG_MASK;
+        addr |= LOCALLY_ASSIGNED_MASK;
+        addr &= ~MULTICAST_MASK;
+        return new MacAddress(addr);
+    }
+
+    /**
+     * Returns a randomly generated MAC address using the given Random object and the same
+     * OUI values as the given MacAddress.
+     *
+     * The locally assigned bit is always set to 1. The multicast bit is always set to 0.
+     *
+     * @param base a base MacAddress whose OUI is used for generating the random address.
+     * @param r a standard Java Random object used for generating the random address.
+     * @return a random locally assigned MacAddress.
+     *
+     * @hide
+     */
+    public static @NonNull MacAddress createRandomUnicastAddress(MacAddress base, Random r) {
+        long addr = (base.mAddr & OUI_MASK) | (NIC_MASK & r.nextLong());
+        addr |= LOCALLY_ASSIGNED_MASK;
+        addr &= ~MULTICAST_MASK;
+        return new MacAddress(addr);
+    }
+
+    // Convenience function for working around the lack of byte literals.
+    private static byte[] addr(int... in) {
+        if (in.length != ETHER_ADDR_LEN) {
+            throw new IllegalArgumentException(Arrays.toString(in)
+                    + " was not an array with length equal to " + ETHER_ADDR_LEN);
+        }
+        byte[] out = new byte[ETHER_ADDR_LEN];
+        for (int i = 0; i < ETHER_ADDR_LEN; i++) {
+            out[i] = (byte) in[i];
+        }
+        return out;
+    }
+
+    /**
+     * Checks if this MAC Address matches the provided range.
+     *
+     * @param baseAddress MacAddress representing the base address to compare with.
+     * @param mask MacAddress representing the mask to use during comparison.
+     * @return true if this MAC Address matches the given range.
+     *
+     * @hide
+     */
+    public boolean matches(@NonNull MacAddress baseAddress, @NonNull MacAddress mask) {
+        Preconditions.checkNotNull(baseAddress);
+        Preconditions.checkNotNull(mask);
+        return (mAddr & mask.mAddr) == (baseAddress.mAddr & mask.mAddr);
+    }
+
+    /**
+     * Create a link-local Inet6Address from the MAC address. The EUI-48 MAC address is converted
+     * to an EUI-64 MAC address per RFC 4291. The resulting EUI-64 is used to construct a link-local
+     * IPv6 address per RFC 4862.
+     *
+     * @return A link-local Inet6Address constructed from the MAC address.
+     * @hide
+     */
+    public @Nullable Inet6Address getLinkLocalIpv6FromEui48Mac() {
+        byte[] macEui48Bytes = toByteArray();
+        byte[] addr = new byte[16];
+
+        addr[0] = (byte) 0xfe;
+        addr[1] = (byte) 0x80;
+        addr[8] = (byte) (macEui48Bytes[0] ^ (byte) 0x02); // flip the link-local bit
+        addr[9] = macEui48Bytes[1];
+        addr[10] = macEui48Bytes[2];
+        addr[11] = (byte) 0xff;
+        addr[12] = (byte) 0xfe;
+        addr[13] = macEui48Bytes[3];
+        addr[14] = macEui48Bytes[4];
+        addr[15] = macEui48Bytes[5];
+
+        try {
+            return Inet6Address.getByAddress(null, addr, 0);
+        } catch (UnknownHostException e) {
+            return null;
+        }
+    }
+}
diff --git a/android/net/MailTo.java b/android/net/MailTo.java
new file mode 100644
index 0000000..dadb6d9
--- /dev/null
+++ b/android/net/MailTo.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2008 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 java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ *
+ * MailTo URL parser
+ *
+ * This class parses a mailto scheme URL and then can be queried for
+ * the parsed parameters. This implements RFC 2368.
+ *
+ */
+public class MailTo {
+    
+    static public final String MAILTO_SCHEME = "mailto:";
+    
+    // All the parsed content is added to the headers.
+    private HashMap<String, String> mHeaders;
+    
+    // Well known headers
+    static private final String TO = "to";
+    static private final String BODY = "body";
+    static private final String CC = "cc";
+    static private final String SUBJECT = "subject";
+
+    
+    /**
+     * Test to see if the given string is a mailto URL
+     * @param url string to be tested
+     * @return true if the string is a mailto URL
+     */
+    public static boolean isMailTo(String url) {
+        if (url != null && url.startsWith(MAILTO_SCHEME)) {
+            return true;
+        }
+        return false;
+    }
+    
+    /**
+     * Parse and decode a mailto scheme string. This parser implements
+     * RFC 2368. The returned object can be queried for the parsed parameters.
+     * @param url String containing a mailto URL
+     * @return MailTo object
+     * @exception ParseException if the scheme is not a mailto URL
+     */
+    public static MailTo parse(String url) throws ParseException {
+        if (url == null) {
+            throw new NullPointerException();
+        }
+        if (!isMailTo(url)) {
+             throw new ParseException("Not a mailto scheme");
+        }
+        // Strip the scheme as the Uri parser can't cope with it.
+        String noScheme = url.substring(MAILTO_SCHEME.length());
+        Uri email = Uri.parse(noScheme);
+        MailTo m = new MailTo();
+        
+        // Parse out the query parameters
+        String query = email.getQuery();
+        if (query != null ) {
+            String[] queries = query.split("&");
+            for (String q : queries) {
+                String[] nameval = q.split("=");
+                if (nameval.length == 0) {
+                    continue;
+                }
+                // insert the headers with the name in lowercase so that
+                // we can easily find common headers
+                m.mHeaders.put(Uri.decode(nameval[0]).toLowerCase(Locale.ROOT),
+                        nameval.length > 1 ? Uri.decode(nameval[1]) : null);
+            }
+        }
+        
+        // Address can be specified in both the headers and just after the
+        // mailto line. Join the two together.
+        String address = email.getPath();
+        if (address != null) {
+            String addr = m.getTo();
+            if (addr != null) {
+                address += ", " + addr;
+            }
+            m.mHeaders.put(TO, address);
+        }
+        
+        return m;
+    }
+    
+    /**
+     * Retrieve the To address line from the parsed mailto URL. This could be
+     * several email address that are comma-space delimited.
+     * If no To line was specified, then null is return
+     * @return comma delimited email addresses or null
+     */
+    public String getTo() {
+        return mHeaders.get(TO);
+    }
+    
+    /**
+     * Retrieve the CC address line from the parsed mailto URL. This could be
+     * several email address that are comma-space delimited.
+     * If no CC line was specified, then null is return
+     * @return comma delimited email addresses or null
+     */
+    public String getCc() {
+        return mHeaders.get(CC);
+    }
+    
+    /**
+     * Retrieve the subject line from the parsed mailto URL.
+     * If no subject line was specified, then null is return
+     * @return subject or null
+     */
+    public String getSubject() {
+        return mHeaders.get(SUBJECT);
+    }
+    
+    /**
+     * Retrieve the body line from the parsed mailto URL.
+     * If no body line was specified, then null is return
+     * @return body or null
+     */
+    public String getBody() {
+        return mHeaders.get(BODY);
+    }
+    
+    /**
+     * Retrieve all the parsed email headers from the mailto URL
+     * @return map containing all parsed values
+     */
+    public Map<String, String> getHeaders() {
+        return mHeaders;
+    }
+    
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder(MAILTO_SCHEME);
+        sb.append('?');
+        for (Map.Entry<String,String> header : mHeaders.entrySet()) {
+            sb.append(Uri.encode(header.getKey()));
+            sb.append('=');
+            sb.append(Uri.encode(header.getValue()));
+            sb.append('&');
+        }
+        return sb.toString();
+    }
+    
+    /**
+     * Private constructor. The only way to build a Mailto object is through
+     * the parse() method.
+     */
+    private MailTo() {
+        mHeaders = new HashMap<String, String>();
+    }
+}
diff --git a/android/net/MatchAllNetworkSpecifier.java b/android/net/MatchAllNetworkSpecifier.java
new file mode 100644
index 0000000..ab4f627
--- /dev/null
+++ b/android/net/MatchAllNetworkSpecifier.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * MatchAllNetworkSpecifier is a marker class used by NetworkFactory classes to indicate
+ * that they accept (match) any network specifier in requests.
+ *
+ * The class must never be used as part of a network request (those semantics aren't specified).
+ *
+ * @hide
+ */
+public final class MatchAllNetworkSpecifier extends NetworkSpecifier implements Parcelable {
+    /**
+     * Utility method which verifies that the ns argument is not a MatchAllNetworkSpecifier and
+     * throws an IllegalArgumentException if it is.
+     */
+    public static void checkNotMatchAllNetworkSpecifier(NetworkSpecifier ns) {
+        if (ns instanceof MatchAllNetworkSpecifier) {
+            throw new IllegalArgumentException("A MatchAllNetworkSpecifier is not permitted");
+        }
+    }
+
+    public boolean satisfiedBy(NetworkSpecifier other) {
+        /*
+         * The method is called by a NetworkRequest to see if it is satisfied by a proposed
+         * network (e.g. as offered by a network factory). Since MatchAllNetweorkSpecifier must
+         * not be used in network requests this method should never be called.
+         */
+        throw new IllegalStateException(
+                "MatchAllNetworkSpecifier must not be used in NetworkRequests");
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        return o instanceof MatchAllNetworkSpecifier;
+    }
+
+    @Override
+    public int hashCode() {
+        return 0;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        // Nothing to write.
+    }
+
+    public static final @android.annotation.NonNull Parcelable.Creator<MatchAllNetworkSpecifier> CREATOR =
+            new Parcelable.Creator<MatchAllNetworkSpecifier>() {
+        public MatchAllNetworkSpecifier createFromParcel(Parcel in) {
+            return new MatchAllNetworkSpecifier();
+        }
+        public MatchAllNetworkSpecifier[] newArray(int size) {
+            return new MatchAllNetworkSpecifier[size];
+        }
+    };
+}
diff --git a/android/net/MobileLinkQualityInfo.java b/android/net/MobileLinkQualityInfo.java
new file mode 100644
index 0000000..06c739d
--- /dev/null
+++ b/android/net/MobileLinkQualityInfo.java
@@ -0,0 +1,301 @@
+/*
+ * Copyright (C) 2013 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.UnsupportedAppUsage;
+import android.os.Parcel;
+
+/**
+ *  Class that represents useful attributes of mobile network links
+ *  such as the upload/download throughput or error rate etc.
+ *  @hide
+ */
+public class MobileLinkQualityInfo extends LinkQualityInfo {
+    // Represents TelephonyManager.NetworkType
+    private int mMobileNetworkType = UNKNOWN_INT;
+    private int mRssi = UNKNOWN_INT;
+    private int mGsmErrorRate = UNKNOWN_INT;
+    private int mCdmaDbm = UNKNOWN_INT;
+    private int mCdmaEcio = UNKNOWN_INT;
+    private int mEvdoDbm = UNKNOWN_INT;
+    private int mEvdoEcio = UNKNOWN_INT;
+    private int mEvdoSnr = UNKNOWN_INT;
+    private int mLteSignalStrength = UNKNOWN_INT;
+    private int mLteRsrp = UNKNOWN_INT;
+    private int mLteRsrq = UNKNOWN_INT;
+    private int mLteRssnr = UNKNOWN_INT;
+    private int mLteCqi = UNKNOWN_INT;
+
+    /**
+     * Implement the Parcelable interface.
+     * @hide
+     */
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        super.writeToParcel(dest, flags, OBJECT_TYPE_MOBILE_LINK_QUALITY_INFO);
+
+        dest.writeInt(mMobileNetworkType);
+        dest.writeInt(mRssi);
+        dest.writeInt(mGsmErrorRate);
+        dest.writeInt(mCdmaDbm);
+        dest.writeInt(mCdmaEcio);
+        dest.writeInt(mEvdoDbm);
+        dest.writeInt(mEvdoEcio);
+        dest.writeInt(mEvdoSnr);
+        dest.writeInt(mLteSignalStrength);
+        dest.writeInt(mLteRsrp);
+        dest.writeInt(mLteRsrq);
+        dest.writeInt(mLteRssnr);
+        dest.writeInt(mLteCqi);
+    }
+
+    /* Un-parceling helper */
+    /**
+     * @hide
+     */
+    public static MobileLinkQualityInfo createFromParcelBody(Parcel in) {
+
+        MobileLinkQualityInfo li = new MobileLinkQualityInfo();
+
+        li.initializeFromParcel(in);
+
+        li.mMobileNetworkType = in.readInt();
+        li.mRssi = in.readInt();
+        li.mGsmErrorRate = in.readInt();
+        li.mCdmaDbm = in.readInt();
+        li.mCdmaEcio = in.readInt();
+        li.mEvdoDbm = in.readInt();
+        li.mEvdoEcio = in.readInt();
+        li.mEvdoSnr = in.readInt();
+        li.mLteSignalStrength = in.readInt();
+        li.mLteRsrp = in.readInt();
+        li.mLteRsrq = in.readInt();
+        li.mLteRssnr = in.readInt();
+        li.mLteCqi = in.readInt();
+
+        return li;
+    }
+
+    /**
+     * returns mobile network type as defined by {@link android.telephony.TelephonyManager}
+     * @return network type or {@link android.net.LinkQualityInfo#UNKNOWN_INT}
+     */
+    @UnsupportedAppUsage
+    public int getMobileNetworkType() {
+        return mMobileNetworkType;
+    }
+
+    /**
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public void setMobileNetworkType(int mobileNetworkType) {
+        mMobileNetworkType = mobileNetworkType;
+    }
+
+    /**
+     * returns signal strength for GSM networks
+     * @return signal strength in db or {@link android.net.LinkQualityInfo#UNKNOWN_INT}
+     */
+    public int getRssi() {
+        return mRssi;
+    }
+
+    /**
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public void setRssi(int Rssi) {
+        mRssi = Rssi;
+    }
+
+    /**
+     * returns error rates for GSM networks
+     * @return error rate or {@link android.net.LinkQualityInfo#UNKNOWN_INT}
+     */
+    public int getGsmErrorRate() {
+        return mGsmErrorRate;
+    }
+
+    /**
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public void setGsmErrorRate(int gsmErrorRate) {
+        mGsmErrorRate = gsmErrorRate;
+    }
+
+    /**
+     * returns signal strength for CDMA networks
+     * @return signal strength in db or {@link android.net.LinkQualityInfo#UNKNOWN_INT}
+     */
+    public int getCdmaDbm() {
+        return mCdmaDbm;
+    }
+
+    /**
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public void setCdmaDbm(int cdmaDbm) {
+        mCdmaDbm = cdmaDbm;
+    }
+
+    /**
+     * returns signal to noise ratio for CDMA networks
+     * @return signal to noise ratio in db or {@link android.net.LinkQualityInfo#UNKNOWN_INT}
+     */
+    public int getCdmaEcio() {
+        return mCdmaEcio;
+    }
+
+    /**
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public void setCdmaEcio(int cdmaEcio) {
+        mCdmaEcio = cdmaEcio;
+    }
+
+    /**
+     * returns signal strength for EVDO networks
+     * @return signal strength in db or {@link android.net.LinkQualityInfo#UNKNOWN_INT}
+     */
+    public int getEvdoDbm() {
+        return mEvdoDbm;
+    }
+
+    /**
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public void setEvdoDbm(int evdoDbm) {
+        mEvdoDbm = evdoDbm;
+    }
+
+    /**
+     * returns signal to noise ratio for EVDO spectrum
+     * @return signal to noise ration in db or {@link android.net.LinkQualityInfo#UNKNOWN_INT}
+     */
+    public int getEvdoEcio() {
+        return mEvdoEcio;
+    }
+
+    /**
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public void setEvdoEcio(int evdoEcio) {
+        mEvdoEcio = evdoEcio;
+    }
+
+    /**
+     * returns end-to-end signal to noise ratio for EVDO networks
+     * @return signal to noise ration in db or {@link android.net.LinkQualityInfo#UNKNOWN_INT}
+     */
+    public int getEvdoSnr() {
+        return mEvdoSnr;
+    }
+
+    /**
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public void setEvdoSnr(int evdoSnr) {
+        mEvdoSnr = evdoSnr;
+    }
+
+    /**
+     * returns signal strength for LTE network
+     * @return signal strength in db or {@link android.net.LinkQualityInfo#UNKNOWN_INT}
+     */
+    public int getLteSignalStrength() {
+        return mLteSignalStrength;
+    }
+
+    /**
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public void setLteSignalStrength(int lteSignalStrength) {
+        mLteSignalStrength = lteSignalStrength;
+    }
+
+    /**
+     * returns RSRP (Reference Signal Received Power) for LTE network
+     * @return RSRP in db or {@link android.net.LinkQualityInfo#UNKNOWN_INT}
+     */
+    public int getLteRsrp() {
+        return mLteRsrp;
+    }
+
+    /**
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public void setLteRsrp(int lteRsrp) {
+        mLteRsrp = lteRsrp;
+    }
+
+    /**
+     * returns RSRQ (Reference Signal Received Quality) for LTE network
+     * @return RSRQ ??? or {@link android.net.LinkQualityInfo#UNKNOWN_INT}
+     */
+    public int getLteRsrq() {
+        return mLteRsrq;
+    }
+
+    /**
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public void setLteRsrq(int lteRsrq) {
+        mLteRsrq = lteRsrq;
+    }
+
+    /**
+     * returns signal to noise ratio for LTE networks
+     * @return signal to noise ration in db or {@link android.net.LinkQualityInfo#UNKNOWN_INT}
+     */
+    public int getLteRssnr() {
+        return mLteRssnr;
+    }
+
+    /**
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public void setLteRssnr(int lteRssnr) {
+        mLteRssnr = lteRssnr;
+    }
+
+    /**
+     * returns channel quality indicator for LTE networks
+     * @return CQI or {@link android.net.LinkQualityInfo#UNKNOWN_INT}
+     */
+    public int getLteCqi() {
+        return mLteCqi;
+    }
+
+    /**
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public void setLteCqi(int lteCqi) {
+        mLteCqi = lteCqi;
+    }
+}
diff --git a/android/net/NattKeepalivePacketData.java b/android/net/NattKeepalivePacketData.java
new file mode 100644
index 0000000..a77c244
--- /dev/null
+++ b/android/net/NattKeepalivePacketData.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright (C) 2019 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 static android.net.SocketKeepalive.ERROR_INVALID_IP_ADDRESS;
+import static android.net.SocketKeepalive.ERROR_INVALID_PORT;
+
+import android.net.SocketKeepalive.InvalidPacketException;
+import android.net.util.IpUtils;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.system.OsConstants;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/** @hide */
+public final class NattKeepalivePacketData extends KeepalivePacketData implements Parcelable {
+    // This should only be constructed via static factory methods, such as
+    // nattKeepalivePacket
+    private NattKeepalivePacketData(InetAddress srcAddress, int srcPort,
+            InetAddress dstAddress, int dstPort, byte[] data) throws
+            InvalidPacketException {
+        super(srcAddress, srcPort, dstAddress, dstPort, data);
+    }
+
+    /**
+     * Factory method to create Nat-T keepalive packet structure.
+     */
+    public static NattKeepalivePacketData nattKeepalivePacket(
+            InetAddress srcAddress, int srcPort, InetAddress dstAddress, int dstPort)
+            throws InvalidPacketException {
+
+        if (!(srcAddress instanceof Inet4Address) || !(dstAddress instanceof Inet4Address)) {
+            throw new InvalidPacketException(ERROR_INVALID_IP_ADDRESS);
+        }
+
+        if (dstPort != NattSocketKeepalive.NATT_PORT) {
+            throw new InvalidPacketException(ERROR_INVALID_PORT);
+        }
+
+        int length = IPV4_HEADER_LENGTH + UDP_HEADER_LENGTH + 1;
+        ByteBuffer buf = ByteBuffer.allocate(length);
+        buf.order(ByteOrder.BIG_ENDIAN);
+        buf.putShort((short) 0x4500);             // IP version and TOS
+        buf.putShort((short) length);
+        buf.putInt(0);                            // ID, flags, offset
+        buf.put((byte) 64);                       // TTL
+        buf.put((byte) OsConstants.IPPROTO_UDP);
+        int ipChecksumOffset = buf.position();
+        buf.putShort((short) 0);                  // IP checksum
+        buf.put(srcAddress.getAddress());
+        buf.put(dstAddress.getAddress());
+        buf.putShort((short) srcPort);
+        buf.putShort((short) dstPort);
+        buf.putShort((short) (length - 20));      // UDP length
+        int udpChecksumOffset = buf.position();
+        buf.putShort((short) 0);                  // UDP checksum
+        buf.put((byte) 0xff);                     // NAT-T keepalive
+        buf.putShort(ipChecksumOffset, IpUtils.ipChecksum(buf, 0));
+        buf.putShort(udpChecksumOffset, IpUtils.udpChecksum(buf, 0, IPV4_HEADER_LENGTH));
+
+        return new NattKeepalivePacketData(srcAddress, srcPort, dstAddress, dstPort, buf.array());
+    }
+
+    /** Parcelable Implementation */
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Write to parcel */
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeString(srcAddress.getHostAddress());
+        out.writeString(dstAddress.getHostAddress());
+        out.writeInt(srcPort);
+        out.writeInt(dstPort);
+    }
+
+    /** Parcelable Creator */
+    public static final Parcelable.Creator<NattKeepalivePacketData> CREATOR =
+            new Parcelable.Creator<NattKeepalivePacketData>() {
+                public NattKeepalivePacketData createFromParcel(Parcel in) {
+                    final InetAddress srcAddress =
+                            InetAddresses.parseNumericAddress(in.readString());
+                    final InetAddress dstAddress =
+                            InetAddresses.parseNumericAddress(in.readString());
+                    final int srcPort = in.readInt();
+                    final int dstPort = in.readInt();
+                    try {
+                        return NattKeepalivePacketData.nattKeepalivePacket(srcAddress, srcPort,
+                                    dstAddress, dstPort);
+                    } catch (InvalidPacketException e) {
+                        throw new IllegalArgumentException(
+                                "Invalid NAT-T keepalive data: " + e.error);
+                    }
+                }
+
+                public NattKeepalivePacketData[] newArray(int size) {
+                    return new NattKeepalivePacketData[size];
+                }
+            };
+}
diff --git a/android/net/NattSocketKeepalive.java b/android/net/NattSocketKeepalive.java
new file mode 100644
index 0000000..b0ce0c7
--- /dev/null
+++ b/android/net/NattSocketKeepalive.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2018 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.NonNull;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.net.InetAddress;
+import java.util.concurrent.Executor;
+
+/** @hide */
+public final class NattSocketKeepalive extends SocketKeepalive {
+    /** The NAT-T destination port for IPsec */
+    public static final int NATT_PORT = 4500;
+
+    @NonNull private final InetAddress mSource;
+    @NonNull private final InetAddress mDestination;
+    private final int mResourceId;
+
+    NattSocketKeepalive(@NonNull IConnectivityManager service,
+            @NonNull Network network,
+            @NonNull ParcelFileDescriptor pfd,
+            int resourceId,
+            @NonNull InetAddress source,
+            @NonNull InetAddress destination,
+            @NonNull Executor executor,
+            @NonNull Callback callback) {
+        super(service, network, pfd, executor, callback);
+        mSource = source;
+        mDestination = destination;
+        mResourceId = resourceId;
+    }
+
+    @Override
+    void startImpl(int intervalSec) {
+        mExecutor.execute(() -> {
+            try {
+                mService.startNattKeepaliveWithFd(mNetwork, mPfd.getFileDescriptor(), mResourceId,
+                        intervalSec, mCallback,
+                        mSource.getHostAddress(), mDestination.getHostAddress());
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error starting socket keepalive: ", e);
+                throw e.rethrowFromSystemServer();
+            }
+        });
+    }
+
+    @Override
+    void stopImpl() {
+        mExecutor.execute(() -> {
+            try {
+                if (mSlot != null) {
+                    mService.stopKeepalive(mNetwork, mSlot);
+                }
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error stopping socket keepalive: ", e);
+                throw e.rethrowFromSystemServer();
+            }
+        });
+    }
+}
diff --git a/android/net/Network.java b/android/net/Network.java
new file mode 100644
index 0000000..3f56def
--- /dev/null
+++ b/android/net/Network.java
@@ -0,0 +1,510 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.annotation.UnsupportedAppUsage;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.OsConstants;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.okhttp.internalandroidapi.Dns;
+import com.android.okhttp.internalandroidapi.HttpURLConnectionFactory;
+
+import libcore.io.IoUtils;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.MalformedURLException;
+import java.net.Socket;
+import java.net.SocketAddress;
+import java.net.SocketException;
+import java.net.URL;
+import java.net.URLConnection;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+import java.util.concurrent.TimeUnit;
+
+import javax.net.SocketFactory;
+
+/**
+ * Identifies a {@code Network}.  This is supplied to applications via
+ * {@link ConnectivityManager.NetworkCallback} in response to the active
+ * {@link ConnectivityManager#requestNetwork} or passive
+ * {@link ConnectivityManager#registerNetworkCallback} calls.
+ * It is used to direct traffic to the given {@code Network}, either on a {@link Socket} basis
+ * through a targeted {@link SocketFactory} or process-wide via
+ * {@link ConnectivityManager#bindProcessToNetwork}.
+ */
+public class Network implements Parcelable {
+
+    /**
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public final int netId;
+
+    // Objects used to perform per-network operations such as getSocketFactory
+    // and openConnection, and a lock to protect access to them.
+    private volatile NetworkBoundSocketFactory mNetworkBoundSocketFactory = null;
+    // mLock should be used to control write access to mUrlConnectionFactory.
+    // maybeInitUrlConnectionFactory() must be called prior to reading this field.
+    private volatile HttpURLConnectionFactory mUrlConnectionFactory;
+    private final Object mLock = new Object();
+
+    // Default connection pool values. These are evaluated at startup, just
+    // like the OkHttp code. Also like the OkHttp code, we will throw parse
+    // exceptions at class loading time if the properties are set but are not
+    // valid integers.
+    private static final boolean httpKeepAlive =
+            Boolean.parseBoolean(System.getProperty("http.keepAlive", "true"));
+    private static final int httpMaxConnections =
+            httpKeepAlive ? Integer.parseInt(System.getProperty("http.maxConnections", "5")) : 0;
+    private static final long httpKeepAliveDurationMs =
+            Long.parseLong(System.getProperty("http.keepAliveDuration", "300000"));  // 5 minutes.
+    // Value used to obfuscate network handle longs.
+    // The HANDLE_MAGIC value MUST be kept in sync with the corresponding
+    // value in the native/android/net.c NDK implementation.
+    private static final long HANDLE_MAGIC = 0xcafed00dL;
+    private static final int HANDLE_MAGIC_SIZE = 32;
+
+    // A boolean to control how getAllByName()/getByName() behaves in the face
+    // of Private DNS.
+    //
+    // When true, these calls will request that DNS resolution bypass any
+    // Private DNS that might otherwise apply. Use of this feature is restricted
+    // and permission checks are made by netd (attempts to bypass Private DNS
+    // without appropriate permission are silently turned into vanilla DNS
+    // requests). This only affects DNS queries made using this network object.
+    //
+    // It it not parceled to receivers because (a) it can be set or cleared at
+    // anytime and (b) receivers should be explicit about attempts to bypass
+    // Private DNS so that the intent of the code is easily determined and
+    // code search audits are possible.
+    private final transient boolean mPrivateDnsBypass;
+
+    /**
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public Network(int netId) {
+        this(netId, false);
+    }
+
+    /**
+     * @hide
+     */
+    public Network(int netId, boolean privateDnsBypass) {
+        this.netId = netId;
+        this.mPrivateDnsBypass = privateDnsBypass;
+    }
+
+    /**
+     * @hide
+     */
+    @SystemApi
+    @TestApi
+    public Network(@NonNull Network that) {
+        this(that.netId, that.mPrivateDnsBypass);
+    }
+
+    /**
+     * Operates the same as {@code InetAddress.getAllByName} except that host
+     * resolution is done on this network.
+     *
+     * @param host the hostname or literal IP string to be resolved.
+     * @return the array of addresses associated with the specified host.
+     * @throws UnknownHostException if the address lookup fails.
+     */
+    public InetAddress[] getAllByName(String host) throws UnknownHostException {
+        return InetAddress.getAllByNameOnNet(host, getNetIdForResolv());
+    }
+
+    /**
+     * Operates the same as {@code InetAddress.getByName} except that host
+     * resolution is done on this network.
+     *
+     * @param host the hostname to be resolved to an address or {@code null}.
+     * @return the {@code InetAddress} instance representing the host.
+     * @throws UnknownHostException
+     *             if the address lookup fails.
+     */
+    public InetAddress getByName(String host) throws UnknownHostException {
+        return InetAddress.getByNameOnNet(host, getNetIdForResolv());
+    }
+
+    /**
+     * Obtain a Network object for which Private DNS is to be bypassed when attempting
+     * to use {@link #getAllByName(String)}/{@link #getByName(String)} methods on the given
+     * instance for hostname resolution.
+     *
+     * @hide
+     */
+    @TestApi
+    @SystemApi
+    public @NonNull Network getPrivateDnsBypassingCopy() {
+        return new Network(netId, true);
+    }
+
+    /**
+     * Returns a netid marked with the Private DNS bypass flag.
+     *
+     * This flag must be kept in sync with the NETID_USE_LOCAL_NAMESERVERS flag
+     * in system/netd/include/NetdClient.h.
+     *
+     * @hide
+     */
+    public int getNetIdForResolv() {
+        return mPrivateDnsBypass
+                ? (int) (0x80000000L | (long) netId)  // Non-portable DNS resolution flag.
+                : netId;
+    }
+
+    /**
+     * A {@code SocketFactory} that produces {@code Socket}'s bound to this network.
+     */
+    private class NetworkBoundSocketFactory extends SocketFactory {
+        private Socket connectToHost(String host, int port, SocketAddress localAddress)
+                throws IOException {
+            // Lookup addresses only on this Network.
+            InetAddress[] hostAddresses = getAllByName(host);
+            // Try all addresses.
+            for (int i = 0; i < hostAddresses.length; i++) {
+                try {
+                    Socket socket = createSocket();
+                    boolean failed = true;
+                    try {
+                        if (localAddress != null) socket.bind(localAddress);
+                        socket.connect(new InetSocketAddress(hostAddresses[i], port));
+                        failed = false;
+                        return socket;
+                    } finally {
+                        if (failed) IoUtils.closeQuietly(socket);
+                    }
+                } catch (IOException e) {
+                    if (i == (hostAddresses.length - 1)) throw e;
+                }
+            }
+            throw new UnknownHostException(host);
+        }
+
+        @Override
+        public Socket createSocket(String host, int port, InetAddress localHost, int localPort)
+                throws IOException {
+            return connectToHost(host, port, new InetSocketAddress(localHost, localPort));
+        }
+
+        @Override
+        public Socket createSocket(InetAddress address, int port, InetAddress localAddress,
+                int localPort) throws IOException {
+            Socket socket = createSocket();
+            boolean failed = true;
+            try {
+                socket.bind(new InetSocketAddress(localAddress, localPort));
+                socket.connect(new InetSocketAddress(address, port));
+                failed = false;
+            } finally {
+                if (failed) IoUtils.closeQuietly(socket);
+            }
+            return socket;
+        }
+
+        @Override
+        public Socket createSocket(InetAddress host, int port) throws IOException {
+            Socket socket = createSocket();
+            boolean failed = true;
+            try {
+                socket.connect(new InetSocketAddress(host, port));
+                failed = false;
+            } finally {
+                if (failed) IoUtils.closeQuietly(socket);
+            }
+            return socket;
+        }
+
+        @Override
+        public Socket createSocket(String host, int port) throws IOException {
+            return connectToHost(host, port, null);
+        }
+
+        @Override
+        public Socket createSocket() throws IOException {
+            Socket socket = new Socket();
+            boolean failed = true;
+            try {
+                bindSocket(socket);
+                failed = false;
+            } finally {
+                if (failed) IoUtils.closeQuietly(socket);
+            }
+            return socket;
+        }
+    }
+
+    /**
+     * Returns a {@link SocketFactory} bound to this network.  Any {@link Socket} created by
+     * this factory will have its traffic sent over this {@code Network}.  Note that if this
+     * {@code Network} ever disconnects, this factory and any {@link Socket} it produced in the
+     * past or future will cease to work.
+     *
+     * @return a {@link SocketFactory} which produces {@link Socket} instances bound to this
+     *         {@code Network}.
+     */
+    public SocketFactory getSocketFactory() {
+        if (mNetworkBoundSocketFactory == null) {
+            synchronized (mLock) {
+                if (mNetworkBoundSocketFactory == null) {
+                    mNetworkBoundSocketFactory = new NetworkBoundSocketFactory();
+                }
+            }
+        }
+        return mNetworkBoundSocketFactory;
+    }
+
+    // TODO: This creates a connection pool and host resolver for
+    // every Network object, instead of one for every NetId. This is
+    // suboptimal, because an app could potentially have more than one
+    // Network object for the same NetId, causing increased memory footprint
+    // and performance penalties due to lack of connection reuse (connection
+    // setup time, congestion window growth time, etc.).
+    //
+    // Instead, investigate only having one connection pool and host resolver
+    // for every NetId, perhaps by using a static HashMap of NetIds to
+    // connection pools and host resolvers. The tricky part is deciding when
+    // to remove a map entry; a WeakHashMap shouldn't be used because whether
+    // a Network is referenced doesn't correlate with whether a new Network
+    // will be instantiated in the near future with the same NetID. A good
+    // solution would involve purging empty (or when all connections are timed
+    // out) ConnectionPools.
+    private void maybeInitUrlConnectionFactory() {
+        synchronized (mLock) {
+            if (mUrlConnectionFactory == null) {
+                // Set configuration on the HttpURLConnectionFactory that will be good for all
+                // connections created by this Network. Configuration that might vary is left
+                // until openConnection() and passed as arguments.
+                Dns dnsLookup = hostname -> Arrays.asList(Network.this.getAllByName(hostname));
+                HttpURLConnectionFactory urlConnectionFactory = new HttpURLConnectionFactory();
+                urlConnectionFactory.setDns(dnsLookup); // Let traffic go via dnsLookup
+                // A private connection pool just for this Network.
+                urlConnectionFactory.setNewConnectionPool(httpMaxConnections,
+                        httpKeepAliveDurationMs, TimeUnit.MILLISECONDS);
+                mUrlConnectionFactory = urlConnectionFactory;
+            }
+        }
+    }
+
+    /**
+     * Opens the specified {@link URL} on this {@code Network}, such that all traffic will be sent
+     * on this Network. The URL protocol must be {@code HTTP} or {@code HTTPS}.
+     *
+     * @return a {@code URLConnection} to the resource referred to by this URL.
+     * @throws MalformedURLException if the URL protocol is not HTTP or HTTPS.
+     * @throws IOException if an error occurs while opening the connection.
+     * @see java.net.URL#openConnection()
+     */
+    public URLConnection openConnection(URL url) throws IOException {
+        final ConnectivityManager cm = ConnectivityManager.getInstanceOrNull();
+        if (cm == null) {
+            throw new IOException("No ConnectivityManager yet constructed, please construct one");
+        }
+        // TODO: Should this be optimized to avoid fetching the global proxy for every request?
+        final ProxyInfo proxyInfo = cm.getProxyForNetwork(this);
+        final java.net.Proxy proxy;
+        if (proxyInfo != null) {
+            proxy = proxyInfo.makeProxy();
+        } else {
+            proxy = java.net.Proxy.NO_PROXY;
+        }
+        return openConnection(url, proxy);
+    }
+
+    /**
+     * Opens the specified {@link URL} on this {@code Network}, such that all traffic will be sent
+     * on this Network. The URL protocol must be {@code HTTP} or {@code HTTPS}.
+     *
+     * @param proxy the proxy through which the connection will be established.
+     * @return a {@code URLConnection} to the resource referred to by this URL.
+     * @throws MalformedURLException if the URL protocol is not HTTP or HTTPS.
+     * @throws IllegalArgumentException if the argument proxy is null.
+     * @throws IOException if an error occurs while opening the connection.
+     * @see java.net.URL#openConnection()
+     */
+    public URLConnection openConnection(URL url, java.net.Proxy proxy) throws IOException {
+        if (proxy == null) throw new IllegalArgumentException("proxy is null");
+        maybeInitUrlConnectionFactory();
+        SocketFactory socketFactory = getSocketFactory();
+        return mUrlConnectionFactory.openConnection(url, socketFactory, proxy);
+    }
+
+    /**
+     * Binds the specified {@link DatagramSocket} to this {@code Network}. All data traffic on the
+     * socket will be sent on this {@code Network}, irrespective of any process-wide network binding
+     * set by {@link ConnectivityManager#bindProcessToNetwork}. The socket must not be
+     * connected.
+     */
+    public void bindSocket(DatagramSocket socket) throws IOException {
+        // Query a property of the underlying socket to ensure that the socket's file descriptor
+        // exists, is available to bind to a network and is not closed.
+        socket.getReuseAddress();
+        bindSocket(socket.getFileDescriptor$());
+    }
+
+    /**
+     * Binds the specified {@link Socket} to this {@code Network}. All data traffic on the socket
+     * will be sent on this {@code Network}, irrespective of any process-wide network binding set by
+     * {@link ConnectivityManager#bindProcessToNetwork}. The socket must not be connected.
+     */
+    public void bindSocket(Socket socket) throws IOException {
+        // Query a property of the underlying socket to ensure that the socket's file descriptor
+        // exists, is available to bind to a network and is not closed.
+        socket.getReuseAddress();
+        bindSocket(socket.getFileDescriptor$());
+    }
+
+    /**
+     * Binds the specified {@link FileDescriptor} to this {@code Network}. All data traffic on the
+     * socket represented by this file descriptor will be sent on this {@code Network},
+     * irrespective of any process-wide network binding set by
+     * {@link ConnectivityManager#bindProcessToNetwork}. The socket must not be connected.
+     */
+    public void bindSocket(FileDescriptor fd) throws IOException {
+        try {
+            final SocketAddress peer = Os.getpeername(fd);
+            final InetAddress inetPeer = ((InetSocketAddress) peer).getAddress();
+            if (!inetPeer.isAnyLocalAddress()) {
+                // Apparently, the kernel doesn't update a connected UDP socket's
+                // routing upon mark changes.
+                throw new SocketException("Socket is connected");
+            }
+        } catch (ErrnoException e) {
+            // getpeername() failed.
+            if (e.errno != OsConstants.ENOTCONN) {
+                throw e.rethrowAsSocketException();
+            }
+        } catch (ClassCastException e) {
+            // Wasn't an InetSocketAddress.
+            throw new SocketException("Only AF_INET/AF_INET6 sockets supported");
+        }
+
+        final int err = NetworkUtils.bindSocketToNetwork(fd.getInt$(), netId);
+        if (err != 0) {
+            // bindSocketToNetwork returns negative errno.
+            throw new ErrnoException("Binding socket to network " + netId, -err)
+                    .rethrowAsSocketException();
+        }
+    }
+
+    /**
+     * Returns a {@link Network} object given a handle returned from {@link #getNetworkHandle}.
+     *
+     * @param networkHandle a handle returned from {@link #getNetworkHandle}.
+     * @return A {@link Network} object derived from {@code networkHandle}.
+     */
+    public static Network fromNetworkHandle(long networkHandle) {
+        if (networkHandle == 0) {
+            throw new IllegalArgumentException(
+                    "Network.fromNetworkHandle refusing to instantiate NETID_UNSET Network.");
+        }
+        if ((networkHandle & ((1L << HANDLE_MAGIC_SIZE) - 1)) != HANDLE_MAGIC
+                || networkHandle < 0) {
+            throw new IllegalArgumentException(
+                    "Value passed to fromNetworkHandle() is not a network handle.");
+        }
+        return new Network((int) (networkHandle >> HANDLE_MAGIC_SIZE));
+    }
+
+    /**
+     * Returns a handle representing this {@code Network}, for use with the NDK API.
+     */
+    public long getNetworkHandle() {
+        // The network handle is explicitly not the same as the netId.
+        //
+        // The netId is an implementation detail which might be changed in the
+        // future, or which alone (i.e. in the absence of some additional
+        // context) might not be sufficient to fully identify a Network.
+        //
+        // As such, the intention is to prevent accidental misuse of the API
+        // that might result if a developer assumed that handles and netIds
+        // were identical and passing a netId to a call expecting a handle
+        // "just worked".  Such accidental misuse, if widely deployed, might
+        // prevent future changes to the semantics of the netId field or
+        // inhibit the expansion of state required for Network objects.
+        //
+        // This extra layer of indirection might be seen as paranoia, and might
+        // never end up being necessary, but the added complexity is trivial.
+        // At some future date it may be desirable to realign the handle with
+        // Multiple Provisioning Domains API recommendations, as made by the
+        // IETF mif working group.
+        if (netId == 0) {
+            return 0L;  // make this zero condition obvious for debugging
+        }
+        return (((long) netId) << HANDLE_MAGIC_SIZE) | HANDLE_MAGIC;
+    }
+
+    // implement the Parcelable interface
+    public int describeContents() {
+        return 0;
+    }
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(netId);
+    }
+
+    public static final @android.annotation.NonNull Creator<Network> CREATOR =
+        new Creator<Network>() {
+            public Network createFromParcel(Parcel in) {
+                int netId = in.readInt();
+
+                return new Network(netId);
+            }
+
+            public Network[] newArray(int size) {
+                return new Network[size];
+            }
+    };
+
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof Network)) return false;
+        Network other = (Network)obj;
+        return this.netId == other.netId;
+    }
+
+    @Override
+    public int hashCode() {
+        return netId * 11;
+    }
+
+    @Override
+    public String toString() {
+        return Integer.toString(netId);
+    }
+
+    /** @hide */
+    public void writeToProto(ProtoOutputStream proto, long fieldId) {
+        final long token = proto.start(fieldId);
+        proto.write(NetworkProto.NET_ID, netId);
+        proto.end(token);
+    }
+}
diff --git a/android/net/NetworkAgent.java b/android/net/NetworkAgent.java
new file mode 100644
index 0000000..9cd4f53
--- /dev/null
+++ b/android/net/NetworkAgent.java
@@ -0,0 +1,560 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Messenger;
+import android.util.Log;
+
+import com.android.internal.util.AsyncChannel;
+import com.android.internal.util.Protocol;
+
+import java.util.ArrayList;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * A Utility class for handling for communicating between bearer-specific
+ * code and ConnectivityService.
+ *
+ * A bearer may have more than one NetworkAgent if it can simultaneously
+ * support separate networks (IMS / Internet / MMS Apns on cellular, or
+ * perhaps connections with different SSID or P2P for Wi-Fi).
+ *
+ * @hide
+ */
+public abstract class NetworkAgent extends Handler {
+    // Guaranteed to be valid (not NETID_UNSET), otherwise registerNetworkAgent() would have thrown
+    // an exception.
+    public final int netId;
+
+    private volatile AsyncChannel mAsyncChannel;
+    private final String LOG_TAG;
+    private static final boolean DBG = true;
+    private static final boolean VDBG = false;
+    private final Context mContext;
+    private final ArrayList<Message>mPreConnectedQueue = new ArrayList<Message>();
+    private volatile long mLastBwRefreshTime = 0;
+    private static final long BW_REFRESH_MIN_WIN_MS = 500;
+    private boolean mPollLceScheduled = false;
+    private AtomicBoolean mPollLcePending = new AtomicBoolean(false);
+    public final int mFactorySerialNumber;
+
+    private static final int BASE = Protocol.BASE_NETWORK_AGENT;
+
+    /**
+     * Sent by ConnectivityService to the NetworkAgent to inform it of
+     * suspected connectivity problems on its network.  The NetworkAgent
+     * should take steps to verify and correct connectivity.
+     */
+    public static final int CMD_SUSPECT_BAD = BASE;
+
+    /**
+     * Sent by the NetworkAgent (note the EVENT vs CMD prefix) to
+     * ConnectivityService to pass the current NetworkInfo (connection state).
+     * Sent when the NetworkInfo changes, mainly due to change of state.
+     * obj = NetworkInfo
+     */
+    public static final int EVENT_NETWORK_INFO_CHANGED = BASE + 1;
+
+    /**
+     * Sent by the NetworkAgent to ConnectivityService to pass the current
+     * NetworkCapabilties.
+     * obj = NetworkCapabilities
+     */
+    public static final int EVENT_NETWORK_CAPABILITIES_CHANGED = BASE + 2;
+
+    /**
+     * Sent by the NetworkAgent to ConnectivityService to pass the current
+     * NetworkProperties.
+     * obj = NetworkProperties
+     */
+    public static final int EVENT_NETWORK_PROPERTIES_CHANGED = BASE + 3;
+
+    /* centralize place where base network score, and network score scaling, will be
+     * stored, so as we can consistently compare apple and oranges, or wifi, ethernet and LTE
+     */
+    public static final int WIFI_BASE_SCORE = 60;
+
+    /**
+     * Sent by the NetworkAgent to ConnectivityService to pass the current
+     * network score.
+     * obj = network score Integer
+     */
+    public static final int EVENT_NETWORK_SCORE_CHANGED = BASE + 4;
+
+    /**
+     * Sent by ConnectivityService to the NetworkAgent to inform the agent of the
+     * networks status - whether we could use the network or could not, due to
+     * either a bad network configuration (no internet link) or captive portal.
+     *
+     * arg1 = either {@code VALID_NETWORK} or {@code INVALID_NETWORK}
+     * obj = Bundle containing map from {@code REDIRECT_URL_KEY} to {@code String}
+     *       representing URL that Internet probe was redirect to, if it was redirected,
+     *       or mapping to {@code null} otherwise.
+     */
+    public static final int CMD_REPORT_NETWORK_STATUS = BASE + 7;
+
+    public static final int VALID_NETWORK = 1;
+    public static final int INVALID_NETWORK = 2;
+
+    public static String REDIRECT_URL_KEY = "redirect URL";
+
+     /**
+     * Sent by the NetworkAgent to ConnectivityService to indicate this network was
+     * explicitly selected.  This should be sent before the NetworkInfo is marked
+     * CONNECTED so it can be given special treatment at that time.
+     *
+     * obj = boolean indicating whether to use this network even if unvalidated
+     */
+    public static final int EVENT_SET_EXPLICITLY_SELECTED = BASE + 8;
+
+    /**
+     * Sent by ConnectivityService to the NetworkAgent to inform the agent of
+     * whether the network should in the future be used even if not validated.
+     * This decision is made by the user, but it is the network transport's
+     * responsibility to remember it.
+     *
+     * arg1 = 1 if true, 0 if false
+     */
+    public static final int CMD_SAVE_ACCEPT_UNVALIDATED = BASE + 9;
+
+    /**
+     * Sent by ConnectivityService to the NetworkAgent to inform the agent to pull
+     * the underlying network connection for updated bandwidth information.
+     */
+    public static final int CMD_REQUEST_BANDWIDTH_UPDATE = BASE + 10;
+
+    /**
+     * Sent by ConnectivityService to the NetworkAgent to request that the specified packet be sent
+     * periodically on the given interval.
+     *
+     *   arg1 = the slot number of the keepalive to start
+     *   arg2 = interval in seconds
+     *   obj = KeepalivePacketData object describing the data to be sent
+     *
+     * Also used internally by ConnectivityService / KeepaliveTracker, with different semantics.
+     */
+    public static final int CMD_START_SOCKET_KEEPALIVE = BASE + 11;
+
+    /**
+     * Requests that the specified keepalive packet be stopped.
+     *
+     * arg1 = slot number of the keepalive to stop.
+     *
+     * Also used internally by ConnectivityService / KeepaliveTracker, with different semantics.
+     */
+    public static final int CMD_STOP_SOCKET_KEEPALIVE = BASE + 12;
+
+    /**
+     * Sent by the NetworkAgent to ConnectivityService to provide status on a socket keepalive
+     * request. This may either be the reply to a CMD_START_SOCKET_KEEPALIVE, or an asynchronous
+     * error notification.
+     *
+     * This is also sent by KeepaliveTracker to the app's {@link SocketKeepalive},
+     * so that the app's {@link SocketKeepalive.Callback} methods can be called.
+     *
+     * arg1 = slot number of the keepalive
+     * arg2 = error code
+     */
+    public static final int EVENT_SOCKET_KEEPALIVE = BASE + 13;
+
+    // TODO: move the above 2 constants down so they are in order once merge conflicts are resolved
+    /**
+     * Sent by the KeepaliveTracker to NetworkAgent to add a packet filter.
+     *
+     * For TCP keepalive offloads, keepalive packets are sent by the firmware. However, because the
+     * remote site will send ACK packets in response to the keepalive packets, the firmware also
+     * needs to be configured to properly filter the ACKs to prevent the system from waking up.
+     * This does not happen with UDP, so this message is TCP-specific.
+     * arg1 = slot number of the keepalive to filter for.
+     * obj = the keepalive packet to send repeatedly.
+     */
+    public static final int CMD_ADD_KEEPALIVE_PACKET_FILTER = BASE + 16;
+
+    /**
+     * Sent by the KeepaliveTracker to NetworkAgent to remove a packet filter. See
+     * {@link #CMD_ADD_KEEPALIVE_PACKET_FILTER}.
+     * arg1 = slot number of the keepalive packet filter to remove.
+     */
+    public static final int CMD_REMOVE_KEEPALIVE_PACKET_FILTER = BASE + 17;
+
+    /**
+     * Sent by ConnectivityService to inform this network transport of signal strength thresholds
+     * that when crossed should trigger a system wakeup and a NetworkCapabilities update.
+     *
+     *   obj = int[] describing signal strength thresholds.
+     */
+    public static final int CMD_SET_SIGNAL_STRENGTH_THRESHOLDS = BASE + 14;
+
+    /**
+     * Sent by ConnectivityService to the NeworkAgent to inform the agent to avoid
+     * automatically reconnecting to this network (e.g. via autojoin).  Happens
+     * when user selects "No" option on the "Stay connected?" dialog box.
+     */
+    public static final int CMD_PREVENT_AUTOMATIC_RECONNECT = BASE + 15;
+
+    // TODO : remove these two constructors. They are a stopgap measure to help sheperding a number
+    // of dependent changes that would conflict throughout the automerger graph. Having these
+    // temporarily helps with the process of going through with all these dependent changes across
+    // the entire tree.
+    public NetworkAgent(Looper looper, Context context, String logTag, NetworkInfo ni,
+            NetworkCapabilities nc, LinkProperties lp, int score) {
+        this(looper, context, logTag, ni, nc, lp, score, null, NetworkFactory.SerialNumber.NONE);
+    }
+    public NetworkAgent(Looper looper, Context context, String logTag, NetworkInfo ni,
+            NetworkCapabilities nc, LinkProperties lp, int score, NetworkMisc misc) {
+        this(looper, context, logTag, ni, nc, lp, score, misc, NetworkFactory.SerialNumber.NONE);
+    }
+
+    public NetworkAgent(Looper looper, Context context, String logTag, NetworkInfo ni,
+            NetworkCapabilities nc, LinkProperties lp, int score, int factorySerialNumber) {
+        this(looper, context, logTag, ni, nc, lp, score, null, factorySerialNumber);
+    }
+
+    public NetworkAgent(Looper looper, Context context, String logTag, NetworkInfo ni,
+            NetworkCapabilities nc, LinkProperties lp, int score, NetworkMisc misc,
+            int factorySerialNumber) {
+        super(looper);
+        LOG_TAG = logTag;
+        mContext = context;
+        mFactorySerialNumber = factorySerialNumber;
+        if (ni == null || nc == null || lp == null) {
+            throw new IllegalArgumentException();
+        }
+
+        if (VDBG) log("Registering NetworkAgent");
+        ConnectivityManager cm = (ConnectivityManager)mContext.getSystemService(
+                Context.CONNECTIVITY_SERVICE);
+        netId = cm.registerNetworkAgent(new Messenger(this), new NetworkInfo(ni),
+                new LinkProperties(lp), new NetworkCapabilities(nc), score, misc,
+                factorySerialNumber);
+    }
+
+    @Override
+    public void handleMessage(Message msg) {
+        switch (msg.what) {
+            case AsyncChannel.CMD_CHANNEL_FULL_CONNECTION: {
+                if (mAsyncChannel != null) {
+                    log("Received new connection while already connected!");
+                } else {
+                    if (VDBG) log("NetworkAgent fully connected");
+                    AsyncChannel ac = new AsyncChannel();
+                    ac.connected(null, this, msg.replyTo);
+                    ac.replyToMessage(msg, AsyncChannel.CMD_CHANNEL_FULLY_CONNECTED,
+                            AsyncChannel.STATUS_SUCCESSFUL);
+                    synchronized (mPreConnectedQueue) {
+                        mAsyncChannel = ac;
+                        for (Message m : mPreConnectedQueue) {
+                            ac.sendMessage(m);
+                        }
+                        mPreConnectedQueue.clear();
+                    }
+                }
+                break;
+            }
+            case AsyncChannel.CMD_CHANNEL_DISCONNECT: {
+                if (VDBG) log("CMD_CHANNEL_DISCONNECT");
+                if (mAsyncChannel != null) mAsyncChannel.disconnect();
+                break;
+            }
+            case AsyncChannel.CMD_CHANNEL_DISCONNECTED: {
+                if (DBG) log("NetworkAgent channel lost");
+                // let the client know CS is done with us.
+                unwanted();
+                synchronized (mPreConnectedQueue) {
+                    mAsyncChannel = null;
+                }
+                break;
+            }
+            case CMD_SUSPECT_BAD: {
+                log("Unhandled Message " + msg);
+                break;
+            }
+            case CMD_REQUEST_BANDWIDTH_UPDATE: {
+                long currentTimeMs = System.currentTimeMillis();
+                if (VDBG) {
+                    log("CMD_REQUEST_BANDWIDTH_UPDATE request received.");
+                }
+                if (currentTimeMs >= (mLastBwRefreshTime + BW_REFRESH_MIN_WIN_MS)) {
+                    mPollLceScheduled = false;
+                    if (mPollLcePending.getAndSet(true) == false) {
+                        pollLceData();
+                    }
+                } else {
+                    // deliver the request at a later time rather than discard it completely.
+                    if (!mPollLceScheduled) {
+                        long waitTime = mLastBwRefreshTime + BW_REFRESH_MIN_WIN_MS -
+                                currentTimeMs + 1;
+                        mPollLceScheduled = sendEmptyMessageDelayed(
+                                CMD_REQUEST_BANDWIDTH_UPDATE, waitTime);
+                    }
+                }
+                break;
+            }
+            case CMD_REPORT_NETWORK_STATUS: {
+                String redirectUrl = ((Bundle)msg.obj).getString(REDIRECT_URL_KEY);
+                if (VDBG) {
+                    log("CMD_REPORT_NETWORK_STATUS(" +
+                            (msg.arg1 == VALID_NETWORK ? "VALID, " : "INVALID, ") + redirectUrl);
+                }
+                networkStatus(msg.arg1, redirectUrl);
+                break;
+            }
+            case CMD_SAVE_ACCEPT_UNVALIDATED: {
+                saveAcceptUnvalidated(msg.arg1 != 0);
+                break;
+            }
+            case CMD_START_SOCKET_KEEPALIVE: {
+                startSocketKeepalive(msg);
+                break;
+            }
+            case CMD_STOP_SOCKET_KEEPALIVE: {
+                stopSocketKeepalive(msg);
+                break;
+            }
+
+            case CMD_SET_SIGNAL_STRENGTH_THRESHOLDS: {
+                ArrayList<Integer> thresholds =
+                        ((Bundle) msg.obj).getIntegerArrayList("thresholds");
+                // TODO: Change signal strength thresholds API to use an ArrayList<Integer>
+                // rather than convert to int[].
+                int[] intThresholds = new int[(thresholds != null) ? thresholds.size() : 0];
+                for (int i = 0; i < intThresholds.length; i++) {
+                    intThresholds[i] = thresholds.get(i);
+                }
+                setSignalStrengthThresholds(intThresholds);
+                break;
+            }
+            case CMD_PREVENT_AUTOMATIC_RECONNECT: {
+                preventAutomaticReconnect();
+                break;
+            }
+            case CMD_ADD_KEEPALIVE_PACKET_FILTER: {
+                addKeepalivePacketFilter(msg);
+                break;
+            }
+            case CMD_REMOVE_KEEPALIVE_PACKET_FILTER: {
+                removeKeepalivePacketFilter(msg);
+                break;
+            }
+        }
+    }
+
+    private void queueOrSendMessage(int what, Object obj) {
+        queueOrSendMessage(what, 0, 0, obj);
+    }
+
+    private void queueOrSendMessage(int what, int arg1, int arg2) {
+        queueOrSendMessage(what, arg1, arg2, null);
+    }
+
+    private void queueOrSendMessage(int what, int arg1, int arg2, Object obj) {
+        Message msg = Message.obtain();
+        msg.what = what;
+        msg.arg1 = arg1;
+        msg.arg2 = arg2;
+        msg.obj = obj;
+        queueOrSendMessage(msg);
+    }
+
+    private void queueOrSendMessage(Message msg) {
+        synchronized (mPreConnectedQueue) {
+            if (mAsyncChannel != null) {
+                mAsyncChannel.sendMessage(msg);
+            } else {
+                mPreConnectedQueue.add(msg);
+            }
+        }
+    }
+
+    /**
+     * Called by the bearer code when it has new LinkProperties data.
+     */
+    public void sendLinkProperties(LinkProperties linkProperties) {
+        queueOrSendMessage(EVENT_NETWORK_PROPERTIES_CHANGED, new LinkProperties(linkProperties));
+    }
+
+    /**
+     * Called by the bearer code when it has new NetworkInfo data.
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    public void sendNetworkInfo(NetworkInfo networkInfo) {
+        queueOrSendMessage(EVENT_NETWORK_INFO_CHANGED, new NetworkInfo(networkInfo));
+    }
+
+    /**
+     * Called by the bearer code when it has new NetworkCapabilities data.
+     */
+    public void sendNetworkCapabilities(NetworkCapabilities networkCapabilities) {
+        mPollLcePending.set(false);
+        mLastBwRefreshTime = System.currentTimeMillis();
+        queueOrSendMessage(EVENT_NETWORK_CAPABILITIES_CHANGED,
+                new NetworkCapabilities(networkCapabilities));
+    }
+
+    /**
+     * Called by the bearer code when it has a new score for this network.
+     */
+    public void sendNetworkScore(int score) {
+        if (score < 0) {
+            throw new IllegalArgumentException("Score must be >= 0");
+        }
+        queueOrSendMessage(EVENT_NETWORK_SCORE_CHANGED,  score, 0);
+    }
+
+    /**
+     * Called by the bearer to indicate this network was manually selected by the user.
+     * This should be called before the NetworkInfo is marked CONNECTED so that this
+     * Network can be given special treatment at that time. If {@code acceptUnvalidated} is
+     * {@code true}, then the system will switch to this network. If it is {@code false} and the
+     * network cannot be validated, the system will ask the user whether to switch to this network.
+     * If the user confirms and selects "don't ask again", then the system will call
+     * {@link #saveAcceptUnvalidated} to persist the user's choice. Thus, if the transport ever
+     * calls this method with {@code acceptUnvalidated} set to {@code false}, it must also implement
+     * {@link #saveAcceptUnvalidated} to respect the user's choice.
+     */
+    public void explicitlySelected(boolean acceptUnvalidated) {
+        explicitlySelected(true /* explicitlySelected */, acceptUnvalidated);
+    }
+
+    /**
+     * Called by the bearer to indicate this network was manually selected by the user.
+     * This should be called before the NetworkInfo is marked CONNECTED so that this
+     * Network can be given special treatment at that time. If {@code acceptUnvalidated} is
+     * {@code true}, then the system will switch to this network. If it is {@code false} and the
+     * network cannot be validated, the system will ask the user whether to switch to this network.
+     * If the user confirms and selects "don't ask again", then the system will call
+     * {@link #saveAcceptUnvalidated} to persist the user's choice. Thus, if the transport ever
+     * calls this method with {@code acceptUnvalidated} set to {@code false}, it must also implement
+     * {@link #saveAcceptUnvalidated} to respect the user's choice.
+     */
+    public void explicitlySelected(boolean explicitlySelected, boolean acceptUnvalidated) {
+        queueOrSendMessage(EVENT_SET_EXPLICITLY_SELECTED,
+                explicitlySelected ? 1 : 0,
+                acceptUnvalidated ? 1 : 0);
+    }
+
+    /**
+     * Called when ConnectivityService has indicated they no longer want this network.
+     * The parent factory should (previously) have received indication of the change
+     * as well, either canceling NetworkRequests or altering their score such that this
+     * network won't be immediately requested again.
+     */
+    abstract protected void unwanted();
+
+    /**
+     * Called when ConnectivityService request a bandwidth update. The parent factory
+     * shall try to overwrite this method and produce a bandwidth update if capable.
+     */
+    protected void pollLceData() {
+    }
+
+    /**
+     * Called when the system determines the usefulness of this network.
+     *
+     * Networks claiming internet connectivity will have their internet
+     * connectivity verified.
+     *
+     * Currently there are two possible values:
+     * {@code VALID_NETWORK} if the system is happy with the connection,
+     * {@code INVALID_NETWORK} if the system is not happy.
+     * TODO - add indications of captive portal-ness and related success/failure,
+     * ie, CAPTIVE_SUCCESS_NETWORK, CAPTIVE_NETWORK for successful login and detection
+     *
+     * This may be called multiple times as the network status changes and may
+     * generate false negatives if we lose ip connectivity before the link is torn down.
+     *
+     * @param status one of {@code VALID_NETWORK} or {@code INVALID_NETWORK}.
+     * @param redirectUrl If the Internet probe was redirected, this is the destination it was
+     *         redirected to, otherwise {@code null}.
+     */
+    protected void networkStatus(int status, String redirectUrl) {
+    }
+
+    /**
+     * Called when the user asks to remember the choice to use this network even if unvalidated.
+     * The transport is responsible for remembering the choice, and the next time the user connects
+     * to the network, should explicitlySelected with {@code acceptUnvalidated} set to {@code true}.
+     * This method will only be called if {@link #explicitlySelected} was called with
+     * {@code acceptUnvalidated} set to {@code false}.
+     */
+    protected void saveAcceptUnvalidated(boolean accept) {
+    }
+
+    /**
+     * Requests that the network hardware send the specified packet at the specified interval.
+     */
+    protected void startSocketKeepalive(Message msg) {
+        onSocketKeepaliveEvent(msg.arg1, SocketKeepalive.ERROR_UNSUPPORTED);
+    }
+
+    /**
+     * Requests that the network hardware send the specified packet at the specified interval.
+     */
+    protected void stopSocketKeepalive(Message msg) {
+        onSocketKeepaliveEvent(msg.arg1, SocketKeepalive.ERROR_UNSUPPORTED);
+    }
+
+    /**
+     * Called by the network when a socket keepalive event occurs.
+     */
+    public void onSocketKeepaliveEvent(int slot, int reason) {
+        queueOrSendMessage(EVENT_SOCKET_KEEPALIVE, slot, reason);
+    }
+
+    /**
+     * Called by ConnectivityService to add specific packet filter to network hardware to block
+     * ACKs matching the sent keepalive packets. Implementations that support this feature must
+     * override this method.
+     */
+    protected void addKeepalivePacketFilter(Message msg) {
+    }
+
+    /**
+     * Called by ConnectivityService to remove a packet filter installed with
+     * {@link #addKeepalivePacketFilter(Message)}. Implementations that support this feature
+     * must override this method.
+     */
+    protected void removeKeepalivePacketFilter(Message msg) {
+    }
+
+    /**
+     * Called by ConnectivityService to inform this network transport of signal strength thresholds
+     * that when crossed should trigger a system wakeup and a NetworkCapabilities update.
+     */
+    protected void setSignalStrengthThresholds(int[] thresholds) {
+    }
+
+    /**
+     * Called when the user asks to not stay connected to this network because it was found to not
+     * provide Internet access.  Usually followed by call to {@code unwanted}.  The transport is
+     * responsible for making sure the device does not automatically reconnect to the same network
+     * after the {@code unwanted} call.
+     */
+    protected void preventAutomaticReconnect() {
+    }
+
+    protected void log(String s) {
+        Log.d(LOG_TAG, "NetworkAgent: " + s);
+    }
+}
diff --git a/android/net/NetworkAgentHelper.java b/android/net/NetworkAgentHelper.java
new file mode 100644
index 0000000..7ea7665
--- /dev/null
+++ b/android/net/NetworkAgentHelper.java
@@ -0,0 +1,16 @@
+package android.net;
+
+import android.annotation.NonNull;
+
+/**
+ * Wrapper around {@link android.net.NetworkAgent} to help test coverage
+ *
+ * {@link NetworkAgent} will call non-public method unwanted() when the
+ * agent should be disabled.
+ */
+public class NetworkAgentHelper {
+    public static void callUnwanted(@NonNull NetworkAgent networkAgent) {
+        System.out.println("NetworkAgentHelper Faking unwanted() call from connectivity manager");
+        networkAgent.unwanted();
+    }
+}
diff --git a/android/net/NetworkBadging.java b/android/net/NetworkBadging.java
new file mode 100644
index 0000000..cd9ea88
--- /dev/null
+++ b/android/net/NetworkBadging.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.net;
+
+import android.annotation.DrawableRes;
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.res.Resources;
+import android.content.res.Resources.Theme;
+import android.graphics.drawable.Drawable;
+import android.net.wifi.WifiManager;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Utility methods for working with network badging.
+ *
+ * @removed
+ *
+ */
+@Deprecated
+public class NetworkBadging {
+
+    @IntDef({BADGING_NONE, BADGING_SD, BADGING_HD, BADGING_4K})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Badging {}
+
+    public static final int BADGING_NONE = 0;
+    public static final int BADGING_SD = 10;
+    public static final int BADGING_HD = 20;
+    public static final int BADGING_4K = 30;
+
+    private NetworkBadging() {}
+
+    /**
+     * Returns a Wi-Fi icon for a network with a given signal level and badging value.
+     *
+     * @param signalLevel The level returned by {@link WifiManager#calculateSignalLevel(int, int)}
+     *                    for a network. Must be between 0 and {@link WifiManager#RSSI_LEVELS}-1.
+     * @param badging  {@see NetworkBadging#Badging}, retrieved from
+     *                 {@link ScoredNetwork#calculateBadge(int)}.
+     * @param theme The theme for the current application, may be null.
+     * @return Drawable for the given icon
+     * @throws IllegalArgumentException if {@code signalLevel} is out of range or {@code badging}
+     *                                  is an invalid value
+     */
+    @NonNull public static Drawable getWifiIcon(
+            @IntRange(from=0, to=4) int signalLevel, @Badging int badging, @Nullable Theme theme) {
+        return Resources.getSystem().getDrawable(getWifiSignalResource(signalLevel), theme);
+    }
+
+    /**
+     * Returns the wifi signal resource id for the given signal level.
+     *
+     * <p>This wifi signal resource is a wifi icon to be displayed by itself when there is no badge.
+     *
+     * @param signalLevel The level returned by {@link WifiManager#calculateSignalLevel(int, int)}
+     *                    for a network. Must be between 0 and {@link WifiManager#RSSI_LEVELS}-1.
+     * @return the @DrawableRes for the icon
+     * @throws IllegalArgumentException for an invalid signal level
+     * @hide
+     */
+    @DrawableRes private static int getWifiSignalResource(int signalLevel) {
+        switch (signalLevel) {
+            case 0:
+                return com.android.internal.R.drawable.ic_wifi_signal_0;
+            case 1:
+                return com.android.internal.R.drawable.ic_wifi_signal_1;
+            case 2:
+                return com.android.internal.R.drawable.ic_wifi_signal_2;
+            case 3:
+                return com.android.internal.R.drawable.ic_wifi_signal_3;
+            case 4:
+                return com.android.internal.R.drawable.ic_wifi_signal_4;
+            default:
+                throw new IllegalArgumentException("Invalid signal level: " + signalLevel);
+        }
+    }
+}
diff --git a/android/net/NetworkCapabilities.java b/android/net/NetworkCapabilities.java
new file mode 100644
index 0000000..3e325b7
--- /dev/null
+++ b/android/net/NetworkCapabilities.java
@@ -0,0 +1,1709 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.annotation.UnsupportedAppUsage;
+import android.net.ConnectivityManager.NetworkCallback;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.ArraySet;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.BitUtils;
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+import java.util.Set;
+import java.util.StringJoiner;
+
+/**
+ * Representation of the capabilities of an active network. Instances are
+ * typically obtained through
+ * {@link NetworkCallback#onCapabilitiesChanged(Network, NetworkCapabilities)}
+ * or {@link ConnectivityManager#getNetworkCapabilities(Network)}.
+ * <p>
+ * This replaces the old {@link ConnectivityManager#TYPE_MOBILE} method of
+ * network selection. Rather than indicate a need for Wi-Fi because an
+ * application needs high bandwidth and risk obsolescence when a new, fast
+ * network appears (like LTE), the application should specify it needs high
+ * bandwidth. Similarly if an application needs an unmetered network for a bulk
+ * transfer it can specify that rather than assuming all cellular based
+ * connections are metered and all Wi-Fi based connections are not.
+ */
+public final class NetworkCapabilities implements Parcelable {
+    private static final String TAG = "NetworkCapabilities";
+    private static final int INVALID_UID = -1;
+
+    /**
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public NetworkCapabilities() {
+        clearAll();
+        mNetworkCapabilities = DEFAULT_CAPABILITIES;
+    }
+
+    public NetworkCapabilities(NetworkCapabilities nc) {
+        if (nc != null) {
+            set(nc);
+        }
+    }
+
+    /**
+     * Completely clears the contents of this object, removing even the capabilities that are set
+     * by default when the object is constructed.
+     * @hide
+     */
+    public void clearAll() {
+        mNetworkCapabilities = mTransportTypes = mUnwantedNetworkCapabilities = 0;
+        mLinkUpBandwidthKbps = mLinkDownBandwidthKbps = LINK_BANDWIDTH_UNSPECIFIED;
+        mNetworkSpecifier = null;
+        mTransportInfo = null;
+        mSignalStrength = SIGNAL_STRENGTH_UNSPECIFIED;
+        mUids = null;
+        mEstablishingVpnAppUid = INVALID_UID;
+        mSSID = null;
+    }
+
+    /**
+     * Set all contents of this object to the contents of a NetworkCapabilities.
+     * @hide
+     */
+    public void set(@NonNull NetworkCapabilities nc) {
+        mNetworkCapabilities = nc.mNetworkCapabilities;
+        mTransportTypes = nc.mTransportTypes;
+        mLinkUpBandwidthKbps = nc.mLinkUpBandwidthKbps;
+        mLinkDownBandwidthKbps = nc.mLinkDownBandwidthKbps;
+        mNetworkSpecifier = nc.mNetworkSpecifier;
+        mTransportInfo = nc.mTransportInfo;
+        mSignalStrength = nc.mSignalStrength;
+        setUids(nc.mUids); // Will make the defensive copy
+        mEstablishingVpnAppUid = nc.mEstablishingVpnAppUid;
+        mUnwantedNetworkCapabilities = nc.mUnwantedNetworkCapabilities;
+        mSSID = nc.mSSID;
+    }
+
+    /**
+     * Represents the network's capabilities.  If any are specified they will be satisfied
+     * by any Network that matches all of them.
+     */
+    @UnsupportedAppUsage
+    private long mNetworkCapabilities;
+
+    /**
+     * If any capabilities specified here they must not exist in the matching Network.
+     */
+    private long mUnwantedNetworkCapabilities;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = { "NET_CAPABILITY_" }, value = {
+            NET_CAPABILITY_MMS,
+            NET_CAPABILITY_SUPL,
+            NET_CAPABILITY_DUN,
+            NET_CAPABILITY_FOTA,
+            NET_CAPABILITY_IMS,
+            NET_CAPABILITY_CBS,
+            NET_CAPABILITY_WIFI_P2P,
+            NET_CAPABILITY_IA,
+            NET_CAPABILITY_RCS,
+            NET_CAPABILITY_XCAP,
+            NET_CAPABILITY_EIMS,
+            NET_CAPABILITY_NOT_METERED,
+            NET_CAPABILITY_INTERNET,
+            NET_CAPABILITY_NOT_RESTRICTED,
+            NET_CAPABILITY_TRUSTED,
+            NET_CAPABILITY_NOT_VPN,
+            NET_CAPABILITY_VALIDATED,
+            NET_CAPABILITY_CAPTIVE_PORTAL,
+            NET_CAPABILITY_NOT_ROAMING,
+            NET_CAPABILITY_FOREGROUND,
+            NET_CAPABILITY_NOT_CONGESTED,
+            NET_CAPABILITY_NOT_SUSPENDED,
+            NET_CAPABILITY_OEM_PAID,
+            NET_CAPABILITY_MCX,
+            NET_CAPABILITY_PARTIAL_CONNECTIVITY,
+    })
+    public @interface NetCapability { }
+
+    /**
+     * Indicates this is a network that has the ability to reach the
+     * carrier's MMSC for sending and receiving MMS messages.
+     */
+    public static final int NET_CAPABILITY_MMS            = 0;
+
+    /**
+     * Indicates this is a network that has the ability to reach the carrier's
+     * SUPL server, used to retrieve GPS information.
+     */
+    public static final int NET_CAPABILITY_SUPL           = 1;
+
+    /**
+     * Indicates this is a network that has the ability to reach the carrier's
+     * DUN or tethering gateway.
+     */
+    public static final int NET_CAPABILITY_DUN            = 2;
+
+    /**
+     * Indicates this is a network that has the ability to reach the carrier's
+     * FOTA portal, used for over the air updates.
+     */
+    public static final int NET_CAPABILITY_FOTA           = 3;
+
+    /**
+     * Indicates this is a network that has the ability to reach the carrier's
+     * IMS servers, used for network registration and signaling.
+     */
+    public static final int NET_CAPABILITY_IMS            = 4;
+
+    /**
+     * Indicates this is a network that has the ability to reach the carrier's
+     * CBS servers, used for carrier specific services.
+     */
+    public static final int NET_CAPABILITY_CBS            = 5;
+
+    /**
+     * Indicates this is a network that has the ability to reach a Wi-Fi direct
+     * peer.
+     */
+    public static final int NET_CAPABILITY_WIFI_P2P       = 6;
+
+    /**
+     * Indicates this is a network that has the ability to reach a carrier's
+     * Initial Attach servers.
+     */
+    public static final int NET_CAPABILITY_IA             = 7;
+
+    /**
+     * Indicates this is a network that has the ability to reach a carrier's
+     * RCS servers, used for Rich Communication Services.
+     */
+    public static final int NET_CAPABILITY_RCS            = 8;
+
+    /**
+     * Indicates this is a network that has the ability to reach a carrier's
+     * XCAP servers, used for configuration and control.
+     */
+    public static final int NET_CAPABILITY_XCAP           = 9;
+
+    /**
+     * Indicates this is a network that has the ability to reach a carrier's
+     * Emergency IMS servers or other services, used for network signaling
+     * during emergency calls.
+     */
+    public static final int NET_CAPABILITY_EIMS           = 10;
+
+    /**
+     * Indicates that this network is unmetered.
+     */
+    public static final int NET_CAPABILITY_NOT_METERED    = 11;
+
+    /**
+     * Indicates that this network should be able to reach the internet.
+     */
+    public static final int NET_CAPABILITY_INTERNET       = 12;
+
+    /**
+     * Indicates that this network is available for general use.  If this is not set
+     * applications should not attempt to communicate on this network.  Note that this
+     * is simply informative and not enforcement - enforcement is handled via other means.
+     * Set by default.
+     */
+    public static final int NET_CAPABILITY_NOT_RESTRICTED = 13;
+
+    /**
+     * Indicates that the user has indicated implicit trust of this network.  This
+     * generally means it's a sim-selected carrier, a plugged in ethernet, a paired
+     * BT device or a wifi the user asked to connect to.  Untrusted networks
+     * are probably limited to unknown wifi AP.  Set by default.
+     */
+    public static final int NET_CAPABILITY_TRUSTED        = 14;
+
+    /**
+     * Indicates that this network is not a VPN.  This capability is set by default and should be
+     * explicitly cleared for VPN networks.
+     */
+    public static final int NET_CAPABILITY_NOT_VPN        = 15;
+
+    /**
+     * Indicates that connectivity on this network was successfully validated. For example, for a
+     * network with NET_CAPABILITY_INTERNET, it means that Internet connectivity was successfully
+     * detected.
+     */
+    public static final int NET_CAPABILITY_VALIDATED      = 16;
+
+    /**
+     * Indicates that this network was found to have a captive portal in place last time it was
+     * probed.
+     */
+    public static final int NET_CAPABILITY_CAPTIVE_PORTAL = 17;
+
+    /**
+     * Indicates that this network is not roaming.
+     */
+    public static final int NET_CAPABILITY_NOT_ROAMING = 18;
+
+    /**
+     * Indicates that this network is available for use by apps, and not a network that is being
+     * kept up in the background to facilitate fast network switching.
+     */
+    public static final int NET_CAPABILITY_FOREGROUND = 19;
+
+    /**
+     * Indicates that this network is not congested.
+     * <p>
+     * When a network is congested, applications should defer network traffic
+     * that can be done at a later time, such as uploading analytics.
+     */
+    public static final int NET_CAPABILITY_NOT_CONGESTED = 20;
+
+    /**
+     * Indicates that this network is not currently suspended.
+     * <p>
+     * When a network is suspended, the network's IP addresses and any connections
+     * established on the network remain valid, but the network is temporarily unable
+     * to transfer data. This can happen, for example, if a cellular network experiences
+     * a temporary loss of signal, such as when driving through a tunnel, etc.
+     * A network with this capability is not suspended, so is expected to be able to
+     * transfer data.
+     */
+    public static final int NET_CAPABILITY_NOT_SUSPENDED = 21;
+
+    /**
+     * Indicates that traffic that goes through this network is paid by oem. For example,
+     * this network can be used by system apps to upload telemetry data.
+     * @hide
+     */
+    @SystemApi
+    public static final int NET_CAPABILITY_OEM_PAID = 22;
+
+    /**
+     * Indicates this is a network that has the ability to reach a carrier's Mission Critical
+     * servers.
+     */
+    public static final int NET_CAPABILITY_MCX = 23;
+
+    /**
+     * Indicates that this network was tested to only provide partial connectivity.
+     * @hide
+     */
+    @SystemApi
+    public static final int NET_CAPABILITY_PARTIAL_CONNECTIVITY = 24;
+
+    private static final int MIN_NET_CAPABILITY = NET_CAPABILITY_MMS;
+    private static final int MAX_NET_CAPABILITY = NET_CAPABILITY_PARTIAL_CONNECTIVITY;
+
+    /**
+     * Network capabilities that are expected to be mutable, i.e., can change while a particular
+     * network is connected.
+     */
+    private static final long MUTABLE_CAPABILITIES =
+            // TRUSTED can change when user explicitly connects to an untrusted network in Settings.
+            // http://b/18206275
+            (1 << NET_CAPABILITY_TRUSTED)
+            | (1 << NET_CAPABILITY_VALIDATED)
+            | (1 << NET_CAPABILITY_CAPTIVE_PORTAL)
+            | (1 << NET_CAPABILITY_NOT_ROAMING)
+            | (1 << NET_CAPABILITY_FOREGROUND)
+            | (1 << NET_CAPABILITY_NOT_CONGESTED)
+            | (1 << NET_CAPABILITY_NOT_SUSPENDED)
+            | (1 << NET_CAPABILITY_PARTIAL_CONNECTIVITY);
+
+    /**
+     * Network capabilities that are not allowed in NetworkRequests. This exists because the
+     * NetworkFactory / NetworkAgent model does not deal well with the situation where a
+     * capability's presence cannot be known in advance. If such a capability is requested, then we
+     * can get into a cycle where the NetworkFactory endlessly churns out NetworkAgents that then
+     * get immediately torn down because they do not have the requested capability.
+     */
+    private static final long NON_REQUESTABLE_CAPABILITIES =
+            MUTABLE_CAPABILITIES & ~(1 << NET_CAPABILITY_TRUSTED);
+
+    /**
+     * Capabilities that are set by default when the object is constructed.
+     */
+    private static final long DEFAULT_CAPABILITIES =
+            (1 << NET_CAPABILITY_NOT_RESTRICTED) |
+            (1 << NET_CAPABILITY_TRUSTED) |
+            (1 << NET_CAPABILITY_NOT_VPN);
+
+    /**
+     * Capabilities that suggest that a network is restricted.
+     * {@see #maybeMarkCapabilitiesRestricted}, {@see #FORCE_RESTRICTED_CAPABILITIES}
+     */
+    @VisibleForTesting
+    /* package */ static final long RESTRICTED_CAPABILITIES =
+            (1 << NET_CAPABILITY_CBS) |
+            (1 << NET_CAPABILITY_DUN) |
+            (1 << NET_CAPABILITY_EIMS) |
+            (1 << NET_CAPABILITY_FOTA) |
+            (1 << NET_CAPABILITY_IA) |
+            (1 << NET_CAPABILITY_IMS) |
+            (1 << NET_CAPABILITY_RCS) |
+            (1 << NET_CAPABILITY_XCAP) |
+            (1 << NET_CAPABILITY_MCX);
+
+    /**
+     * Capabilities that force network to be restricted.
+     * {@see #maybeMarkCapabilitiesRestricted}.
+     */
+    private static final long FORCE_RESTRICTED_CAPABILITIES =
+            (1 << NET_CAPABILITY_OEM_PAID);
+
+    /**
+     * Capabilities that suggest that a network is unrestricted.
+     * {@see #maybeMarkCapabilitiesRestricted}.
+     */
+    @VisibleForTesting
+    /* package */ static final long UNRESTRICTED_CAPABILITIES =
+            (1 << NET_CAPABILITY_INTERNET) |
+            (1 << NET_CAPABILITY_MMS) |
+            (1 << NET_CAPABILITY_SUPL) |
+            (1 << NET_CAPABILITY_WIFI_P2P);
+
+    /**
+     * Capabilities that are managed by ConnectivityService.
+     */
+    private static final long CONNECTIVITY_MANAGED_CAPABILITIES =
+            (1 << NET_CAPABILITY_VALIDATED)
+            | (1 << NET_CAPABILITY_CAPTIVE_PORTAL)
+            | (1 << NET_CAPABILITY_FOREGROUND)
+            | (1 << NET_CAPABILITY_PARTIAL_CONNECTIVITY);
+
+    /**
+     * Adds the given capability to this {@code NetworkCapability} instance.
+     * Multiple capabilities may be applied sequentially.  Note that when searching
+     * for a network to satisfy a request, all capabilities requested must be satisfied.
+     * <p>
+     * If the given capability was previously added to the list of unwanted capabilities
+     * then the capability will also be removed from the list of unwanted capabilities.
+     *
+     * @param capability the capability to be added.
+     * @return This NetworkCapabilities instance, to facilitate chaining.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public @NonNull NetworkCapabilities addCapability(@NetCapability int capability) {
+        checkValidCapability(capability);
+        mNetworkCapabilities |= 1 << capability;
+        mUnwantedNetworkCapabilities &= ~(1 << capability);  // remove from unwanted capability list
+        return this;
+    }
+
+    /**
+     * Adds the given capability to the list of unwanted capabilities of this
+     * {@code NetworkCapability} instance.  Multiple unwanted capabilities may be applied
+     * sequentially.  Note that when searching for a network to satisfy a request, the network
+     * must not contain any capability from unwanted capability list.
+     * <p>
+     * If the capability was previously added to the list of required capabilities (for
+     * example, it was there by default or added using {@link #addCapability(int)} method), then
+     * it will be removed from the list of required capabilities as well.
+     *
+     * @see #addCapability(int)
+     * @hide
+     */
+    public void addUnwantedCapability(@NetCapability int capability) {
+        checkValidCapability(capability);
+        mUnwantedNetworkCapabilities |= 1 << capability;
+        mNetworkCapabilities &= ~(1 << capability);  // remove from requested capabilities
+    }
+
+    /**
+     * Removes (if found) the given capability from this {@code NetworkCapability} instance.
+     * <p>
+     * Note that this method removes capabilities that were added via {@link #addCapability(int)},
+     * {@link #addUnwantedCapability(int)} or {@link #setCapabilities(int[], int[])} .
+     *
+     * @param capability the capability to be removed.
+     * @return This NetworkCapabilities instance, to facilitate chaining.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public @NonNull NetworkCapabilities removeCapability(@NetCapability int capability) {
+        checkValidCapability(capability);
+        final long mask = ~(1 << capability);
+        mNetworkCapabilities &= mask;
+        mUnwantedNetworkCapabilities &= mask;
+        return this;
+    }
+
+    /**
+     * Sets (or clears) the given capability on this {@link NetworkCapabilities}
+     * instance.
+     *
+     * @hide
+     */
+    public @NonNull NetworkCapabilities setCapability(@NetCapability int capability,
+            boolean value) {
+        if (value) {
+            addCapability(capability);
+        } else {
+            removeCapability(capability);
+        }
+        return this;
+    }
+
+    /**
+     * Gets all the capabilities set on this {@code NetworkCapability} instance.
+     *
+     * @return an array of capability values for this instance.
+     * @hide
+     */
+    @TestApi
+    public @NetCapability int[] getCapabilities() {
+        return BitUtils.unpackBits(mNetworkCapabilities);
+    }
+
+    /**
+     * Gets all the unwanted capabilities set on this {@code NetworkCapability} instance.
+     *
+     * @return an array of unwanted capability values for this instance.
+     * @hide
+     */
+    public @NetCapability int[] getUnwantedCapabilities() {
+        return BitUtils.unpackBits(mUnwantedNetworkCapabilities);
+    }
+
+
+    /**
+     * Sets all the capabilities set on this {@code NetworkCapability} instance.
+     * This overwrites any existing capabilities.
+     *
+     * @hide
+     */
+    public void setCapabilities(@NetCapability int[] capabilities,
+            @NetCapability int[] unwantedCapabilities) {
+        mNetworkCapabilities = BitUtils.packBits(capabilities);
+        mUnwantedNetworkCapabilities = BitUtils.packBits(unwantedCapabilities);
+    }
+
+    /**
+     * @deprecated use {@link #setCapabilities(int[], int[])}
+     * @hide
+     */
+    @Deprecated
+    public void setCapabilities(@NetCapability int[] capabilities) {
+        setCapabilities(capabilities, new int[] {});
+    }
+
+    /**
+     * Tests for the presence of a capability on this instance.
+     *
+     * @param capability the capabilities to be tested for.
+     * @return {@code true} if set on this instance.
+     */
+    public boolean hasCapability(@NetCapability int capability) {
+        return isValidCapability(capability)
+                && ((mNetworkCapabilities & (1 << capability)) != 0);
+    }
+
+    /** @hide */
+    public boolean hasUnwantedCapability(@NetCapability int capability) {
+        return isValidCapability(capability)
+                && ((mUnwantedNetworkCapabilities & (1 << capability)) != 0);
+    }
+
+    /**
+     * Check if this NetworkCapabilities has system managed capabilities or not.
+     * @hide
+     */
+    public boolean hasConnectivityManagedCapability() {
+        return ((mNetworkCapabilities & CONNECTIVITY_MANAGED_CAPABILITIES) != 0);
+    }
+
+    /** Note this method may result in having the same capability in wanted and unwanted lists. */
+    private void combineNetCapabilities(@NonNull NetworkCapabilities nc) {
+        this.mNetworkCapabilities |= nc.mNetworkCapabilities;
+        this.mUnwantedNetworkCapabilities |= nc.mUnwantedNetworkCapabilities;
+    }
+
+    /**
+     * Convenience function that returns a human-readable description of the first mutable
+     * capability we find. Used to present an error message to apps that request mutable
+     * capabilities.
+     *
+     * @hide
+     */
+    public @Nullable String describeFirstNonRequestableCapability() {
+        final long nonRequestable = (mNetworkCapabilities | mUnwantedNetworkCapabilities)
+                & NON_REQUESTABLE_CAPABILITIES;
+
+        if (nonRequestable != 0) {
+            return capabilityNameOf(BitUtils.unpackBits(nonRequestable)[0]);
+        }
+        if (mLinkUpBandwidthKbps != 0 || mLinkDownBandwidthKbps != 0) return "link bandwidth";
+        if (hasSignalStrength()) return "signalStrength";
+        return null;
+    }
+
+    private boolean satisfiedByNetCapabilities(@NonNull NetworkCapabilities nc,
+            boolean onlyImmutable) {
+        long requestedCapabilities = mNetworkCapabilities;
+        long requestedUnwantedCapabilities = mUnwantedNetworkCapabilities;
+        long providedCapabilities = nc.mNetworkCapabilities;
+
+        if (onlyImmutable) {
+            requestedCapabilities &= ~MUTABLE_CAPABILITIES;
+            requestedUnwantedCapabilities &= ~MUTABLE_CAPABILITIES;
+        }
+        return ((providedCapabilities & requestedCapabilities) == requestedCapabilities)
+                && ((requestedUnwantedCapabilities & providedCapabilities) == 0);
+    }
+
+    /** @hide */
+    public boolean equalsNetCapabilities(@NonNull NetworkCapabilities nc) {
+        return (nc.mNetworkCapabilities == this.mNetworkCapabilities)
+                && (nc.mUnwantedNetworkCapabilities == this.mUnwantedNetworkCapabilities);
+    }
+
+    private boolean equalsNetCapabilitiesRequestable(@NonNull NetworkCapabilities that) {
+        return ((this.mNetworkCapabilities & ~NON_REQUESTABLE_CAPABILITIES) ==
+                (that.mNetworkCapabilities & ~NON_REQUESTABLE_CAPABILITIES))
+                && ((this.mUnwantedNetworkCapabilities & ~NON_REQUESTABLE_CAPABILITIES) ==
+                (that.mUnwantedNetworkCapabilities & ~NON_REQUESTABLE_CAPABILITIES));
+    }
+
+    /**
+     * Removes the NET_CAPABILITY_NOT_RESTRICTED capability if all the capabilities it provides are
+     * typically provided by restricted networks.
+     *
+     * TODO: consider:
+     * - Renaming it to guessRestrictedCapability and make it set the
+     *   restricted capability bit in addition to clearing it.
+     * @hide
+     */
+    public void maybeMarkCapabilitiesRestricted() {
+        // Check if we have any capability that forces the network to be restricted.
+        final boolean forceRestrictedCapability =
+                (mNetworkCapabilities & FORCE_RESTRICTED_CAPABILITIES) != 0;
+
+        // Verify there aren't any unrestricted capabilities.  If there are we say
+        // the whole thing is unrestricted unless it is forced to be restricted.
+        final boolean hasUnrestrictedCapabilities =
+                (mNetworkCapabilities & UNRESTRICTED_CAPABILITIES) != 0;
+
+        // Must have at least some restricted capabilities.
+        final boolean hasRestrictedCapabilities =
+                (mNetworkCapabilities & RESTRICTED_CAPABILITIES) != 0;
+
+        if (forceRestrictedCapability
+                || (hasRestrictedCapabilities && !hasUnrestrictedCapabilities)) {
+            removeCapability(NET_CAPABILITY_NOT_RESTRICTED);
+        }
+    }
+
+    /**
+     * Representing the transport type.  Apps should generally not care about transport.  A
+     * request for a fast internet connection could be satisfied by a number of different
+     * transports.  If any are specified here it will be satisfied a Network that matches
+     * any of them.  If a caller doesn't care about the transport it should not specify any.
+     */
+    private long mTransportTypes;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = { "TRANSPORT_" }, value = {
+            TRANSPORT_CELLULAR,
+            TRANSPORT_WIFI,
+            TRANSPORT_BLUETOOTH,
+            TRANSPORT_ETHERNET,
+            TRANSPORT_VPN,
+            TRANSPORT_WIFI_AWARE,
+            TRANSPORT_LOWPAN,
+            TRANSPORT_TEST,
+    })
+    public @interface Transport { }
+
+    /**
+     * Indicates this network uses a Cellular transport.
+     */
+    public static final int TRANSPORT_CELLULAR = 0;
+
+    /**
+     * Indicates this network uses a Wi-Fi transport.
+     */
+    public static final int TRANSPORT_WIFI = 1;
+
+    /**
+     * Indicates this network uses a Bluetooth transport.
+     */
+    public static final int TRANSPORT_BLUETOOTH = 2;
+
+    /**
+     * Indicates this network uses an Ethernet transport.
+     */
+    public static final int TRANSPORT_ETHERNET = 3;
+
+    /**
+     * Indicates this network uses a VPN transport.
+     */
+    public static final int TRANSPORT_VPN = 4;
+
+    /**
+     * Indicates this network uses a Wi-Fi Aware transport.
+     */
+    public static final int TRANSPORT_WIFI_AWARE = 5;
+
+    /**
+     * Indicates this network uses a LoWPAN transport.
+     */
+    public static final int TRANSPORT_LOWPAN = 6;
+
+    /**
+     * Indicates this network uses a Test-only virtual interface as a transport.
+     *
+     * @hide
+     */
+    @TestApi
+    public static final int TRANSPORT_TEST = 7;
+
+    /** @hide */
+    public static final int MIN_TRANSPORT = TRANSPORT_CELLULAR;
+    /** @hide */
+    public static final int MAX_TRANSPORT = TRANSPORT_TEST;
+
+    /** @hide */
+    public static boolean isValidTransport(@Transport int transportType) {
+        return (MIN_TRANSPORT <= transportType) && (transportType <= MAX_TRANSPORT);
+    }
+
+    private static final String[] TRANSPORT_NAMES = {
+        "CELLULAR",
+        "WIFI",
+        "BLUETOOTH",
+        "ETHERNET",
+        "VPN",
+        "WIFI_AWARE",
+        "LOWPAN",
+        "TEST"
+    };
+
+    /**
+     * Adds the given transport type to this {@code NetworkCapability} instance.
+     * Multiple transports may be applied sequentially.  Note that when searching
+     * for a network to satisfy a request, any listed in the request will satisfy the request.
+     * For example {@code TRANSPORT_WIFI} and {@code TRANSPORT_ETHERNET} added to a
+     * {@code NetworkCapabilities} would cause either a Wi-Fi network or an Ethernet network
+     * to be selected.  This is logically different than
+     * {@code NetworkCapabilities.NET_CAPABILITY_*} listed above.
+     *
+     * @param transportType the transport type to be added.
+     * @return This NetworkCapabilities instance, to facilitate chaining.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public @NonNull NetworkCapabilities addTransportType(@Transport int transportType) {
+        checkValidTransportType(transportType);
+        mTransportTypes |= 1 << transportType;
+        setNetworkSpecifier(mNetworkSpecifier); // used for exception checking
+        return this;
+    }
+
+    /**
+     * Removes (if found) the given transport from this {@code NetworkCapability} instance.
+     *
+     * @param transportType the transport type to be removed.
+     * @return This NetworkCapabilities instance, to facilitate chaining.
+     * @hide
+     */
+    public @NonNull NetworkCapabilities removeTransportType(@Transport int transportType) {
+        checkValidTransportType(transportType);
+        mTransportTypes &= ~(1 << transportType);
+        setNetworkSpecifier(mNetworkSpecifier); // used for exception checking
+        return this;
+    }
+
+    /**
+     * Sets (or clears) the given transport on this {@link NetworkCapabilities}
+     * instance.
+     *
+     * @hide
+     */
+    public @NonNull NetworkCapabilities setTransportType(@Transport int transportType,
+            boolean value) {
+        if (value) {
+            addTransportType(transportType);
+        } else {
+            removeTransportType(transportType);
+        }
+        return this;
+    }
+
+    /**
+     * Gets all the transports set on this {@code NetworkCapability} instance.
+     *
+     * @return an array of transport type values for this instance.
+     * @hide
+     */
+    @TestApi
+    @SystemApi
+    @NonNull public @Transport int[] getTransportTypes() {
+        return BitUtils.unpackBits(mTransportTypes);
+    }
+
+    /**
+     * Sets all the transports set on this {@code NetworkCapability} instance.
+     * This overwrites any existing transports.
+     *
+     * @hide
+     */
+    public void setTransportTypes(@Transport int[] transportTypes) {
+        mTransportTypes = BitUtils.packBits(transportTypes);
+    }
+
+    /**
+     * Tests for the presence of a transport on this instance.
+     *
+     * @param transportType the transport type to be tested for.
+     * @return {@code true} if set on this instance.
+     */
+    public boolean hasTransport(@Transport int transportType) {
+        return isValidTransport(transportType) && ((mTransportTypes & (1 << transportType)) != 0);
+    }
+
+    private void combineTransportTypes(NetworkCapabilities nc) {
+        this.mTransportTypes |= nc.mTransportTypes;
+    }
+
+    private boolean satisfiedByTransportTypes(NetworkCapabilities nc) {
+        return ((this.mTransportTypes == 0) ||
+                ((this.mTransportTypes & nc.mTransportTypes) != 0));
+    }
+
+    /** @hide */
+    public boolean equalsTransportTypes(NetworkCapabilities nc) {
+        return (nc.mTransportTypes == this.mTransportTypes);
+    }
+
+    /**
+     * UID of the app that manages this network, or INVALID_UID if none/unknown.
+     *
+     * This field keeps track of the UID of the app that created this network and is in charge
+     * of managing it. In the practice, it is used to store the UID of VPN apps so it is named
+     * accordingly, but it may be renamed if other mechanisms are offered for third party apps
+     * to create networks.
+     *
+     * Because this field is only used in the services side (and to avoid apps being able to
+     * set this to whatever they want), this field is not parcelled and will not be conserved
+     * across the IPC boundary.
+     * @hide
+     */
+    private int mEstablishingVpnAppUid = INVALID_UID;
+
+    /**
+     * Set the UID of the managing app.
+     * @hide
+     */
+    public void setEstablishingVpnAppUid(final int uid) {
+        mEstablishingVpnAppUid = uid;
+    }
+
+    /** @hide */
+    public int getEstablishingVpnAppUid() {
+        return mEstablishingVpnAppUid;
+    }
+
+    /**
+     * Value indicating that link bandwidth is unspecified.
+     * @hide
+     */
+    public static final int LINK_BANDWIDTH_UNSPECIFIED = 0;
+
+    /**
+     * Passive link bandwidth.  This is a rough guide of the expected peak bandwidth
+     * for the first hop on the given transport.  It is not measured, but may take into account
+     * link parameters (Radio technology, allocated channels, etc).
+     */
+    private int mLinkUpBandwidthKbps = LINK_BANDWIDTH_UNSPECIFIED;
+    private int mLinkDownBandwidthKbps = LINK_BANDWIDTH_UNSPECIFIED;
+
+    /**
+     * Sets the upstream bandwidth for this network in Kbps.  This always only refers to
+     * the estimated first hop transport bandwidth.
+     * <p>
+     * Note that when used to request a network, this specifies the minimum acceptable.
+     * When received as the state of an existing network this specifies the typical
+     * first hop bandwidth expected.  This is never measured, but rather is inferred
+     * from technology type and other link parameters.  It could be used to differentiate
+     * between very slow 1xRTT cellular links and other faster networks or even between
+     * 802.11b vs 802.11AC wifi technologies.  It should not be used to differentiate between
+     * fast backhauls and slow backhauls.
+     *
+     * @param upKbps the estimated first hop upstream (device to network) bandwidth.
+     * @hide
+     */
+    public @NonNull NetworkCapabilities setLinkUpstreamBandwidthKbps(int upKbps) {
+        mLinkUpBandwidthKbps = upKbps;
+        return this;
+    }
+
+    /**
+     * Retrieves the upstream bandwidth for this network in Kbps.  This always only refers to
+     * the estimated first hop transport bandwidth.
+     *
+     * @return The estimated first hop upstream (device to network) bandwidth.
+     */
+    public int getLinkUpstreamBandwidthKbps() {
+        return mLinkUpBandwidthKbps;
+    }
+
+    /**
+     * Sets the downstream bandwidth for this network in Kbps.  This always only refers to
+     * the estimated first hop transport bandwidth.
+     * <p>
+     * Note that when used to request a network, this specifies the minimum acceptable.
+     * When received as the state of an existing network this specifies the typical
+     * first hop bandwidth expected.  This is never measured, but rather is inferred
+     * from technology type and other link parameters.  It could be used to differentiate
+     * between very slow 1xRTT cellular links and other faster networks or even between
+     * 802.11b vs 802.11AC wifi technologies.  It should not be used to differentiate between
+     * fast backhauls and slow backhauls.
+     *
+     * @param downKbps the estimated first hop downstream (network to device) bandwidth.
+     * @hide
+     */
+    public @NonNull NetworkCapabilities setLinkDownstreamBandwidthKbps(int downKbps) {
+        mLinkDownBandwidthKbps = downKbps;
+        return this;
+    }
+
+    /**
+     * Retrieves the downstream bandwidth for this network in Kbps.  This always only refers to
+     * the estimated first hop transport bandwidth.
+     *
+     * @return The estimated first hop downstream (network to device) bandwidth.
+     */
+    public int getLinkDownstreamBandwidthKbps() {
+        return mLinkDownBandwidthKbps;
+    }
+
+    private void combineLinkBandwidths(NetworkCapabilities nc) {
+        this.mLinkUpBandwidthKbps =
+                Math.max(this.mLinkUpBandwidthKbps, nc.mLinkUpBandwidthKbps);
+        this.mLinkDownBandwidthKbps =
+                Math.max(this.mLinkDownBandwidthKbps, nc.mLinkDownBandwidthKbps);
+    }
+    private boolean satisfiedByLinkBandwidths(NetworkCapabilities nc) {
+        return !(this.mLinkUpBandwidthKbps > nc.mLinkUpBandwidthKbps ||
+                this.mLinkDownBandwidthKbps > nc.mLinkDownBandwidthKbps);
+    }
+    private boolean equalsLinkBandwidths(NetworkCapabilities nc) {
+        return (this.mLinkUpBandwidthKbps == nc.mLinkUpBandwidthKbps &&
+                this.mLinkDownBandwidthKbps == nc.mLinkDownBandwidthKbps);
+    }
+    /** @hide */
+    public static int minBandwidth(int a, int b) {
+        if (a == LINK_BANDWIDTH_UNSPECIFIED)  {
+            return b;
+        } else if (b == LINK_BANDWIDTH_UNSPECIFIED) {
+            return a;
+        } else {
+            return Math.min(a, b);
+        }
+    }
+    /** @hide */
+    public static int maxBandwidth(int a, int b) {
+        return Math.max(a, b);
+    }
+
+    private NetworkSpecifier mNetworkSpecifier = null;
+    private TransportInfo mTransportInfo = null;
+
+    /**
+     * Sets the optional bearer specific network specifier.
+     * This has no meaning if a single transport is also not specified, so calling
+     * this without a single transport set will generate an exception, as will
+     * subsequently adding or removing transports after this is set.
+     * </p>
+     *
+     * @param networkSpecifier A concrete, parcelable framework class that extends
+     *                         NetworkSpecifier.
+     * @return This NetworkCapabilities instance, to facilitate chaining.
+     * @hide
+     */
+    public @NonNull NetworkCapabilities setNetworkSpecifier(NetworkSpecifier networkSpecifier) {
+        if (networkSpecifier != null && Long.bitCount(mTransportTypes) != 1) {
+            throw new IllegalStateException("Must have a single transport specified to use " +
+                    "setNetworkSpecifier");
+        }
+
+        mNetworkSpecifier = networkSpecifier;
+
+        return this;
+    }
+
+    /**
+     * Sets the optional transport specific information.
+     *
+     * @param transportInfo A concrete, parcelable framework class that extends
+     * {@link TransportInfo}.
+     * @return This NetworkCapabilities instance, to facilitate chaining.
+     * @hide
+     */
+    public @NonNull NetworkCapabilities setTransportInfo(TransportInfo transportInfo) {
+        mTransportInfo = transportInfo;
+        return this;
+    }
+
+    /**
+     * Gets the optional bearer specific network specifier. May be {@code null} if not set.
+     *
+     * @return The optional {@link NetworkSpecifier} specifying the bearer specific network
+     *         specifier or {@code null}. See {@link #setNetworkSpecifier}.
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    public @Nullable NetworkSpecifier getNetworkSpecifier() {
+        return mNetworkSpecifier;
+    }
+
+    /**
+     * Returns a transport-specific information container. The application may cast this
+     * container to a concrete sub-class based on its knowledge of the network request. The
+     * application should be able to deal with a {@code null} return value or an invalid case,
+     * e.g. use {@code instanceof} operator to verify expected type.
+     *
+     * @return A concrete implementation of the {@link TransportInfo} class or null if not
+     * available for the network.
+     */
+    @Nullable public TransportInfo getTransportInfo() {
+        return mTransportInfo;
+    }
+
+    private void combineSpecifiers(NetworkCapabilities nc) {
+        if (mNetworkSpecifier != null && !mNetworkSpecifier.equals(nc.mNetworkSpecifier)) {
+            throw new IllegalStateException("Can't combine two networkSpecifiers");
+        }
+        setNetworkSpecifier(nc.mNetworkSpecifier);
+    }
+
+    private boolean satisfiedBySpecifier(NetworkCapabilities nc) {
+        return mNetworkSpecifier == null || mNetworkSpecifier.satisfiedBy(nc.mNetworkSpecifier)
+                || nc.mNetworkSpecifier instanceof MatchAllNetworkSpecifier;
+    }
+
+    private boolean equalsSpecifier(NetworkCapabilities nc) {
+        return Objects.equals(mNetworkSpecifier, nc.mNetworkSpecifier);
+    }
+
+    private void combineTransportInfos(NetworkCapabilities nc) {
+        if (mTransportInfo != null && !mTransportInfo.equals(nc.mTransportInfo)) {
+            throw new IllegalStateException("Can't combine two TransportInfos");
+        }
+        setTransportInfo(nc.mTransportInfo);
+    }
+
+    private boolean equalsTransportInfo(NetworkCapabilities nc) {
+        return Objects.equals(mTransportInfo, nc.mTransportInfo);
+    }
+
+    /**
+     * Magic value that indicates no signal strength provided. A request specifying this value is
+     * always satisfied.
+     */
+    public static final int SIGNAL_STRENGTH_UNSPECIFIED = Integer.MIN_VALUE;
+
+    /**
+     * Signal strength. This is a signed integer, and higher values indicate better signal.
+     * The exact units are bearer-dependent. For example, Wi-Fi uses RSSI.
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+    private int mSignalStrength = SIGNAL_STRENGTH_UNSPECIFIED;
+
+    /**
+     * Sets the signal strength. This is a signed integer, with higher values indicating a stronger
+     * signal. The exact units are bearer-dependent. For example, Wi-Fi uses the same RSSI units
+     * reported by wifi code.
+     * <p>
+     * Note that when used to register a network callback, this specifies the minimum acceptable
+     * signal strength. When received as the state of an existing network it specifies the current
+     * value. A value of code SIGNAL_STRENGTH_UNSPECIFIED} means no value when received and has no
+     * effect when requesting a callback.
+     *
+     * @param signalStrength the bearer-specific signal strength.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public @NonNull NetworkCapabilities setSignalStrength(int signalStrength) {
+        mSignalStrength = signalStrength;
+        return this;
+    }
+
+    /**
+     * Returns {@code true} if this object specifies a signal strength.
+     *
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public boolean hasSignalStrength() {
+        return mSignalStrength > SIGNAL_STRENGTH_UNSPECIFIED;
+    }
+
+    /**
+     * Retrieves the signal strength.
+     *
+     * @return The bearer-specific signal strength.
+     */
+    public int getSignalStrength() {
+        return mSignalStrength;
+    }
+
+    private void combineSignalStrength(NetworkCapabilities nc) {
+        this.mSignalStrength = Math.max(this.mSignalStrength, nc.mSignalStrength);
+    }
+
+    private boolean satisfiedBySignalStrength(NetworkCapabilities nc) {
+        return this.mSignalStrength <= nc.mSignalStrength;
+    }
+
+    private boolean equalsSignalStrength(NetworkCapabilities nc) {
+        return this.mSignalStrength == nc.mSignalStrength;
+    }
+
+    /**
+     * List of UIDs this network applies to. No restriction if null.
+     * <p>
+     * For networks, mUids represent the list of network this applies to, and null means this
+     * network applies to all UIDs.
+     * For requests, mUids is the list of UIDs this network MUST apply to to match ; ALL UIDs
+     * must be included in a network so that they match. As an exception to the general rule,
+     * a null mUids field for requests mean "no requirements" rather than what the general rule
+     * would suggest ("must apply to all UIDs") : this is because this has shown to be what users
+     * of this API expect in practice. A network that must match all UIDs can still be
+     * expressed with a set ranging the entire set of possible UIDs.
+     * <p>
+     * mUids is typically (and at this time, only) used by VPN. This network is only available to
+     * the UIDs in this list, and it is their default network. Apps in this list that wish to
+     * bypass the VPN can do so iff the VPN app allows them to or if they are privileged. If this
+     * member is null, then the network is not restricted by app UID. If it's an empty list, then
+     * it means nobody can use it.
+     * As a special exception, the app managing this network (as identified by its UID stored in
+     * mEstablishingVpnAppUid) can always see this network. This is embodied by a special check in
+     * satisfiedByUids. That still does not mean the network necessarily <strong>applies</strong>
+     * to the app that manages it as determined by #appliesToUid.
+     * <p>
+     * Please note that in principle a single app can be associated with multiple UIDs because
+     * each app will have a different UID when it's run as a different (macro-)user. A single
+     * macro user can only have a single active VPN app at any given time however.
+     * <p>
+     * Also please be aware this class does not try to enforce any normalization on this. Callers
+     * can only alter the UIDs by setting them wholesale : this class does not provide any utility
+     * to add or remove individual UIDs or ranges. If callers have any normalization needs on
+     * their own (like requiring sortedness or no overlap) they need to enforce it
+     * themselves. Some of the internal methods also assume this is normalized as in no adjacent
+     * or overlapping ranges are present.
+     *
+     * @hide
+     */
+    private ArraySet<UidRange> mUids = null;
+
+    /**
+     * Convenience method to set the UIDs this network applies to to a single UID.
+     * @hide
+     */
+    public @NonNull NetworkCapabilities setSingleUid(int uid) {
+        final ArraySet<UidRange> identity = new ArraySet<>(1);
+        identity.add(new UidRange(uid, uid));
+        setUids(identity);
+        return this;
+    }
+
+    /**
+     * Set the list of UIDs this network applies to.
+     * This makes a copy of the set so that callers can't modify it after the call.
+     * @hide
+     */
+    public @NonNull NetworkCapabilities setUids(Set<UidRange> uids) {
+        if (null == uids) {
+            mUids = null;
+        } else {
+            mUids = new ArraySet<>(uids);
+        }
+        return this;
+    }
+
+    /**
+     * Get the list of UIDs this network applies to.
+     * This returns a copy of the set so that callers can't modify the original object.
+     * @hide
+     */
+    public @Nullable Set<UidRange> getUids() {
+        return null == mUids ? null : new ArraySet<>(mUids);
+    }
+
+    /**
+     * Test whether this network applies to this UID.
+     * @hide
+     */
+    public boolean appliesToUid(int uid) {
+        if (null == mUids) return true;
+        for (UidRange range : mUids) {
+            if (range.contains(uid)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Tests if the set of UIDs that this network applies to is the same as the passed network.
+     * <p>
+     * This test only checks whether equal range objects are in both sets. It will
+     * return false if the ranges are not exactly the same, even if the covered UIDs
+     * are for an equivalent result.
+     * <p>
+     * Note that this method is not very optimized, which is fine as long as it's not used very
+     * often.
+     * <p>
+     * nc is assumed nonnull.
+     *
+     * @hide
+     */
+    @VisibleForTesting
+    public boolean equalsUids(@NonNull NetworkCapabilities nc) {
+        Set<UidRange> comparedUids = nc.mUids;
+        if (null == comparedUids) return null == mUids;
+        if (null == mUids) return false;
+        // Make a copy so it can be mutated to check that all ranges in mUids
+        // also are in uids.
+        final Set<UidRange> uids = new ArraySet<>(mUids);
+        for (UidRange range : comparedUids) {
+            if (!uids.contains(range)) {
+                return false;
+            }
+            uids.remove(range);
+        }
+        return uids.isEmpty();
+    }
+
+    /**
+     * Test whether the passed NetworkCapabilities satisfies the UIDs this capabilities require.
+     *
+     * This method is called on the NetworkCapabilities embedded in a request with the
+     * capabilities of an available network. It checks whether all the UIDs from this listen
+     * (representing the UIDs that must have access to the network) are satisfied by the UIDs
+     * in the passed nc (representing the UIDs that this network is available to).
+     * <p>
+     * As a special exception, the UID that created the passed network (as represented by its
+     * mEstablishingVpnAppUid field) always satisfies a NetworkRequest requiring it (of LISTEN
+     * or REQUEST types alike), even if the network does not apply to it. That is so a VPN app
+     * can see its own network when it listens for it.
+     * <p>
+     * nc is assumed nonnull. Else, NPE.
+     * @see #appliesToUid
+     * @hide
+     */
+    public boolean satisfiedByUids(@NonNull NetworkCapabilities nc) {
+        if (null == nc.mUids || null == mUids) return true; // The network satisfies everything.
+        for (UidRange requiredRange : mUids) {
+            if (requiredRange.contains(nc.mEstablishingVpnAppUid)) return true;
+            if (!nc.appliesToUidRange(requiredRange)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Returns whether this network applies to the passed ranges.
+     * This assumes that to apply, the passed range has to be entirely contained
+     * within one of the ranges this network applies to. If the ranges are not normalized,
+     * this method may return false even though all required UIDs are covered because no
+     * single range contained them all.
+     * @hide
+     */
+    @VisibleForTesting
+    public boolean appliesToUidRange(@Nullable UidRange requiredRange) {
+        if (null == mUids) return true;
+        for (UidRange uidRange : mUids) {
+            if (uidRange.containsRange(requiredRange)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Combine the UIDs this network currently applies to with the UIDs the passed
+     * NetworkCapabilities apply to.
+     * nc is assumed nonnull.
+     */
+    private void combineUids(@NonNull NetworkCapabilities nc) {
+        if (null == nc.mUids || null == mUids) {
+            mUids = null;
+            return;
+        }
+        mUids.addAll(nc.mUids);
+    }
+
+
+    /**
+     * The SSID of the network, or null if not applicable or unknown.
+     * <p>
+     * This is filled in by wifi code.
+     * @hide
+     */
+    private String mSSID;
+
+    /**
+     * Sets the SSID of this network.
+     * @hide
+     */
+    public @NonNull NetworkCapabilities setSSID(@Nullable String ssid) {
+        mSSID = ssid;
+        return this;
+    }
+
+    /**
+     * Gets the SSID of this network, or null if none or unknown.
+     * @hide
+     */
+    public @Nullable String getSSID() {
+        return mSSID;
+    }
+
+    /**
+     * Tests if the SSID of this network is the same as the SSID of the passed network.
+     * @hide
+     */
+    public boolean equalsSSID(@NonNull NetworkCapabilities nc) {
+        return Objects.equals(mSSID, nc.mSSID);
+    }
+
+    /**
+     * Check if the SSID requirements of this object are matched by the passed object.
+     * @hide
+     */
+    public boolean satisfiedBySSID(@NonNull NetworkCapabilities nc) {
+        return mSSID == null || mSSID.equals(nc.mSSID);
+    }
+
+    /**
+     * Combine SSIDs of the capabilities.
+     * <p>
+     * This is only legal if either the SSID of this object is null, or both SSIDs are
+     * equal.
+     * @hide
+     */
+    private void combineSSIDs(@NonNull NetworkCapabilities nc) {
+        if (mSSID != null && !mSSID.equals(nc.mSSID)) {
+            throw new IllegalStateException("Can't combine two SSIDs");
+        }
+        setSSID(nc.mSSID);
+    }
+
+    /**
+     * Combine a set of Capabilities to this one.  Useful for coming up with the complete set.
+     * <p>
+     * Note that this method may break an invariant of having a particular capability in either
+     * wanted or unwanted lists but never in both.  Requests that have the same capability in
+     * both lists will never be satisfied.
+     * @hide
+     */
+    public void combineCapabilities(@NonNull NetworkCapabilities nc) {
+        combineNetCapabilities(nc);
+        combineTransportTypes(nc);
+        combineLinkBandwidths(nc);
+        combineSpecifiers(nc);
+        combineTransportInfos(nc);
+        combineSignalStrength(nc);
+        combineUids(nc);
+        combineSSIDs(nc);
+    }
+
+    /**
+     * Check if our requirements are satisfied by the given {@code NetworkCapabilities}.
+     *
+     * @param nc the {@code NetworkCapabilities} that may or may not satisfy our requirements.
+     * @param onlyImmutable if {@code true}, do not consider mutable requirements such as link
+     *         bandwidth, signal strength, or validation / captive portal status.
+     *
+     * @hide
+     */
+    private boolean satisfiedByNetworkCapabilities(NetworkCapabilities nc, boolean onlyImmutable) {
+        return (nc != null
+                && satisfiedByNetCapabilities(nc, onlyImmutable)
+                && satisfiedByTransportTypes(nc)
+                && (onlyImmutable || satisfiedByLinkBandwidths(nc))
+                && satisfiedBySpecifier(nc)
+                && (onlyImmutable || satisfiedBySignalStrength(nc))
+                && (onlyImmutable || satisfiedByUids(nc))
+                && (onlyImmutable || satisfiedBySSID(nc)));
+    }
+
+    /**
+     * Check if our requirements are satisfied by the given {@code NetworkCapabilities}.
+     *
+     * @param nc the {@code NetworkCapabilities} that may or may not satisfy our requirements.
+     *
+     * @hide
+     */
+    @TestApi
+    @SystemApi
+    public boolean satisfiedByNetworkCapabilities(@Nullable NetworkCapabilities nc) {
+        return satisfiedByNetworkCapabilities(nc, false);
+    }
+
+    /**
+     * Check if our immutable requirements are satisfied by the given {@code NetworkCapabilities}.
+     *
+     * @param nc the {@code NetworkCapabilities} that may or may not satisfy our requirements.
+     *
+     * @hide
+     */
+    public boolean satisfiedByImmutableNetworkCapabilities(@Nullable NetworkCapabilities nc) {
+        return satisfiedByNetworkCapabilities(nc, true);
+    }
+
+    /**
+     * Checks that our immutable capabilities are the same as those of the given
+     * {@code NetworkCapabilities} and return a String describing any difference.
+     * The returned String is empty if there is no difference.
+     *
+     * @hide
+     */
+    public String describeImmutableDifferences(@Nullable NetworkCapabilities that) {
+        if (that == null) {
+            return "other NetworkCapabilities was null";
+        }
+
+        StringJoiner joiner = new StringJoiner(", ");
+
+        // Ignore NOT_METERED being added or removed as it is effectively dynamic. http://b/63326103
+        // TODO: properly support NOT_METERED as a mutable and requestable capability.
+        final long mask = ~MUTABLE_CAPABILITIES & ~(1 << NET_CAPABILITY_NOT_METERED);
+        long oldImmutableCapabilities = this.mNetworkCapabilities & mask;
+        long newImmutableCapabilities = that.mNetworkCapabilities & mask;
+        if (oldImmutableCapabilities != newImmutableCapabilities) {
+            String before = capabilityNamesOf(BitUtils.unpackBits(oldImmutableCapabilities));
+            String after = capabilityNamesOf(BitUtils.unpackBits(newImmutableCapabilities));
+            joiner.add(String.format("immutable capabilities changed: %s -> %s", before, after));
+        }
+
+        if (!equalsSpecifier(that)) {
+            NetworkSpecifier before = this.getNetworkSpecifier();
+            NetworkSpecifier after = that.getNetworkSpecifier();
+            joiner.add(String.format("specifier changed: %s -> %s", before, after));
+        }
+
+        if (!equalsTransportTypes(that)) {
+            String before = transportNamesOf(this.getTransportTypes());
+            String after = transportNamesOf(that.getTransportTypes());
+            joiner.add(String.format("transports changed: %s -> %s", before, after));
+        }
+
+        return joiner.toString();
+    }
+
+    /**
+     * Checks that our requestable capabilities are the same as those of the given
+     * {@code NetworkCapabilities}.
+     *
+     * @hide
+     */
+    public boolean equalRequestableCapabilities(@Nullable NetworkCapabilities nc) {
+        if (nc == null) return false;
+        return (equalsNetCapabilitiesRequestable(nc) &&
+                equalsTransportTypes(nc) &&
+                equalsSpecifier(nc));
+    }
+
+    @Override
+    public boolean equals(@Nullable Object obj) {
+        if (obj == null || (obj instanceof NetworkCapabilities == false)) return false;
+        NetworkCapabilities that = (NetworkCapabilities) obj;
+        return (equalsNetCapabilities(that)
+                && equalsTransportTypes(that)
+                && equalsLinkBandwidths(that)
+                && equalsSignalStrength(that)
+                && equalsSpecifier(that)
+                && equalsTransportInfo(that)
+                && equalsUids(that)
+                && equalsSSID(that));
+    }
+
+    @Override
+    public int hashCode() {
+        return (int) (mNetworkCapabilities & 0xFFFFFFFF)
+                + ((int) (mNetworkCapabilities >> 32) * 3)
+                + ((int) (mUnwantedNetworkCapabilities & 0xFFFFFFFF) * 5)
+                + ((int) (mUnwantedNetworkCapabilities >> 32) * 7)
+                + ((int) (mTransportTypes & 0xFFFFFFFF) * 11)
+                + ((int) (mTransportTypes >> 32) * 13)
+                + (mLinkUpBandwidthKbps * 17)
+                + (mLinkDownBandwidthKbps * 19)
+                + Objects.hashCode(mNetworkSpecifier) * 23
+                + (mSignalStrength * 29)
+                + Objects.hashCode(mUids) * 31
+                + Objects.hashCode(mSSID) * 37
+                + Objects.hashCode(mTransportInfo) * 41;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeLong(mNetworkCapabilities);
+        dest.writeLong(mUnwantedNetworkCapabilities);
+        dest.writeLong(mTransportTypes);
+        dest.writeInt(mLinkUpBandwidthKbps);
+        dest.writeInt(mLinkDownBandwidthKbps);
+        dest.writeParcelable((Parcelable) mNetworkSpecifier, flags);
+        dest.writeParcelable((Parcelable) mTransportInfo, flags);
+        dest.writeInt(mSignalStrength);
+        dest.writeArraySet(mUids);
+        dest.writeString(mSSID);
+    }
+
+    public static final @android.annotation.NonNull Creator<NetworkCapabilities> CREATOR =
+        new Creator<NetworkCapabilities>() {
+            @Override
+            public NetworkCapabilities createFromParcel(Parcel in) {
+                NetworkCapabilities netCap = new NetworkCapabilities();
+
+                netCap.mNetworkCapabilities = in.readLong();
+                netCap.mUnwantedNetworkCapabilities = in.readLong();
+                netCap.mTransportTypes = in.readLong();
+                netCap.mLinkUpBandwidthKbps = in.readInt();
+                netCap.mLinkDownBandwidthKbps = in.readInt();
+                netCap.mNetworkSpecifier = in.readParcelable(null);
+                netCap.mTransportInfo = in.readParcelable(null);
+                netCap.mSignalStrength = in.readInt();
+                netCap.mUids = (ArraySet<UidRange>) in.readArraySet(
+                        null /* ClassLoader, null for default */);
+                netCap.mSSID = in.readString();
+                return netCap;
+            }
+            @Override
+            public NetworkCapabilities[] newArray(int size) {
+                return new NetworkCapabilities[size];
+            }
+        };
+
+    @Override
+    public @NonNull String toString() {
+        final StringBuilder sb = new StringBuilder("[");
+        if (0 != mTransportTypes) {
+            sb.append(" Transports: ");
+            appendStringRepresentationOfBitMaskToStringBuilder(sb, mTransportTypes,
+                    NetworkCapabilities::transportNameOf, "|");
+        }
+        if (0 != mNetworkCapabilities) {
+            sb.append(" Capabilities: ");
+            appendStringRepresentationOfBitMaskToStringBuilder(sb, mNetworkCapabilities,
+                    NetworkCapabilities::capabilityNameOf, "&");
+        }
+        if (0 != mUnwantedNetworkCapabilities) {
+            sb.append(" Unwanted: ");
+            appendStringRepresentationOfBitMaskToStringBuilder(sb, mUnwantedNetworkCapabilities,
+                    NetworkCapabilities::capabilityNameOf, "&");
+        }
+        if (mLinkUpBandwidthKbps > 0) {
+            sb.append(" LinkUpBandwidth>=").append(mLinkUpBandwidthKbps).append("Kbps");
+        }
+        if (mLinkDownBandwidthKbps > 0) {
+            sb.append(" LinkDnBandwidth>=").append(mLinkDownBandwidthKbps).append("Kbps");
+        }
+        if (mNetworkSpecifier != null) {
+            sb.append(" Specifier: <").append(mNetworkSpecifier).append(">");
+        }
+        if (mTransportInfo != null) {
+            sb.append(" TransportInfo: <").append(mTransportInfo).append(">");
+        }
+        if (hasSignalStrength()) {
+            sb.append(" SignalStrength: ").append(mSignalStrength);
+        }
+
+        if (null != mUids) {
+            if ((1 == mUids.size()) && (mUids.valueAt(0).count() == 1)) {
+                sb.append(" Uid: ").append(mUids.valueAt(0).start);
+            } else {
+                sb.append(" Uids: <").append(mUids).append(">");
+            }
+        }
+        if (mEstablishingVpnAppUid != INVALID_UID) {
+            sb.append(" EstablishingAppUid: ").append(mEstablishingVpnAppUid);
+        }
+
+        if (null != mSSID) {
+            sb.append(" SSID: ").append(mSSID);
+        }
+
+        sb.append("]");
+        return sb.toString();
+    }
+
+
+    private interface NameOf {
+        String nameOf(int value);
+    }
+    /**
+     * @hide
+     */
+    public static void appendStringRepresentationOfBitMaskToStringBuilder(@NonNull StringBuilder sb,
+            long bitMask, @NonNull NameOf nameFetcher, @NonNull String separator) {
+        int bitPos = 0;
+        boolean firstElementAdded = false;
+        while (bitMask != 0) {
+            if ((bitMask & 1) != 0) {
+                if (firstElementAdded) {
+                    sb.append(separator);
+                } else {
+                    firstElementAdded = true;
+                }
+                sb.append(nameFetcher.nameOf(bitPos));
+            }
+            bitMask >>= 1;
+            ++bitPos;
+        }
+    }
+
+    /** @hide */
+    public void writeToProto(@NonNull ProtoOutputStream proto, long fieldId) {
+        final long token = proto.start(fieldId);
+
+        for (int transport : getTransportTypes()) {
+            proto.write(NetworkCapabilitiesProto.TRANSPORTS, transport);
+        }
+
+        for (int capability : getCapabilities()) {
+            proto.write(NetworkCapabilitiesProto.CAPABILITIES, capability);
+        }
+
+        proto.write(NetworkCapabilitiesProto.LINK_UP_BANDWIDTH_KBPS, mLinkUpBandwidthKbps);
+        proto.write(NetworkCapabilitiesProto.LINK_DOWN_BANDWIDTH_KBPS, mLinkDownBandwidthKbps);
+
+        if (mNetworkSpecifier != null) {
+            proto.write(NetworkCapabilitiesProto.NETWORK_SPECIFIER, mNetworkSpecifier.toString());
+        }
+        if (mTransportInfo != null) {
+            // TODO b/120653863: write transport-specific info to proto?
+        }
+
+        proto.write(NetworkCapabilitiesProto.CAN_REPORT_SIGNAL_STRENGTH, hasSignalStrength());
+        proto.write(NetworkCapabilitiesProto.SIGNAL_STRENGTH, mSignalStrength);
+
+        proto.end(token);
+    }
+
+    /**
+     * @hide
+     */
+    public static @NonNull String capabilityNamesOf(@Nullable @NetCapability int[] capabilities) {
+        StringJoiner joiner = new StringJoiner("|");
+        if (capabilities != null) {
+            for (int c : capabilities) {
+                joiner.add(capabilityNameOf(c));
+            }
+        }
+        return joiner.toString();
+    }
+
+    /**
+     * @hide
+     */
+    public static @NonNull String capabilityNameOf(@NetCapability int capability) {
+        switch (capability) {
+            case NET_CAPABILITY_MMS:                  return "MMS";
+            case NET_CAPABILITY_SUPL:                 return "SUPL";
+            case NET_CAPABILITY_DUN:                  return "DUN";
+            case NET_CAPABILITY_FOTA:                 return "FOTA";
+            case NET_CAPABILITY_IMS:                  return "IMS";
+            case NET_CAPABILITY_CBS:                  return "CBS";
+            case NET_CAPABILITY_WIFI_P2P:             return "WIFI_P2P";
+            case NET_CAPABILITY_IA:                   return "IA";
+            case NET_CAPABILITY_RCS:                  return "RCS";
+            case NET_CAPABILITY_XCAP:                 return "XCAP";
+            case NET_CAPABILITY_EIMS:                 return "EIMS";
+            case NET_CAPABILITY_NOT_METERED:          return "NOT_METERED";
+            case NET_CAPABILITY_INTERNET:             return "INTERNET";
+            case NET_CAPABILITY_NOT_RESTRICTED:       return "NOT_RESTRICTED";
+            case NET_CAPABILITY_TRUSTED:              return "TRUSTED";
+            case NET_CAPABILITY_NOT_VPN:              return "NOT_VPN";
+            case NET_CAPABILITY_VALIDATED:            return "VALIDATED";
+            case NET_CAPABILITY_CAPTIVE_PORTAL:       return "CAPTIVE_PORTAL";
+            case NET_CAPABILITY_NOT_ROAMING:          return "NOT_ROAMING";
+            case NET_CAPABILITY_FOREGROUND:           return "FOREGROUND";
+            case NET_CAPABILITY_NOT_CONGESTED:        return "NOT_CONGESTED";
+            case NET_CAPABILITY_NOT_SUSPENDED:        return "NOT_SUSPENDED";
+            case NET_CAPABILITY_OEM_PAID:             return "OEM_PAID";
+            case NET_CAPABILITY_MCX:                  return "MCX";
+            case NET_CAPABILITY_PARTIAL_CONNECTIVITY: return "PARTIAL_CONNECTIVITY";
+            default:                                  return Integer.toString(capability);
+        }
+    }
+
+    /**
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public static @NonNull String transportNamesOf(@Nullable @Transport int[] types) {
+        StringJoiner joiner = new StringJoiner("|");
+        if (types != null) {
+            for (int t : types) {
+                joiner.add(transportNameOf(t));
+            }
+        }
+        return joiner.toString();
+    }
+
+    /**
+     * @hide
+     */
+    public static @NonNull String transportNameOf(@Transport int transport) {
+        if (!isValidTransport(transport)) {
+            return "UNKNOWN";
+        }
+        return TRANSPORT_NAMES[transport];
+    }
+
+    private static void checkValidTransportType(@Transport int transport) {
+        Preconditions.checkArgument(
+                isValidTransport(transport), "Invalid TransportType " + transport);
+    }
+
+    private static boolean isValidCapability(@NetworkCapabilities.NetCapability int capability) {
+        return capability >= MIN_NET_CAPABILITY && capability <= MAX_NET_CAPABILITY;
+    }
+
+    private static void checkValidCapability(@NetworkCapabilities.NetCapability int capability) {
+        Preconditions.checkArgument(isValidCapability(capability),
+                "NetworkCapability " + capability + "out of range");
+    }
+
+    /**
+     * Check if this {@code NetworkCapability} instance is metered.
+     *
+     * @return {@code true} if {@code NET_CAPABILITY_NOT_METERED} is not set on this instance.
+     * @hide
+     */
+    public boolean isMetered() {
+        return !hasCapability(NET_CAPABILITY_NOT_METERED);
+    }
+}
diff --git a/android/net/NetworkConfig.java b/android/net/NetworkConfig.java
new file mode 100644
index 0000000..32a2cda
--- /dev/null
+++ b/android/net/NetworkConfig.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2010 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 java.util.Locale;
+
+/**
+ * Describes the buildtime configuration of a network.
+ * Holds settings read from resources.
+ * @hide
+ */
+public class NetworkConfig {
+    /**
+     * Human readable string
+     */
+    public String name;
+
+    /**
+     * Type from ConnectivityManager
+     */
+    public int type;
+
+    /**
+     * the radio number from radio attributes config
+     */
+    public int radio;
+
+    /**
+     * higher number == higher priority when turning off connections
+     */
+    public int priority;
+
+    /**
+     * indicates the boot time dependencyMet setting
+     */
+    public boolean dependencyMet;
+
+    /**
+     * indicates the default restoral timer in seconds
+     * if the network is used as a special network feature
+     * -1 indicates no restoration of default
+     */
+    public int restoreTime;
+
+    /**
+     * input string from config.xml resource.  Uses the form:
+     * [Connection name],[ConnectivityManager connection type],
+     * [associated radio-type],[priority],[dependencyMet]
+     */
+    public NetworkConfig(String init) {
+        String fragments[] = init.split(",");
+        name = fragments[0].trim().toLowerCase(Locale.ROOT);
+        type = Integer.parseInt(fragments[1]);
+        radio = Integer.parseInt(fragments[2]);
+        priority = Integer.parseInt(fragments[3]);
+        restoreTime = Integer.parseInt(fragments[4]);
+        dependencyMet = Boolean.parseBoolean(fragments[5]);
+    }
+
+    /**
+     * Indicates if this network is supposed to be default-routable
+     */
+    public boolean isDefault() {
+        return (type == radio);
+    }
+}
diff --git a/android/net/NetworkFactory.java b/android/net/NetworkFactory.java
new file mode 100644
index 0000000..5b1d12c
--- /dev/null
+++ b/android/net/NetworkFactory.java
@@ -0,0 +1,475 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.os.Build;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Messenger;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.AsyncChannel;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.internal.util.Protocol;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * A NetworkFactory is an entity that creates NetworkAgent objects.
+ * The bearers register with ConnectivityService using {@link #register} and
+ * their factory will start receiving scored NetworkRequests.  NetworkRequests
+ * can be filtered 3 ways: by NetworkCapabilities, by score and more complexly by
+ * overridden function.  All of these can be dynamic - changing NetworkCapabilities
+ * or score forces re-evaluation of all current requests.
+ *
+ * If any requests pass the filter some overrideable functions will be called.
+ * If the bearer only cares about very simple start/stopNetwork callbacks, those
+ * functions can be overridden.  If the bearer needs more interaction, it can
+ * override addNetworkRequest and removeNetworkRequest which will give it each
+ * request that passes their current filters.
+ * @hide
+ **/
+public class NetworkFactory extends Handler {
+    /** @hide */
+    public static class SerialNumber {
+        // Guard used by no network factory.
+        public static final int NONE = -1;
+        // A hardcoded serial number for NetworkAgents representing VPNs. These agents are
+        // not created by any factory, so they use this constant for clarity instead of NONE.
+        public static final int VPN = -2;
+        private static final AtomicInteger sNetworkFactorySerialNumber = new AtomicInteger(1);
+        /** Returns a unique serial number for a factory. */
+        public static final int nextSerialNumber() {
+            return sNetworkFactorySerialNumber.getAndIncrement();
+        }
+    }
+
+    private static final boolean DBG = true;
+    private static final boolean VDBG = false;
+
+    private static final int BASE = Protocol.BASE_NETWORK_FACTORY;
+    /**
+     * Pass a network request to the bearer.  If the bearer believes it can
+     * satisfy the request it should connect to the network and create a
+     * NetworkAgent.  Once the NetworkAgent is fully functional it will
+     * register itself with ConnectivityService using registerNetworkAgent.
+     * If the bearer cannot immediately satisfy the request (no network,
+     * user disabled the radio, lower-scored network) it should remember
+     * any NetworkRequests it may be able to satisfy in the future.  It may
+     * disregard any that it will never be able to service, for example
+     * those requiring a different bearer.
+     * msg.obj = NetworkRequest
+     * msg.arg1 = score - the score of the network currently satisfying this
+     *            request.  If this bearer knows in advance it cannot
+     *            exceed this score it should not try to connect, holding the request
+     *            for the future.
+     *            Note that subsequent events may give a different (lower
+     *            or higher) score for this request, transmitted to each
+     *            NetworkFactory through additional CMD_REQUEST_NETWORK msgs
+     *            with the same NetworkRequest but an updated score.
+     *            Also, network conditions may change for this bearer
+     *            allowing for a better score in the future.
+     * msg.arg2 = the serial number of the factory currently responsible for the
+     *            NetworkAgent handling this request, or SerialNumber.NONE if none.
+     */
+    public static final int CMD_REQUEST_NETWORK = BASE;
+
+    /**
+     * Cancel a network request
+     * msg.obj = NetworkRequest
+     */
+    public static final int CMD_CANCEL_REQUEST = BASE + 1;
+
+    /**
+     * Internally used to set our best-guess score.
+     * msg.arg1 = new score
+     */
+    private static final int CMD_SET_SCORE = BASE + 2;
+
+    /**
+     * Internally used to set our current filter for coarse bandwidth changes with
+     * technology changes.
+     * msg.obj = new filter
+     */
+    private static final int CMD_SET_FILTER = BASE + 3;
+
+    /**
+     * Sent by NetworkFactory to ConnectivityService to indicate that a request is
+     * unfulfillable.
+     * @see #releaseRequestAsUnfulfillableByAnyFactory(NetworkRequest).
+     */
+    public static final int EVENT_UNFULFILLABLE_REQUEST = BASE + 4;
+
+    private final Context mContext;
+    private final ArrayList<Message> mPreConnectedQueue = new ArrayList<Message>();
+    private AsyncChannel mAsyncChannel;
+    private final String LOG_TAG;
+
+    private final SparseArray<NetworkRequestInfo> mNetworkRequests =
+            new SparseArray<NetworkRequestInfo>();
+
+    private int mScore;
+    private NetworkCapabilities mCapabilityFilter;
+
+    private int mRefCount = 0;
+    private Messenger mMessenger = null;
+    private int mSerialNumber;
+
+    @UnsupportedAppUsage
+    public NetworkFactory(Looper looper, Context context, String logTag,
+            NetworkCapabilities filter) {
+        super(looper);
+        LOG_TAG = logTag;
+        mContext = context;
+        mCapabilityFilter = filter;
+    }
+
+    public void register() {
+        if (DBG) log("Registering NetworkFactory");
+        if (mMessenger == null) {
+            mMessenger = new Messenger(this);
+            mSerialNumber = ConnectivityManager.from(mContext).registerNetworkFactory(mMessenger,
+                    LOG_TAG);
+        }
+    }
+
+    public void unregister() {
+        if (DBG) log("Unregistering NetworkFactory");
+        if (mMessenger != null) {
+            ConnectivityManager.from(mContext).unregisterNetworkFactory(mMessenger);
+            mMessenger = null;
+        }
+    }
+
+    @Override
+    public void handleMessage(Message msg) {
+        switch (msg.what) {
+            case AsyncChannel.CMD_CHANNEL_FULL_CONNECTION: {
+                if (mAsyncChannel != null) {
+                    log("Received new connection while already connected!");
+                    break;
+                }
+                if (VDBG) log("NetworkFactory fully connected");
+                AsyncChannel ac = new AsyncChannel();
+                ac.connected(null, this, msg.replyTo);
+                ac.replyToMessage(msg, AsyncChannel.CMD_CHANNEL_FULLY_CONNECTED,
+                        AsyncChannel.STATUS_SUCCESSFUL);
+                mAsyncChannel = ac;
+                for (Message m : mPreConnectedQueue) {
+                    ac.sendMessage(m);
+                }
+                mPreConnectedQueue.clear();
+                break;
+            }
+            case AsyncChannel.CMD_CHANNEL_DISCONNECT: {
+                if (VDBG) log("CMD_CHANNEL_DISCONNECT");
+                if (mAsyncChannel != null) {
+                    mAsyncChannel.disconnect();
+                    mAsyncChannel = null;
+                }
+                break;
+            }
+            case AsyncChannel.CMD_CHANNEL_DISCONNECTED: {
+                if (DBG) log("NetworkFactory channel lost");
+                mAsyncChannel = null;
+                break;
+            }
+            case CMD_REQUEST_NETWORK: {
+                handleAddRequest((NetworkRequest) msg.obj, msg.arg1, msg.arg2);
+                break;
+            }
+            case CMD_CANCEL_REQUEST: {
+                handleRemoveRequest((NetworkRequest) msg.obj);
+                break;
+            }
+            case CMD_SET_SCORE: {
+                handleSetScore(msg.arg1);
+                break;
+            }
+            case CMD_SET_FILTER: {
+                handleSetFilter((NetworkCapabilities) msg.obj);
+                break;
+            }
+        }
+    }
+
+    private class NetworkRequestInfo {
+        public final NetworkRequest request;
+        public int score;
+        public boolean requested; // do we have a request outstanding, limited by score
+        public int factorySerialNumber;
+
+        NetworkRequestInfo(NetworkRequest request, int score, int factorySerialNumber) {
+            this.request = request;
+            this.score = score;
+            this.requested = false;
+            this.factorySerialNumber = factorySerialNumber;
+        }
+
+        @Override
+        public String toString() {
+            return "{" + request + ", score=" + score + ", requested=" + requested + "}";
+        }
+    }
+
+    /**
+     * Add a NetworkRequest that the bearer may want to attempt to satisfy.
+     * @see #CMD_REQUEST_NETWORK
+     *
+     * @param request the request to handle.
+     * @param score the score of the NetworkAgent currently satisfying this request.
+     * @param servingFactorySerialNumber the serial number of the NetworkFactory that
+     *         created the NetworkAgent currently satisfying this request.
+     */
+    // TODO : remove this method. It is a stopgap measure to help sheperding a number
+    // of dependent changes that would conflict throughout the automerger graph. Having this
+    // temporarily helps with the process of going through with all these dependent changes across
+    // the entire tree.
+    @VisibleForTesting
+    protected void handleAddRequest(NetworkRequest request, int score) {
+        handleAddRequest(request, score, SerialNumber.NONE);
+    }
+
+    /**
+     * Add a NetworkRequest that the bearer may want to attempt to satisfy.
+     * @see #CMD_REQUEST_NETWORK
+     *
+     * @param request the request to handle.
+     * @param score the score of the NetworkAgent currently satisfying this request.
+     * @param servingFactorySerialNumber the serial number of the NetworkFactory that
+     *         created the NetworkAgent currently satisfying this request.
+     */
+    @VisibleForTesting
+    protected void handleAddRequest(NetworkRequest request, int score,
+            int servingFactorySerialNumber) {
+        NetworkRequestInfo n = mNetworkRequests.get(request.requestId);
+        if (n == null) {
+            if (DBG) {
+                log("got request " + request + " with score " + score
+                        + " and serial " + servingFactorySerialNumber);
+            }
+            n = new NetworkRequestInfo(request, score, servingFactorySerialNumber);
+            mNetworkRequests.put(n.request.requestId, n);
+        } else {
+            if (VDBG) {
+                log("new score " + score + " for exisiting request " + request
+                        + " with serial " + servingFactorySerialNumber);
+            }
+            n.score = score;
+            n.factorySerialNumber = servingFactorySerialNumber;
+        }
+        if (VDBG) log("  my score=" + mScore + ", my filter=" + mCapabilityFilter);
+
+        evalRequest(n);
+    }
+
+    @VisibleForTesting
+    protected void handleRemoveRequest(NetworkRequest request) {
+        NetworkRequestInfo n = mNetworkRequests.get(request.requestId);
+        if (n != null) {
+            mNetworkRequests.remove(request.requestId);
+            if (n.requested) releaseNetworkFor(n.request);
+        }
+    }
+
+    private void handleSetScore(int score) {
+        mScore = score;
+        evalRequests();
+    }
+
+    private void handleSetFilter(NetworkCapabilities netCap) {
+        mCapabilityFilter = netCap;
+        evalRequests();
+    }
+
+    /**
+     * Overridable function to provide complex filtering.
+     * Called for every request every time a new NetworkRequest is seen
+     * and whenever the filterScore or filterNetworkCapabilities change.
+     *
+     * acceptRequest can be overridden to provide complex filter behavior
+     * for the incoming requests
+     *
+     * For output, this class will call {@link #needNetworkFor} and
+     * {@link #releaseNetworkFor} for every request that passes the filters.
+     * If you don't need to see every request, you can leave the base
+     * implementations of those two functions and instead override
+     * {@link #startNetwork} and {@link #stopNetwork}.
+     *
+     * If you want to see every score fluctuation on every request, set
+     * your score filter to a very high number and watch {@link #needNetworkFor}.
+     *
+     * @return {@code true} to accept the request.
+     */
+    public boolean acceptRequest(NetworkRequest request, int score) {
+        return true;
+    }
+
+    private void evalRequest(NetworkRequestInfo n) {
+        if (VDBG) {
+            log("evalRequest");
+            log(" n.requests = " + n.requested);
+            log(" n.score = " + n.score);
+            log(" mScore = " + mScore);
+            log(" n.factorySerialNumber = " + n.factorySerialNumber);
+            log(" mSerialNumber = " + mSerialNumber);
+        }
+        if (shouldNeedNetworkFor(n)) {
+            if (VDBG) log("  needNetworkFor");
+            needNetworkFor(n.request, n.score);
+            n.requested = true;
+        } else if (shouldReleaseNetworkFor(n)) {
+            if (VDBG) log("  releaseNetworkFor");
+            releaseNetworkFor(n.request);
+            n.requested = false;
+        } else {
+            if (VDBG) log("  done");
+        }
+    }
+
+    private boolean shouldNeedNetworkFor(NetworkRequestInfo n) {
+        // If this request is already tracked, it doesn't qualify for need
+        return !n.requested
+            // If the score of this request is higher or equal to that of this factory and some
+            // other factory is responsible for it, then this factory should not track the request
+            // because it has no hope of satisfying it.
+            && (n.score < mScore || n.factorySerialNumber == mSerialNumber)
+            // If this factory can't satisfy the capability needs of this request, then it
+            // should not be tracked.
+            && n.request.networkCapabilities.satisfiedByNetworkCapabilities(mCapabilityFilter)
+            // Finally if the concrete implementation of the factory rejects the request, then
+            // don't track it.
+            && acceptRequest(n.request, n.score);
+    }
+
+    private boolean shouldReleaseNetworkFor(NetworkRequestInfo n) {
+        // Don't release a request that's not tracked.
+        return n.requested
+            // The request should be released if it can't be satisfied by this factory. That
+            // means either of the following conditions are met :
+            // - Its score is too high to be satisfied by this factory and it's not already
+            //   assigned to the factory
+            // - This factory can't satisfy the capability needs of the request
+            // - The concrete implementation of the factory rejects the request
+            && ((n.score > mScore && n.factorySerialNumber != mSerialNumber)
+                    || !n.request.networkCapabilities.satisfiedByNetworkCapabilities(
+                            mCapabilityFilter)
+                    || !acceptRequest(n.request, n.score));
+    }
+
+    private void evalRequests() {
+        for (int i = 0; i < mNetworkRequests.size(); i++) {
+            NetworkRequestInfo n = mNetworkRequests.valueAt(i);
+            evalRequest(n);
+        }
+    }
+
+    /**
+     * Post a command, on this NetworkFactory Handler, to re-evaluate all
+     * oustanding requests. Can be called from a factory implementation.
+     */
+    protected void reevaluateAllRequests() {
+        post(() -> {
+            evalRequests();
+        });
+    }
+
+    /**
+     * Can be called by a factory to release a request as unfulfillable: the request will be
+     * removed, and the caller will get a
+     * {@link ConnectivityManager.NetworkCallback#onUnavailable()} callback after this function
+     * returns.
+     *
+     * Note: this should only be called by factory which KNOWS that it is the ONLY factory which
+     * is able to fulfill this request!
+     */
+    protected void releaseRequestAsUnfulfillableByAnyFactory(NetworkRequest r) {
+        post(() -> {
+            if (DBG) log("releaseRequestAsUnfulfillableByAnyFactory: " + r);
+            Message msg = obtainMessage(EVENT_UNFULFILLABLE_REQUEST, r);
+            if (mAsyncChannel != null) {
+                mAsyncChannel.sendMessage(msg);
+            } else {
+                mPreConnectedQueue.add(msg);
+            }
+        });
+    }
+
+    // override to do simple mode (request independent)
+    protected void startNetwork() { }
+    protected void stopNetwork() { }
+
+    // override to do fancier stuff
+    protected void needNetworkFor(NetworkRequest networkRequest, int score) {
+        if (++mRefCount == 1) startNetwork();
+    }
+
+    protected void releaseNetworkFor(NetworkRequest networkRequest) {
+        if (--mRefCount == 0) stopNetwork();
+    }
+
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    public void setScoreFilter(int score) {
+        sendMessage(obtainMessage(CMD_SET_SCORE, score, 0));
+    }
+
+    public void setCapabilityFilter(NetworkCapabilities netCap) {
+        sendMessage(obtainMessage(CMD_SET_FILTER, new NetworkCapabilities(netCap)));
+    }
+
+    @VisibleForTesting
+    protected int getRequestCount() {
+        return mNetworkRequests.size();
+    }
+
+    public int getSerialNumber() {
+        return mSerialNumber;
+    }
+
+    protected void log(String s) {
+        Log.d(LOG_TAG, s);
+    }
+
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+        final IndentingPrintWriter pw = new IndentingPrintWriter(writer, "  ");
+        pw.println(toString());
+        pw.increaseIndent();
+        for (int i = 0; i < mNetworkRequests.size(); i++) {
+            pw.println(mNetworkRequests.valueAt(i));
+        }
+        pw.decreaseIndent();
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder("{").append(LOG_TAG).append(" - mSerialNumber=")
+                .append(mSerialNumber).append(", ScoreFilter=")
+                .append(mScore).append(", Filter=").append(mCapabilityFilter).append(", requests=")
+                .append(mNetworkRequests.size()).append(", refCount=").append(mRefCount)
+                .append("}");
+        return sb.toString();
+    }
+}
diff --git a/android/net/NetworkIdentity.java b/android/net/NetworkIdentity.java
new file mode 100644
index 0000000..ce2de85
--- /dev/null
+++ b/android/net/NetworkIdentity.java
@@ -0,0 +1,257 @@
+/*
+ * Copyright (C) 2011 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 static android.net.ConnectivityManager.TYPE_WIFI;
+import static android.net.ConnectivityManager.getNetworkTypeName;
+import static android.net.ConnectivityManager.isNetworkTypeMobile;
+
+import android.content.Context;
+import android.net.wifi.WifiInfo;
+import android.net.wifi.WifiManager;
+import android.os.Build;
+import android.service.NetworkIdentityProto;
+import android.telephony.TelephonyManager;
+import android.util.Slog;
+import android.util.proto.ProtoOutputStream;
+
+import java.util.Objects;
+
+/**
+ * Network definition that includes strong identity. Analogous to combining
+ * {@link NetworkInfo} and an IMSI.
+ *
+ * @hide
+ */
+public class NetworkIdentity implements Comparable<NetworkIdentity> {
+    private static final String TAG = "NetworkIdentity";
+
+    /**
+     * When enabled, combine all {@link #mSubType} together under
+     * {@link #SUBTYPE_COMBINED}.
+     *
+     * @deprecated we no longer offer to collect statistics on a per-subtype
+     *             basis; this is always disabled.
+     */
+    @Deprecated
+    public static final boolean COMBINE_SUBTYPE_ENABLED = true;
+
+    public static final int SUBTYPE_COMBINED = -1;
+
+    final int mType;
+    final int mSubType;
+    final String mSubscriberId;
+    final String mNetworkId;
+    final boolean mRoaming;
+    final boolean mMetered;
+    final boolean mDefaultNetwork;
+
+    public NetworkIdentity(
+            int type, int subType, String subscriberId, String networkId, boolean roaming,
+            boolean metered, boolean defaultNetwork) {
+        mType = type;
+        mSubType = COMBINE_SUBTYPE_ENABLED ? SUBTYPE_COMBINED : subType;
+        mSubscriberId = subscriberId;
+        mNetworkId = networkId;
+        mRoaming = roaming;
+        mMetered = metered;
+        mDefaultNetwork = defaultNetwork;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mType, mSubType, mSubscriberId, mNetworkId, mRoaming, mMetered,
+                mDefaultNetwork);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj instanceof NetworkIdentity) {
+            final NetworkIdentity ident = (NetworkIdentity) obj;
+            return mType == ident.mType && mSubType == ident.mSubType && mRoaming == ident.mRoaming
+                    && Objects.equals(mSubscriberId, ident.mSubscriberId)
+                    && Objects.equals(mNetworkId, ident.mNetworkId)
+                    && mMetered == ident.mMetered
+                    && mDefaultNetwork == ident.mDefaultNetwork;
+        }
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder("{");
+        builder.append("type=").append(getNetworkTypeName(mType));
+        builder.append(", subType=");
+        if (COMBINE_SUBTYPE_ENABLED) {
+            builder.append("COMBINED");
+        } else if (ConnectivityManager.isNetworkTypeMobile(mType)) {
+            builder.append(TelephonyManager.getNetworkTypeName(mSubType));
+        } else {
+            builder.append(mSubType);
+        }
+        if (mSubscriberId != null) {
+            builder.append(", subscriberId=").append(scrubSubscriberId(mSubscriberId));
+        }
+        if (mNetworkId != null) {
+            builder.append(", networkId=").append(mNetworkId);
+        }
+        if (mRoaming) {
+            builder.append(", ROAMING");
+        }
+        builder.append(", metered=").append(mMetered);
+        builder.append(", defaultNetwork=").append(mDefaultNetwork);
+        return builder.append("}").toString();
+    }
+
+    public void writeToProto(ProtoOutputStream proto, long tag) {
+        final long start = proto.start(tag);
+
+        proto.write(NetworkIdentityProto.TYPE, mType);
+
+        // Not dumping mSubType, subtypes are no longer supported.
+
+        if (mSubscriberId != null) {
+            proto.write(NetworkIdentityProto.SUBSCRIBER_ID, scrubSubscriberId(mSubscriberId));
+        }
+        proto.write(NetworkIdentityProto.NETWORK_ID, mNetworkId);
+        proto.write(NetworkIdentityProto.ROAMING, mRoaming);
+        proto.write(NetworkIdentityProto.METERED, mMetered);
+        proto.write(NetworkIdentityProto.DEFAULT_NETWORK, mDefaultNetwork);
+
+        proto.end(start);
+    }
+
+    public int getType() {
+        return mType;
+    }
+
+    public int getSubType() {
+        return mSubType;
+    }
+
+    public String getSubscriberId() {
+        return mSubscriberId;
+    }
+
+    public String getNetworkId() {
+        return mNetworkId;
+    }
+
+    public boolean getRoaming() {
+        return mRoaming;
+    }
+
+    public boolean getMetered() {
+        return mMetered;
+    }
+
+    public boolean getDefaultNetwork() {
+        return mDefaultNetwork;
+    }
+
+    /**
+     * Scrub given IMSI on production builds.
+     */
+    public static String scrubSubscriberId(String subscriberId) {
+        if (Build.IS_ENG) {
+            return subscriberId;
+        } else if (subscriberId != null) {
+            // TODO: parse this as MCC+MNC instead of hard-coding
+            return subscriberId.substring(0, Math.min(6, subscriberId.length())) + "...";
+        } else {
+            return "null";
+        }
+    }
+
+    /**
+     * Scrub given IMSI on production builds.
+     */
+    public static String[] scrubSubscriberId(String[] subscriberId) {
+        if (subscriberId == null) return null;
+        final String[] res = new String[subscriberId.length];
+        for (int i = 0; i < res.length; i++) {
+            res[i] = NetworkIdentity.scrubSubscriberId(subscriberId[i]);
+        }
+        return res;
+    }
+
+    /**
+     * Build a {@link NetworkIdentity} from the given {@link NetworkState},
+     * assuming that any mobile networks are using the current IMSI.
+     */
+    public static NetworkIdentity buildNetworkIdentity(Context context, NetworkState state,
+            boolean defaultNetwork) {
+        final int type = state.networkInfo.getType();
+        final int subType = state.networkInfo.getSubtype();
+
+        String subscriberId = null;
+        String networkId = null;
+        boolean roaming = !state.networkCapabilities.hasCapability(
+                NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING);
+        boolean metered = !state.networkCapabilities.hasCapability(
+                NetworkCapabilities.NET_CAPABILITY_NOT_METERED);
+
+        if (isNetworkTypeMobile(type)) {
+            if (state.subscriberId == null) {
+                if (state.networkInfo.getState() != NetworkInfo.State.DISCONNECTED &&
+                        state.networkInfo.getState() != NetworkInfo.State.UNKNOWN) {
+                    Slog.w(TAG, "Active mobile network without subscriber! ni = "
+                            + state.networkInfo);
+                }
+            }
+
+            subscriberId = state.subscriberId;
+
+        } else if (type == TYPE_WIFI) {
+            if (state.networkId != null) {
+                networkId = state.networkId;
+            } else {
+                final WifiManager wifi = (WifiManager) context.getSystemService(
+                        Context.WIFI_SERVICE);
+                final WifiInfo info = wifi.getConnectionInfo();
+                networkId = info != null ? info.getSSID() : null;
+            }
+        }
+
+        return new NetworkIdentity(type, subType, subscriberId, networkId, roaming, metered,
+                defaultNetwork);
+    }
+
+    @Override
+    public int compareTo(NetworkIdentity another) {
+        int res = Integer.compare(mType, another.mType);
+        if (res == 0) {
+            res = Integer.compare(mSubType, another.mSubType);
+        }
+        if (res == 0 && mSubscriberId != null && another.mSubscriberId != null) {
+            res = mSubscriberId.compareTo(another.mSubscriberId);
+        }
+        if (res == 0 && mNetworkId != null && another.mNetworkId != null) {
+            res = mNetworkId.compareTo(another.mNetworkId);
+        }
+        if (res == 0) {
+            res = Boolean.compare(mRoaming, another.mRoaming);
+        }
+        if (res == 0) {
+            res = Boolean.compare(mMetered, another.mMetered);
+        }
+        if (res == 0) {
+            res = Boolean.compare(mDefaultNetwork, another.mDefaultNetwork);
+        }
+        return res;
+    }
+}
diff --git a/android/net/NetworkInfo.java b/android/net/NetworkInfo.java
new file mode 100644
index 0000000..92f105f
--- /dev/null
+++ b/android/net/NetworkInfo.java
@@ -0,0 +1,586 @@
+/*
+ * Copyright (C) 2008 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.NonNull;
+import android.annotation.UnsupportedAppUsage;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.EnumMap;
+
+/**
+ * Describes the status of a network interface.
+ * <p>Use {@link ConnectivityManager#getActiveNetworkInfo()} to get an instance that represents
+ * the current network connection.
+ *
+ * @deprecated Callers should instead use the {@link ConnectivityManager.NetworkCallback} API to
+ *             learn about connectivity changes, or switch to use
+ *             {@link ConnectivityManager#getNetworkCapabilities} or
+ *             {@link ConnectivityManager#getLinkProperties} to get information synchronously. Keep
+ *             in mind that while callbacks are guaranteed to be called for every event in order,
+ *             synchronous calls have no such constraints, and as such it is unadvisable to use the
+ *             synchronous methods inside the callbacks as they will often not offer a view of
+ *             networking that is consistent (that is: they may return a past or a future state with
+ *             respect to the event being processed by the callback). Instead, callers are advised
+ *             to only use the arguments of the callbacks, possibly memorizing the specific bits of
+ *             information they need to keep from one callback to another.
+ */
+@Deprecated
+public class NetworkInfo implements Parcelable {
+
+    /**
+     * Coarse-grained network state. This is probably what most applications should
+     * use, rather than {@link android.net.NetworkInfo.DetailedState DetailedState}.
+     * The mapping between the two is as follows:
+     * <br/><br/>
+     * <table>
+     * <tr><td><b>Detailed state</b></td><td><b>Coarse-grained state</b></td></tr>
+     * <tr><td><code>IDLE</code></td><td><code>DISCONNECTED</code></td></tr>
+     * <tr><td><code>SCANNING</code></td><td><code>DISCONNECTED</code></td></tr>
+     * <tr><td><code>CONNECTING</code></td><td><code>CONNECTING</code></td></tr>
+     * <tr><td><code>AUTHENTICATING</code></td><td><code>CONNECTING</code></td></tr>
+     * <tr><td><code>OBTAINING_IPADDR</code></td><td><code>CONNECTING</code></td></tr>
+     * <tr><td><code>VERIFYING_POOR_LINK</code></td><td><code>CONNECTING</code></td></tr>
+     * <tr><td><code>CAPTIVE_PORTAL_CHECK</code></td><td><code>CONNECTING</code></td></tr>
+     * <tr><td><code>CONNECTED</code></td><td><code>CONNECTED</code></td></tr>
+     * <tr><td><code>SUSPENDED</code></td><td><code>SUSPENDED</code></td></tr>
+     * <tr><td><code>DISCONNECTING</code></td><td><code>DISCONNECTING</code></td></tr>
+     * <tr><td><code>DISCONNECTED</code></td><td><code>DISCONNECTED</code></td></tr>
+     * <tr><td><code>FAILED</code></td><td><code>DISCONNECTED</code></td></tr>
+     * <tr><td><code>BLOCKED</code></td><td><code>DISCONNECTED</code></td></tr>
+     * </table>
+     *
+     * @deprecated See {@link NetworkInfo}.
+     */
+    @Deprecated
+    public enum State {
+        CONNECTING, CONNECTED, SUSPENDED, DISCONNECTING, DISCONNECTED, UNKNOWN
+    }
+
+    /**
+     * The fine-grained state of a network connection. This level of detail
+     * is probably of interest to few applications. Most should use
+     * {@link android.net.NetworkInfo.State State} instead.
+     *
+     * @deprecated See {@link NetworkInfo}.
+     */
+    @Deprecated
+    public enum DetailedState {
+        /** Ready to start data connection setup. */
+        IDLE,
+        /** Searching for an available access point. */
+        SCANNING,
+        /** Currently setting up data connection. */
+        CONNECTING,
+        /** Network link established, performing authentication. */
+        AUTHENTICATING,
+        /** Awaiting response from DHCP server in order to assign IP address information. */
+        OBTAINING_IPADDR,
+        /** IP traffic should be available. */
+        CONNECTED,
+        /** IP traffic is suspended */
+        SUSPENDED,
+        /** Currently tearing down data connection. */
+        DISCONNECTING,
+        /** IP traffic not available. */
+        DISCONNECTED,
+        /** Attempt to connect failed. */
+        FAILED,
+        /** Access to this network is blocked. */
+        BLOCKED,
+        /** Link has poor connectivity. */
+        VERIFYING_POOR_LINK,
+        /** Checking if network is a captive portal */
+        CAPTIVE_PORTAL_CHECK
+    }
+
+    /**
+     * This is the map described in the Javadoc comment above. The positions
+     * of the elements of the array must correspond to the ordinal values
+     * of <code>DetailedState</code>.
+     */
+    private static final EnumMap<DetailedState, State> stateMap =
+        new EnumMap<DetailedState, State>(DetailedState.class);
+
+    static {
+        stateMap.put(DetailedState.IDLE, State.DISCONNECTED);
+        stateMap.put(DetailedState.SCANNING, State.DISCONNECTED);
+        stateMap.put(DetailedState.CONNECTING, State.CONNECTING);
+        stateMap.put(DetailedState.AUTHENTICATING, State.CONNECTING);
+        stateMap.put(DetailedState.OBTAINING_IPADDR, State.CONNECTING);
+        stateMap.put(DetailedState.VERIFYING_POOR_LINK, State.CONNECTING);
+        stateMap.put(DetailedState.CAPTIVE_PORTAL_CHECK, State.CONNECTING);
+        stateMap.put(DetailedState.CONNECTED, State.CONNECTED);
+        stateMap.put(DetailedState.SUSPENDED, State.SUSPENDED);
+        stateMap.put(DetailedState.DISCONNECTING, State.DISCONNECTING);
+        stateMap.put(DetailedState.DISCONNECTED, State.DISCONNECTED);
+        stateMap.put(DetailedState.FAILED, State.DISCONNECTED);
+        stateMap.put(DetailedState.BLOCKED, State.DISCONNECTED);
+    }
+
+    private int mNetworkType;
+    private int mSubtype;
+    private String mTypeName;
+    private String mSubtypeName;
+    @NonNull
+    private State mState;
+    @NonNull
+    private DetailedState mDetailedState;
+    private String mReason;
+    private String mExtraInfo;
+    private boolean mIsFailover;
+    private boolean mIsAvailable;
+    private boolean mIsRoaming;
+
+    /**
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public NetworkInfo(int type, int subtype, String typeName, String subtypeName) {
+        if (!ConnectivityManager.isNetworkTypeValid(type)
+                && type != ConnectivityManager.TYPE_NONE) {
+            throw new IllegalArgumentException("Invalid network type: " + type);
+        }
+        mNetworkType = type;
+        mSubtype = subtype;
+        mTypeName = typeName;
+        mSubtypeName = subtypeName;
+        setDetailedState(DetailedState.IDLE, null, null);
+        mState = State.UNKNOWN;
+    }
+
+    /** {@hide} */
+    @UnsupportedAppUsage
+    public NetworkInfo(NetworkInfo source) {
+        if (source != null) {
+            synchronized (source) {
+                mNetworkType = source.mNetworkType;
+                mSubtype = source.mSubtype;
+                mTypeName = source.mTypeName;
+                mSubtypeName = source.mSubtypeName;
+                mState = source.mState;
+                mDetailedState = source.mDetailedState;
+                mReason = source.mReason;
+                mExtraInfo = source.mExtraInfo;
+                mIsFailover = source.mIsFailover;
+                mIsAvailable = source.mIsAvailable;
+                mIsRoaming = source.mIsRoaming;
+            }
+        }
+    }
+
+    /**
+     * Reports the type of network to which the
+     * info in this {@code NetworkInfo} pertains.
+     * @return one of {@link ConnectivityManager#TYPE_MOBILE}, {@link
+     * ConnectivityManager#TYPE_WIFI}, {@link ConnectivityManager#TYPE_WIMAX}, {@link
+     * ConnectivityManager#TYPE_ETHERNET},  {@link ConnectivityManager#TYPE_BLUETOOTH}, or other
+     * types defined by {@link ConnectivityManager}.
+     * @deprecated Callers should switch to checking {@link NetworkCapabilities#hasTransport}
+     *             instead with one of the NetworkCapabilities#TRANSPORT_* constants :
+     *             {@link #getType} and {@link #getTypeName} cannot account for networks using
+     *             multiple transports. Note that generally apps should not care about transport;
+     *             {@link NetworkCapabilities#NET_CAPABILITY_NOT_METERED} and
+     *             {@link NetworkCapabilities#getLinkDownstreamBandwidthKbps} are calls that
+     *             apps concerned with meteredness or bandwidth should be looking at, as they
+     *             offer this information with much better accuracy.
+     */
+    @Deprecated
+    public int getType() {
+        synchronized (this) {
+            return mNetworkType;
+        }
+    }
+
+    /**
+     * @deprecated Use {@link NetworkCapabilities} instead
+     * @hide
+     */
+    @Deprecated
+    public void setType(int type) {
+        synchronized (this) {
+            mNetworkType = type;
+        }
+    }
+
+    /**
+     * Return a network-type-specific integer describing the subtype
+     * of the network.
+     * @return the network subtype
+     * @deprecated Use {@link android.telephony.TelephonyManager#getDataNetworkType} instead.
+     */
+    @Deprecated
+    public int getSubtype() {
+        synchronized (this) {
+            return mSubtype;
+        }
+    }
+
+    /**
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public void setSubtype(int subtype, String subtypeName) {
+        synchronized (this) {
+            mSubtype = subtype;
+            mSubtypeName = subtypeName;
+        }
+    }
+
+    /**
+     * Return a human-readable name describe the type of the network,
+     * for example "WIFI" or "MOBILE".
+     * @return the name of the network type
+     * @deprecated Callers should switch to checking {@link NetworkCapabilities#hasTransport}
+     *             instead with one of the NetworkCapabilities#TRANSPORT_* constants :
+     *             {@link #getType} and {@link #getTypeName} cannot account for networks using
+     *             multiple transports. Note that generally apps should not care about transport;
+     *             {@link NetworkCapabilities#NET_CAPABILITY_NOT_METERED} and
+     *             {@link NetworkCapabilities#getLinkDownstreamBandwidthKbps} are calls that
+     *             apps concerned with meteredness or bandwidth should be looking at, as they
+     *             offer this information with much better accuracy.
+     */
+    @Deprecated
+    public String getTypeName() {
+        synchronized (this) {
+            return mTypeName;
+        }
+    }
+
+    /**
+     * Return a human-readable name describing the subtype of the network.
+     * @return the name of the network subtype
+     * @deprecated Use {@link android.telephony.TelephonyManager#getDataNetworkType} instead.
+     */
+    @Deprecated
+    public String getSubtypeName() {
+        synchronized (this) {
+            return mSubtypeName;
+        }
+    }
+
+    /**
+     * Indicates whether network connectivity exists or is in the process
+     * of being established. This is good for applications that need to
+     * do anything related to the network other than read or write data.
+     * For the latter, call {@link #isConnected()} instead, which guarantees
+     * that the network is fully usable.
+     * @return {@code true} if network connectivity exists or is in the process
+     * of being established, {@code false} otherwise.
+     * @deprecated Apps should instead use the
+     *             {@link android.net.ConnectivityManager.NetworkCallback} API to
+     *             learn about connectivity changes.
+     *             {@link ConnectivityManager#registerDefaultNetworkCallback} and
+     *             {@link ConnectivityManager#registerNetworkCallback}. These will
+     *             give a more accurate picture of the connectivity state of
+     *             the device and let apps react more easily and quickly to changes.
+     */
+    @Deprecated
+    public boolean isConnectedOrConnecting() {
+        synchronized (this) {
+            return mState == State.CONNECTED || mState == State.CONNECTING;
+        }
+    }
+
+    /**
+     * Indicates whether network connectivity exists and it is possible to establish
+     * connections and pass data.
+     * <p>Always call this before attempting to perform data transactions.
+     * @return {@code true} if network connectivity exists, {@code false} otherwise.
+     * @deprecated Apps should instead use the
+     *             {@link android.net.ConnectivityManager.NetworkCallback} API to
+     *             learn about connectivity changes. See
+     *             {@link ConnectivityManager#registerDefaultNetworkCallback} and
+     *             {@link ConnectivityManager#registerNetworkCallback}. These will
+     *             give a more accurate picture of the connectivity state of
+     *             the device and let apps react more easily and quickly to changes.
+     */
+    @Deprecated
+    public boolean isConnected() {
+        synchronized (this) {
+            return mState == State.CONNECTED;
+        }
+    }
+
+    /**
+     * Indicates whether network connectivity is possible. A network is unavailable
+     * when a persistent or semi-persistent condition prevents the possibility
+     * of connecting to that network. Examples include
+     * <ul>
+     * <li>The device is out of the coverage area for any network of this type.</li>
+     * <li>The device is on a network other than the home network (i.e., roaming), and
+     * data roaming has been disabled.</li>
+     * <li>The device's radio is turned off, e.g., because airplane mode is enabled.</li>
+     * </ul>
+     * Since Android L, this always returns {@code true}, because the system only
+     * returns info for available networks.
+     * @return {@code true} if the network is available, {@code false} otherwise
+     * @deprecated Apps should instead use the
+     *             {@link android.net.ConnectivityManager.NetworkCallback} API to
+     *             learn about connectivity changes.
+     *             {@link ConnectivityManager#registerDefaultNetworkCallback} and
+     *             {@link ConnectivityManager#registerNetworkCallback}. These will
+     *             give a more accurate picture of the connectivity state of
+     *             the device and let apps react more easily and quickly to changes.
+     */
+    @Deprecated
+    public boolean isAvailable() {
+        synchronized (this) {
+            return mIsAvailable;
+        }
+    }
+
+    /**
+     * Sets if the network is available, ie, if the connectivity is possible.
+     * @param isAvailable the new availability value.
+     * @deprecated Use {@link NetworkCapabilities} instead
+     *
+     * @hide
+     */
+    @Deprecated
+    @UnsupportedAppUsage
+    public void setIsAvailable(boolean isAvailable) {
+        synchronized (this) {
+            mIsAvailable = isAvailable;
+        }
+    }
+
+    /**
+     * Indicates whether the current attempt to connect to the network
+     * resulted from the ConnectivityManager trying to fail over to this
+     * network following a disconnect from another network.
+     * @return {@code true} if this is a failover attempt, {@code false}
+     * otherwise.
+     * @deprecated This field is not populated in recent Android releases,
+     *             and does not make a lot of sense in a multi-network world.
+     */
+    @Deprecated
+    public boolean isFailover() {
+        synchronized (this) {
+            return mIsFailover;
+        }
+    }
+
+    /**
+     * Set the failover boolean.
+     * @param isFailover {@code true} to mark the current connection attempt
+     * as a failover.
+     * @deprecated This hasn't been set in any recent Android release.
+     * @hide
+     */
+    @Deprecated
+    @UnsupportedAppUsage
+    public void setFailover(boolean isFailover) {
+        synchronized (this) {
+            mIsFailover = isFailover;
+        }
+    }
+
+    /**
+     * Indicates whether the device is currently roaming on this network. When
+     * {@code true}, it suggests that use of data on this network may incur
+     * extra costs.
+     *
+     * @return {@code true} if roaming is in effect, {@code false} otherwise.
+     * @deprecated Callers should switch to checking
+     *             {@link NetworkCapabilities#NET_CAPABILITY_NOT_ROAMING}
+     *             instead, since that handles more complex situations, such as
+     *             VPNs.
+     */
+    @Deprecated
+    public boolean isRoaming() {
+        synchronized (this) {
+            return mIsRoaming;
+        }
+    }
+
+    /**
+     * @deprecated Use {@link NetworkCapabilities#NET_CAPABILITY_NOT_ROAMING} instead.
+     * {@hide}
+     */
+    @VisibleForTesting
+    @Deprecated
+    @UnsupportedAppUsage
+    public void setRoaming(boolean isRoaming) {
+        synchronized (this) {
+            mIsRoaming = isRoaming;
+        }
+    }
+
+    /**
+     * Reports the current coarse-grained state of the network.
+     * @return the coarse-grained state
+     * @deprecated Apps should instead use the
+     *             {@link android.net.ConnectivityManager.NetworkCallback} API to
+     *             learn about connectivity changes.
+     *             {@link ConnectivityManager#registerDefaultNetworkCallback} and
+     *             {@link ConnectivityManager#registerNetworkCallback}. These will
+     *             give a more accurate picture of the connectivity state of
+     *             the device and let apps react more easily and quickly to changes.
+     */
+    @Deprecated
+    public State getState() {
+        synchronized (this) {
+            return mState;
+        }
+    }
+
+    /**
+     * Reports the current fine-grained state of the network.
+     * @return the fine-grained state
+     * @deprecated Apps should instead use the
+     *             {@link android.net.ConnectivityManager.NetworkCallback} API to
+     *             learn about connectivity changes. See
+     *             {@link ConnectivityManager#registerDefaultNetworkCallback} and
+     *             {@link ConnectivityManager#registerNetworkCallback}. These will
+     *             give a more accurate picture of the connectivity state of
+     *             the device and let apps react more easily and quickly to changes.
+     */
+    @Deprecated
+    public @NonNull DetailedState getDetailedState() {
+        synchronized (this) {
+            return mDetailedState;
+        }
+    }
+
+    /**
+     * Sets the fine-grained state of the network.
+     * @param detailedState the {@link DetailedState}.
+     * @param reason a {@code String} indicating the reason for the state change,
+     * if one was supplied. May be {@code null}.
+     * @param extraInfo an optional {@code String} providing addditional network state
+     * information passed up from the lower networking layers.
+     * @deprecated Use {@link NetworkCapabilities} instead.
+     * @hide
+     */
+    @Deprecated
+    @UnsupportedAppUsage
+    public void setDetailedState(DetailedState detailedState, String reason, String extraInfo) {
+        synchronized (this) {
+            this.mDetailedState = detailedState;
+            this.mState = stateMap.get(detailedState);
+            this.mReason = reason;
+            this.mExtraInfo = extraInfo;
+        }
+    }
+
+    /**
+     * Set the extraInfo field.
+     * @param extraInfo an optional {@code String} providing addditional network state
+     * information passed up from the lower networking layers.
+     * @deprecated See {@link NetworkInfo#getExtraInfo}.
+     * @hide
+     */
+    @Deprecated
+    public void setExtraInfo(String extraInfo) {
+        synchronized (this) {
+            this.mExtraInfo = extraInfo;
+        }
+    }
+
+    /**
+     * Report the reason an attempt to establish connectivity failed,
+     * if one is available.
+     * @return the reason for failure, or null if not available
+     * @deprecated This method does not have a consistent contract that could make it useful
+     *             to callers.
+     */
+    public String getReason() {
+        synchronized (this) {
+            return mReason;
+        }
+    }
+
+    /**
+     * Report the extra information about the network state, if any was
+     * provided by the lower networking layers.
+     * @return the extra information, or null if not available
+     * @deprecated Use other services e.g. WifiManager to get additional information passed up from
+     *             the lower networking layers.
+     */
+    @Deprecated
+    public String getExtraInfo() {
+        synchronized (this) {
+            return mExtraInfo;
+        }
+    }
+
+    @Override
+    public String toString() {
+        synchronized (this) {
+            StringBuilder builder = new StringBuilder("[");
+            builder.append("type: ").append(getTypeName()).append("[").append(getSubtypeName()).
+            append("], state: ").append(mState).append("/").append(mDetailedState).
+            append(", reason: ").append(mReason == null ? "(unspecified)" : mReason).
+            append(", extra: ").append(mExtraInfo == null ? "(none)" : mExtraInfo).
+            append(", failover: ").append(mIsFailover).
+            append(", available: ").append(mIsAvailable).
+            append(", roaming: ").append(mIsRoaming).
+            append("]");
+            return builder.toString();
+        }
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        synchronized (this) {
+            dest.writeInt(mNetworkType);
+            dest.writeInt(mSubtype);
+            dest.writeString(mTypeName);
+            dest.writeString(mSubtypeName);
+            dest.writeString(mState.name());
+            dest.writeString(mDetailedState.name());
+            dest.writeInt(mIsFailover ? 1 : 0);
+            dest.writeInt(mIsAvailable ? 1 : 0);
+            dest.writeInt(mIsRoaming ? 1 : 0);
+            dest.writeString(mReason);
+            dest.writeString(mExtraInfo);
+        }
+    }
+
+    public static final @android.annotation.NonNull Creator<NetworkInfo> CREATOR = new Creator<NetworkInfo>() {
+        @Override
+        public NetworkInfo createFromParcel(Parcel in) {
+            int netType = in.readInt();
+            int subtype = in.readInt();
+            String typeName = in.readString();
+            String subtypeName = in.readString();
+            NetworkInfo netInfo = new NetworkInfo(netType, subtype, typeName, subtypeName);
+            netInfo.mState = State.valueOf(in.readString());
+            netInfo.mDetailedState = DetailedState.valueOf(in.readString());
+            netInfo.mIsFailover = in.readInt() != 0;
+            netInfo.mIsAvailable = in.readInt() != 0;
+            netInfo.mIsRoaming = in.readInt() != 0;
+            netInfo.mReason = in.readString();
+            netInfo.mExtraInfo = in.readString();
+            return netInfo;
+        }
+
+        @Override
+        public NetworkInfo[] newArray(int size) {
+            return new NetworkInfo[size];
+        }
+    };
+}
diff --git a/android/net/NetworkKey.java b/android/net/NetworkKey.java
new file mode 100644
index 0000000..0a9a3c8
--- /dev/null
+++ b/android/net/NetworkKey.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.net;
+
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.net.wifi.ScanResult;
+import android.net.wifi.WifiInfo;
+import android.net.wifi.WifiSsid;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.util.Objects;
+
+/**
+ * Information which identifies a specific network.
+ *
+ * @hide
+ */
+@SystemApi
+// NOTE: Ideally, we would abstract away the details of what identifies a network of a specific
+// type, so that all networks appear the same and can be scored without concern to the network type
+// itself. However, because no such cross-type identifier currently exists in the Android framework,
+// and because systems might obtain information about networks from sources other than Android
+// devices, we need to provide identifying details about each specific network type (wifi, cell,
+// etc.) so that clients can pull out these details depending on the type of network.
+public class NetworkKey implements Parcelable {
+
+    private static final String TAG = "NetworkKey";
+
+    /** A wifi network, for which {@link #wifiKey} will be populated. */
+    public static final int TYPE_WIFI = 1;
+
+    /**
+     * The type of this network.
+     * @see #TYPE_WIFI
+     */
+    public final int type;
+
+    /**
+     * Information identifying a Wi-Fi network. Only set when {@link #type} equals
+     * {@link #TYPE_WIFI}.
+     */
+    public final WifiKey wifiKey;
+
+    /**
+     * Constructs a new NetworkKey for the given wifi {@link ScanResult}.
+     *
+     * @return  A new {@link NetworkKey} instance or <code>null</code> if the given
+     *          {@link ScanResult} instance is malformed.
+     * @hide
+     */
+    @Nullable
+    public static NetworkKey createFromScanResult(@Nullable ScanResult result) {
+        if (result != null && result.wifiSsid != null) {
+            final String ssid = result.wifiSsid.toString();
+            final String bssid = result.BSSID;
+            if (!TextUtils.isEmpty(ssid) && !ssid.equals(WifiSsid.NONE)
+                    && !TextUtils.isEmpty(bssid)) {
+                WifiKey wifiKey;
+                try {
+                    wifiKey = new WifiKey(String.format("\"%s\"", ssid), bssid);
+                } catch (IllegalArgumentException e) {
+                    Log.e(TAG, "Unable to create WifiKey.", e);
+                    return null;
+                }
+                return new NetworkKey(wifiKey);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Constructs a new NetworkKey for the given {@link WifiInfo}.
+     *
+     * @param wifiInfo the {@link WifiInfo} to create a {@link NetworkKey} for.
+     * @return A new {@link NetworkKey} instance or <code>null</code> if the given {@link WifiInfo}
+     *         instance doesn't represent a connected WiFi network.
+     * @hide
+     */
+    @Nullable
+    public static NetworkKey createFromWifiInfo(@Nullable WifiInfo wifiInfo) {
+        if (wifiInfo != null) {
+            final String ssid = wifiInfo.getSSID();
+            final String bssid = wifiInfo.getBSSID();
+            if (!TextUtils.isEmpty(ssid) && !ssid.equals(WifiSsid.NONE)
+                    && !TextUtils.isEmpty(bssid)) {
+                WifiKey wifiKey;
+                try {
+                    wifiKey = new WifiKey(ssid, bssid);
+                } catch (IllegalArgumentException e) {
+                    Log.e(TAG, "Unable to create WifiKey.", e);
+                    return null;
+                }
+                return new NetworkKey(wifiKey);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Construct a new {@link NetworkKey} for a Wi-Fi network.
+     * @param wifiKey the {@link WifiKey} identifying this Wi-Fi network.
+     */
+    public NetworkKey(WifiKey wifiKey) {
+        this.type = TYPE_WIFI;
+        this.wifiKey = wifiKey;
+    }
+
+    private NetworkKey(Parcel in) {
+        type = in.readInt();
+        switch (type) {
+            case TYPE_WIFI:
+                wifiKey = WifiKey.CREATOR.createFromParcel(in);
+                break;
+            default:
+                throw new IllegalArgumentException("Parcel has unknown type: " + type);
+        }
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeInt(type);
+        switch (type) {
+            case TYPE_WIFI:
+                wifiKey.writeToParcel(out, flags);
+                break;
+            default:
+                throw new IllegalStateException("NetworkKey has unknown type " + type);
+        }
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        NetworkKey that = (NetworkKey) o;
+
+        return type == that.type && Objects.equals(wifiKey, that.wifiKey);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(type, wifiKey);
+    }
+
+    @Override
+    public String toString() {
+        switch (type) {
+            case TYPE_WIFI:
+                return wifiKey.toString();
+            default:
+                // Don't throw an exception here in case someone is logging this object in a catch
+                // block for debugging purposes.
+                return "InvalidKey";
+        }
+    }
+
+    public static final @android.annotation.NonNull Parcelable.Creator<NetworkKey> CREATOR =
+            new Parcelable.Creator<NetworkKey>() {
+                @Override
+                public NetworkKey createFromParcel(Parcel in) {
+                    return new NetworkKey(in);
+                }
+
+                @Override
+                public NetworkKey[] newArray(int size) {
+                    return new NetworkKey[size];
+                }
+            };
+}
diff --git a/android/net/NetworkMisc.java b/android/net/NetworkMisc.java
new file mode 100644
index 0000000..9ba3bd9
--- /dev/null
+++ b/android/net/NetworkMisc.java
@@ -0,0 +1,127 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * A grab-bag of information (metadata, policies, properties, etc) about a
+ * {@link Network}. Since this contains PII, it should not be sent outside the
+ * system.
+ *
+ * @hide
+ */
+public class NetworkMisc implements Parcelable {
+
+    /**
+     * If the {@link Network} is a VPN, whether apps are allowed to bypass the
+     * VPN. This is set by a {@link VpnService} and used by
+     * {@link ConnectivityManager} when creating a VPN.
+     */
+    public boolean allowBypass;
+
+    /**
+     * Set if the network was manually/explicitly connected to by the user either from settings
+     * or a 3rd party app.  For example, turning on cell data is not explicit but tapping on a wifi
+     * ap in the wifi settings to trigger a connection is explicit.  A 3rd party app asking to
+     * connect to a particular access point is also explicit, though this may change in the future
+     * as we want apps to use the multinetwork apis.
+     */
+    public boolean explicitlySelected;
+
+    /**
+     * Set if the user desires to use this network even if it is unvalidated. This field has meaning
+     * only if {@link explicitlySelected} is true. If it is, this field must also be set to the
+     * appropriate value based on previous user choice.
+     */
+    public boolean acceptUnvalidated;
+
+    /**
+     * Whether the user explicitly set that this network should be validated even if presence of
+     * only partial internet connectivity.
+     */
+    public boolean acceptPartialConnectivity;
+
+    /**
+     * Set to avoid surfacing the "Sign in to network" notification.
+     * if carrier receivers/apps are registered to handle the carrier-specific provisioning
+     * procedure, a carrier specific provisioning notification will be placed.
+     * only one notification should be displayed. This field is set based on
+     * which notification should be used for provisioning.
+     */
+    public boolean provisioningNotificationDisabled;
+
+    /**
+     * For mobile networks, this is the subscriber ID (such as IMSI).
+     */
+    public String subscriberId;
+
+    /**
+     * Set to skip 464xlat. This means the device will treat the network as IPv6-only and
+     * will not attempt to detect a NAT64 via RFC 7050 DNS lookups.
+     */
+    public boolean skip464xlat;
+
+    public NetworkMisc() {
+    }
+
+    public NetworkMisc(NetworkMisc nm) {
+        if (nm != null) {
+            allowBypass = nm.allowBypass;
+            explicitlySelected = nm.explicitlySelected;
+            acceptUnvalidated = nm.acceptUnvalidated;
+            subscriberId = nm.subscriberId;
+            provisioningNotificationDisabled = nm.provisioningNotificationDisabled;
+            skip464xlat = nm.skip464xlat;
+        }
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeInt(allowBypass ? 1 : 0);
+        out.writeInt(explicitlySelected ? 1 : 0);
+        out.writeInt(acceptUnvalidated ? 1 : 0);
+        out.writeString(subscriberId);
+        out.writeInt(provisioningNotificationDisabled ? 1 : 0);
+        out.writeInt(skip464xlat ? 1 : 0);
+    }
+
+    public static final @android.annotation.NonNull Creator<NetworkMisc> CREATOR = new Creator<NetworkMisc>() {
+        @Override
+        public NetworkMisc createFromParcel(Parcel in) {
+            NetworkMisc networkMisc = new NetworkMisc();
+            networkMisc.allowBypass = in.readInt() != 0;
+            networkMisc.explicitlySelected = in.readInt() != 0;
+            networkMisc.acceptUnvalidated = in.readInt() != 0;
+            networkMisc.subscriberId = in.readString();
+            networkMisc.provisioningNotificationDisabled = in.readInt() != 0;
+            networkMisc.skip464xlat = in.readInt() != 0;
+            return networkMisc;
+        }
+
+        @Override
+        public NetworkMisc[] newArray(int size) {
+            return new NetworkMisc[size];
+        }
+    };
+}
diff --git a/android/net/NetworkMonitorManager.java b/android/net/NetworkMonitorManager.java
new file mode 100644
index 0000000..0f41302
--- /dev/null
+++ b/android/net/NetworkMonitorManager.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2019 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.NonNull;
+import android.os.Binder;
+import android.os.RemoteException;
+import android.util.Log;
+
+/**
+ * A convenience wrapper for INetworkMonitor.
+ *
+ * Wraps INetworkMonitor calls, making them a bit more friendly to use. Currently handles:
+ * - Clearing calling identity
+ * - Ignoring RemoteExceptions
+ * - Converting to stable parcelables
+ *
+ * By design, all methods on INetworkMonitor are asynchronous oneway IPCs and are thus void. All the
+ * wrapper methods in this class return a boolean that callers can use to determine whether
+ * RemoteException was thrown.
+ */
+public class NetworkMonitorManager {
+
+    @NonNull private final INetworkMonitor mNetworkMonitor;
+    @NonNull private final String mTag;
+
+    public NetworkMonitorManager(@NonNull INetworkMonitor networkMonitorManager,
+            @NonNull String tag) {
+        mNetworkMonitor = networkMonitorManager;
+        mTag = tag;
+    }
+
+    public NetworkMonitorManager(@NonNull INetworkMonitor networkMonitorManager) {
+        this(networkMonitorManager, NetworkMonitorManager.class.getSimpleName());
+    }
+
+    private void log(String s, Throwable e) {
+        Log.e(mTag, s, e);
+    }
+
+    // CHECKSTYLE:OFF Generated code
+
+    public boolean start() {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mNetworkMonitor.start();
+            return true;
+        } catch (RemoteException e) {
+            log("Error in start", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public boolean launchCaptivePortalApp() {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mNetworkMonitor.launchCaptivePortalApp();
+            return true;
+        } catch (RemoteException e) {
+            log("Error in launchCaptivePortalApp", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public boolean notifyCaptivePortalAppFinished(int response) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mNetworkMonitor.notifyCaptivePortalAppFinished(response);
+            return true;
+        } catch (RemoteException e) {
+            log("Error in notifyCaptivePortalAppFinished", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public boolean setAcceptPartialConnectivity() {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mNetworkMonitor.setAcceptPartialConnectivity();
+            return true;
+        } catch (RemoteException e) {
+            log("Error in setAcceptPartialConnectivity", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public boolean forceReevaluation(int uid) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mNetworkMonitor.forceReevaluation(uid);
+            return true;
+        } catch (RemoteException e) {
+            log("Error in forceReevaluation", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public boolean notifyPrivateDnsChanged(PrivateDnsConfigParcel config) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mNetworkMonitor.notifyPrivateDnsChanged(config);
+            return true;
+        } catch (RemoteException e) {
+            log("Error in notifyPrivateDnsChanged", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public boolean notifyDnsResponse(int returnCode) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mNetworkMonitor.notifyDnsResponse(returnCode);
+            return true;
+        } catch (RemoteException e) {
+            log("Error in notifyDnsResponse", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public boolean notifyNetworkConnected(LinkProperties lp, NetworkCapabilities nc) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mNetworkMonitor.notifyNetworkConnected(lp, nc);
+            return true;
+        } catch (RemoteException e) {
+            log("Error in notifyNetworkConnected", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public boolean notifyNetworkDisconnected() {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mNetworkMonitor.notifyNetworkDisconnected();
+            return true;
+        } catch (RemoteException e) {
+            log("Error in notifyNetworkDisconnected", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public boolean notifyLinkPropertiesChanged(LinkProperties lp) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mNetworkMonitor.notifyLinkPropertiesChanged(lp);
+            return true;
+        } catch (RemoteException e) {
+            log("Error in notifyLinkPropertiesChanged", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    public boolean notifyNetworkCapabilitiesChanged(NetworkCapabilities nc) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mNetworkMonitor.notifyNetworkCapabilitiesChanged(nc);
+            return true;
+        } catch (RemoteException e) {
+            log("Error in notifyNetworkCapabilitiesChanged", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    // CHECKSTYLE:ON Generated code
+}
diff --git a/android/net/NetworkPolicy.java b/android/net/NetworkPolicy.java
new file mode 100644
index 0000000..33baebb
--- /dev/null
+++ b/android/net/NetworkPolicy.java
@@ -0,0 +1,300 @@
+/*
+ * Copyright (C) 2011 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.UnsupportedAppUsage;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.BackupUtils;
+import android.util.Range;
+import android.util.RecurrenceRule;
+
+import com.android.internal.util.Preconditions;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.util.Iterator;
+import java.util.Objects;
+
+/**
+ * Policy for networks matching a {@link NetworkTemplate}, including usage cycle
+ * and limits to be enforced.
+ *
+ * @hide
+ */
+public class NetworkPolicy implements Parcelable, Comparable<NetworkPolicy> {
+    private static final int VERSION_INIT = 1;
+    private static final int VERSION_RULE = 2;
+    private static final int VERSION_RAPID = 3;
+
+    public static final int CYCLE_NONE = -1;
+    public static final long WARNING_DISABLED = -1;
+    public static final long LIMIT_DISABLED = -1;
+    public static final long SNOOZE_NEVER = -1;
+
+    @UnsupportedAppUsage
+    public NetworkTemplate template;
+    public RecurrenceRule cycleRule;
+    @UnsupportedAppUsage
+    public long warningBytes = WARNING_DISABLED;
+    @UnsupportedAppUsage
+    public long limitBytes = LIMIT_DISABLED;
+    public long lastWarningSnooze = SNOOZE_NEVER;
+    public long lastLimitSnooze = SNOOZE_NEVER;
+    public long lastRapidSnooze = SNOOZE_NEVER;
+    @UnsupportedAppUsage
+    @Deprecated public boolean metered = true;
+    @UnsupportedAppUsage
+    public boolean inferred = false;
+
+    private static final long DEFAULT_MTU = 1500;
+
+    public static RecurrenceRule buildRule(int cycleDay, ZoneId cycleTimezone) {
+        if (cycleDay != NetworkPolicy.CYCLE_NONE) {
+            return RecurrenceRule.buildRecurringMonthly(cycleDay, cycleTimezone);
+        } else {
+            return RecurrenceRule.buildNever();
+        }
+    }
+
+    @Deprecated
+    public NetworkPolicy(NetworkTemplate template, int cycleDay, String cycleTimezone,
+            long warningBytes, long limitBytes, boolean metered) {
+        this(template, cycleDay, cycleTimezone, warningBytes, limitBytes, SNOOZE_NEVER,
+                SNOOZE_NEVER, metered, false);
+    }
+
+    @Deprecated
+    @UnsupportedAppUsage
+    public NetworkPolicy(NetworkTemplate template, int cycleDay, String cycleTimezone,
+            long warningBytes, long limitBytes, long lastWarningSnooze, long lastLimitSnooze,
+            boolean metered, boolean inferred) {
+        this(template, buildRule(cycleDay, ZoneId.of(cycleTimezone)), warningBytes,
+                limitBytes, lastWarningSnooze, lastLimitSnooze, metered, inferred);
+    }
+
+    @Deprecated
+    public NetworkPolicy(NetworkTemplate template, RecurrenceRule cycleRule, long warningBytes,
+            long limitBytes, long lastWarningSnooze, long lastLimitSnooze, boolean metered,
+            boolean inferred) {
+        this(template, cycleRule, warningBytes, limitBytes, lastWarningSnooze, lastLimitSnooze,
+                SNOOZE_NEVER, metered, inferred);
+    }
+
+    public NetworkPolicy(NetworkTemplate template, RecurrenceRule cycleRule, long warningBytes,
+            long limitBytes, long lastWarningSnooze, long lastLimitSnooze, long lastRapidSnooze,
+            boolean metered, boolean inferred) {
+        this.template = Preconditions.checkNotNull(template, "missing NetworkTemplate");
+        this.cycleRule = Preconditions.checkNotNull(cycleRule, "missing RecurrenceRule");
+        this.warningBytes = warningBytes;
+        this.limitBytes = limitBytes;
+        this.lastWarningSnooze = lastWarningSnooze;
+        this.lastLimitSnooze = lastLimitSnooze;
+        this.lastRapidSnooze = lastRapidSnooze;
+        this.metered = metered;
+        this.inferred = inferred;
+    }
+
+    private NetworkPolicy(Parcel source) {
+        template = source.readParcelable(null);
+        cycleRule = source.readParcelable(null);
+        warningBytes = source.readLong();
+        limitBytes = source.readLong();
+        lastWarningSnooze = source.readLong();
+        lastLimitSnooze = source.readLong();
+        lastRapidSnooze = source.readLong();
+        metered = source.readInt() != 0;
+        inferred = source.readInt() != 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeParcelable(template, flags);
+        dest.writeParcelable(cycleRule, flags);
+        dest.writeLong(warningBytes);
+        dest.writeLong(limitBytes);
+        dest.writeLong(lastWarningSnooze);
+        dest.writeLong(lastLimitSnooze);
+        dest.writeLong(lastRapidSnooze);
+        dest.writeInt(metered ? 1 : 0);
+        dest.writeInt(inferred ? 1 : 0);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public Iterator<Range<ZonedDateTime>> cycleIterator() {
+        return cycleRule.cycleIterator();
+    }
+
+    /**
+     * Test if given measurement is over {@link #warningBytes}.
+     */
+    @UnsupportedAppUsage
+    public boolean isOverWarning(long totalBytes) {
+        return warningBytes != WARNING_DISABLED && totalBytes >= warningBytes;
+    }
+
+    /**
+     * Test if given measurement is near enough to {@link #limitBytes} to be
+     * considered over-limit.
+     */
+    @UnsupportedAppUsage
+    public boolean isOverLimit(long totalBytes) {
+        // over-estimate, since kernel will trigger limit once first packet
+        // trips over limit.
+        totalBytes += 2 * DEFAULT_MTU;
+        return limitBytes != LIMIT_DISABLED && totalBytes >= limitBytes;
+    }
+
+    /**
+     * Clear any existing snooze values, setting to {@link #SNOOZE_NEVER}.
+     */
+    @UnsupportedAppUsage
+    public void clearSnooze() {
+        lastWarningSnooze = SNOOZE_NEVER;
+        lastLimitSnooze = SNOOZE_NEVER;
+        lastRapidSnooze = SNOOZE_NEVER;
+    }
+
+    /**
+     * Test if this policy has a cycle defined, after which usage should reset.
+     */
+    public boolean hasCycle() {
+        return cycleRule.cycleIterator().hasNext();
+    }
+
+    @Override
+    @UnsupportedAppUsage
+    public int compareTo(NetworkPolicy another) {
+        if (another == null || another.limitBytes == LIMIT_DISABLED) {
+            // other value is missing or disabled; we win
+            return -1;
+        }
+        if (limitBytes == LIMIT_DISABLED || another.limitBytes < limitBytes) {
+            // we're disabled or other limit is smaller; they win
+            return 1;
+        }
+        return 0;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(template, cycleRule, warningBytes, limitBytes,
+                lastWarningSnooze, lastLimitSnooze, lastRapidSnooze, metered, inferred);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj instanceof NetworkPolicy) {
+            final NetworkPolicy other = (NetworkPolicy) obj;
+            return warningBytes == other.warningBytes
+                    && limitBytes == other.limitBytes
+                    && lastWarningSnooze == other.lastWarningSnooze
+                    && lastLimitSnooze == other.lastLimitSnooze
+                    && lastRapidSnooze == other.lastRapidSnooze
+                    && metered == other.metered
+                    && inferred == other.inferred
+                    && Objects.equals(template, other.template)
+                    && Objects.equals(cycleRule, other.cycleRule);
+        }
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        return new StringBuilder("NetworkPolicy{")
+                .append("template=").append(template)
+                .append(" cycleRule=").append(cycleRule)
+                .append(" warningBytes=").append(warningBytes)
+                .append(" limitBytes=").append(limitBytes)
+                .append(" lastWarningSnooze=").append(lastWarningSnooze)
+                .append(" lastLimitSnooze=").append(lastLimitSnooze)
+                .append(" lastRapidSnooze=").append(lastRapidSnooze)
+                .append(" metered=").append(metered)
+                .append(" inferred=").append(inferred)
+                .append("}").toString();
+    }
+
+    @UnsupportedAppUsage
+    public static final @android.annotation.NonNull Creator<NetworkPolicy> CREATOR = new Creator<NetworkPolicy>() {
+        @Override
+        public NetworkPolicy createFromParcel(Parcel in) {
+            return new NetworkPolicy(in);
+        }
+
+        @Override
+        public NetworkPolicy[] newArray(int size) {
+            return new NetworkPolicy[size];
+        }
+    };
+
+    public byte[] getBytesForBackup() throws IOException {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        DataOutputStream out = new DataOutputStream(baos);
+
+        out.writeInt(VERSION_RAPID);
+        out.write(template.getBytesForBackup());
+        cycleRule.writeToStream(out);
+        out.writeLong(warningBytes);
+        out.writeLong(limitBytes);
+        out.writeLong(lastWarningSnooze);
+        out.writeLong(lastLimitSnooze);
+        out.writeLong(lastRapidSnooze);
+        out.writeInt(metered ? 1 : 0);
+        out.writeInt(inferred ? 1 : 0);
+        return baos.toByteArray();
+    }
+
+    public static NetworkPolicy getNetworkPolicyFromBackup(DataInputStream in) throws IOException,
+            BackupUtils.BadVersionException {
+        final int version = in.readInt();
+        if (version < VERSION_INIT || version > VERSION_RAPID) {
+            throw new BackupUtils.BadVersionException("Unknown backup version: " + version);
+        }
+
+        final NetworkTemplate template = NetworkTemplate.getNetworkTemplateFromBackup(in);
+        final RecurrenceRule cycleRule;
+        if (version >= VERSION_RULE) {
+            cycleRule = new RecurrenceRule(in);
+        } else {
+            final int cycleDay = in.readInt();
+            final String cycleTimezone = BackupUtils.readString(in);
+            cycleRule = buildRule(cycleDay, ZoneId.of(cycleTimezone));
+        }
+        final long warningBytes = in.readLong();
+        final long limitBytes = in.readLong();
+        final long lastWarningSnooze = in.readLong();
+        final long lastLimitSnooze = in.readLong();
+        final long lastRapidSnooze;
+        if (version >= VERSION_RAPID) {
+            lastRapidSnooze = in.readLong();
+        } else {
+            lastRapidSnooze = SNOOZE_NEVER;
+        }
+        final boolean metered = in.readInt() == 1;
+        final boolean inferred = in.readInt() == 1;
+        return new NetworkPolicy(template, cycleRule, warningBytes, limitBytes, lastWarningSnooze,
+                lastLimitSnooze, lastRapidSnooze, metered, inferred);
+    }
+}
diff --git a/android/net/NetworkPolicyManager.java b/android/net/NetworkPolicyManager.java
new file mode 100644
index 0000000..bf27262
--- /dev/null
+++ b/android/net/NetworkPolicyManager.java
@@ -0,0 +1,385 @@
+/*
+ * Copyright (C) 2011 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 static android.content.pm.PackageManager.GET_SIGNATURES;
+
+import android.annotation.SystemService;
+import android.annotation.UnsupportedAppUsage;
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.Signature;
+import android.net.wifi.WifiConfiguration;
+import android.net.wifi.WifiInfo;
+import android.os.Build;
+import android.os.RemoteException;
+import android.os.UserHandle;
+import android.util.DebugUtils;
+import android.util.Pair;
+import android.util.Range;
+
+import com.google.android.collect.Sets;
+
+import java.time.ZonedDateTime;
+import java.util.HashSet;
+import java.util.Iterator;
+
+/**
+ * Manager for creating and modifying network policy rules.
+ *
+ * {@hide}
+ */
+@SystemService(Context.NETWORK_POLICY_SERVICE)
+public class NetworkPolicyManager {
+
+    /* POLICY_* are masks and can be ORed, although currently they are not.*/
+    /** No specific network policy, use system default. */
+    public static final int POLICY_NONE = 0x0;
+    /** Reject network usage on metered networks when application in background. */
+    public static final int POLICY_REJECT_METERED_BACKGROUND = 0x1;
+    /** Allow metered network use in the background even when in data usage save mode. */
+    public static final int POLICY_ALLOW_METERED_BACKGROUND = 0x4;
+
+    /*
+     * Rules defining whether an uid has access to a network given its type (metered / non-metered).
+     *
+     * These rules are bits and can be used in bitmask operations; in particular:
+     * - rule & RULE_MASK_METERED: returns the metered-networks status.
+     * - rule & RULE_MASK_ALL: returns the all-networks status.
+     *
+     * The RULE_xxx_ALL rules applies to all networks (metered or non-metered), but on
+     * metered networks, the RULE_xxx_METERED rules should be checked first. For example,
+     * if the device is on Battery Saver Mode and Data Saver Mode simulatenously, and a uid
+     * is whitelisted for the former but not the latter, its status would be
+     * RULE_REJECT_METERED | RULE_ALLOW_ALL, meaning it could have access to non-metered
+     * networks but not to metered networks.
+     *
+     * See network-policy-restrictions.md for more info.
+     */
+    /** No specific rule was set */
+    public static final int RULE_NONE = 0;
+    /** Allow traffic on metered networks. */
+    public static final int RULE_ALLOW_METERED = 1 << 0;
+    /** Temporarily allow traffic on metered networks because app is on foreground. */
+    public static final int RULE_TEMPORARY_ALLOW_METERED = 1 << 1;
+    /** Reject traffic on metered networks. */
+    public static final int RULE_REJECT_METERED = 1 << 2;
+    /** Network traffic should be allowed on all networks (metered or non-metered), although
+     * metered-network restrictions could still apply. */
+    public static final int RULE_ALLOW_ALL = 1 << 5;
+    /** Reject traffic on all networks. */
+    public static final int RULE_REJECT_ALL = 1 << 6;
+    /** Mask used to get the {@code RULE_xxx_METERED} rules */
+    public static final int MASK_METERED_NETWORKS = 0b00001111;
+    /** Mask used to get the {@code RULE_xxx_ALL} rules */
+    public static final int MASK_ALL_NETWORKS     = 0b11110000;
+
+    public static final int FIREWALL_RULE_DEFAULT = 0;
+
+    public static final String FIREWALL_CHAIN_NAME_NONE = "none";
+    public static final String FIREWALL_CHAIN_NAME_DOZABLE = "dozable";
+    public static final String FIREWALL_CHAIN_NAME_STANDBY = "standby";
+    public static final String FIREWALL_CHAIN_NAME_POWERSAVE = "powersave";
+
+    private static final boolean ALLOW_PLATFORM_APP_POLICY = true;
+
+    public static final int FOREGROUND_THRESHOLD_STATE =
+            ActivityManager.PROCESS_STATE_BOUND_FOREGROUND_SERVICE;
+
+    /**
+     * {@link Intent} extra that indicates which {@link NetworkTemplate} rule it
+     * applies to.
+     */
+    public static final String EXTRA_NETWORK_TEMPLATE = "android.net.NETWORK_TEMPLATE";
+
+    public static final int OVERRIDE_UNMETERED = 1 << 0;
+    public static final int OVERRIDE_CONGESTED = 1 << 1;
+
+    private final Context mContext;
+    @UnsupportedAppUsage
+    private INetworkPolicyManager mService;
+
+    public NetworkPolicyManager(Context context, INetworkPolicyManager service) {
+        if (service == null) {
+            throw new IllegalArgumentException("missing INetworkPolicyManager");
+        }
+        mContext = context;
+        mService = service;
+    }
+
+    @UnsupportedAppUsage
+    public static NetworkPolicyManager from(Context context) {
+        return (NetworkPolicyManager) context.getSystemService(Context.NETWORK_POLICY_SERVICE);
+    }
+
+    /**
+     * Set policy flags for specific UID.
+     *
+     * @param policy should be {@link #POLICY_NONE} or any combination of {@code POLICY_} flags,
+     *     although it is not validated.
+     */
+    @UnsupportedAppUsage
+    public void setUidPolicy(int uid, int policy) {
+        try {
+            mService.setUidPolicy(uid, policy);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Add policy flags for specific UID.
+     *
+     * <p>The given policy bits will be set for the uid.
+     *
+     * @param policy should be {@link #POLICY_NONE} or any combination of {@code POLICY_} flags,
+     *     although it is not validated.
+     */
+    public void addUidPolicy(int uid, int policy) {
+        try {
+            mService.addUidPolicy(uid, policy);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Clear/remove policy flags for specific UID.
+     *
+     * <p>The given policy bits will be set for the uid.
+     *
+     * @param policy should be {@link #POLICY_NONE} or any combination of {@code POLICY_} flags,
+     *     although it is not validated.
+     */
+    public void removeUidPolicy(int uid, int policy) {
+        try {
+            mService.removeUidPolicy(uid, policy);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    @UnsupportedAppUsage
+    public int getUidPolicy(int uid) {
+        try {
+            return mService.getUidPolicy(uid);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    @UnsupportedAppUsage
+    public int[] getUidsWithPolicy(int policy) {
+        try {
+            return mService.getUidsWithPolicy(policy);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    public void registerListener(INetworkPolicyListener listener) {
+        try {
+            mService.registerListener(listener);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    public void unregisterListener(INetworkPolicyListener listener) {
+        try {
+            mService.unregisterListener(listener);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    public void setNetworkPolicies(NetworkPolicy[] policies) {
+        try {
+            mService.setNetworkPolicies(policies);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    @UnsupportedAppUsage
+    public NetworkPolicy[] getNetworkPolicies() {
+        try {
+            return mService.getNetworkPolicies(mContext.getOpPackageName());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    @UnsupportedAppUsage
+    public void setRestrictBackground(boolean restrictBackground) {
+        try {
+            mService.setRestrictBackground(restrictBackground);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    @UnsupportedAppUsage
+    public boolean getRestrictBackground() {
+        try {
+            return mService.getRestrictBackground();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Resets network policy settings back to factory defaults.
+     *
+     * @hide
+     */
+    public void factoryReset(String subscriber) {
+        try {
+            mService.factoryReset(subscriber);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /** {@hide} */
+    @Deprecated
+    public static Iterator<Pair<ZonedDateTime, ZonedDateTime>> cycleIterator(NetworkPolicy policy) {
+        final Iterator<Range<ZonedDateTime>> it = policy.cycleIterator();
+        return new Iterator<Pair<ZonedDateTime, ZonedDateTime>>() {
+            @Override
+            public boolean hasNext() {
+                return it.hasNext();
+            }
+
+            @Override
+            public Pair<ZonedDateTime, ZonedDateTime> next() {
+                if (hasNext()) {
+                    final Range<ZonedDateTime> r = it.next();
+                    return Pair.create(r.getLower(), r.getUpper());
+                } else {
+                    return Pair.create(null, null);
+                }
+            }
+        };
+    }
+
+    /**
+     * Check if given UID can have a {@link #setUidPolicy(int, int)} defined,
+     * usually to protect critical system services.
+     */
+    @Deprecated
+    public static boolean isUidValidForPolicy(Context context, int uid) {
+        // first, quick-reject non-applications
+        if (!UserHandle.isApp(uid)) {
+            return false;
+        }
+
+        if (!ALLOW_PLATFORM_APP_POLICY) {
+            final PackageManager pm = context.getPackageManager();
+            final HashSet<Signature> systemSignature;
+            try {
+                systemSignature = Sets.newHashSet(
+                        pm.getPackageInfo("android", GET_SIGNATURES).signatures);
+            } catch (NameNotFoundException e) {
+                throw new RuntimeException("problem finding system signature", e);
+            }
+
+            try {
+                // reject apps signed with platform cert
+                for (String packageName : pm.getPackagesForUid(uid)) {
+                    final HashSet<Signature> packageSignature = Sets.newHashSet(
+                            pm.getPackageInfo(packageName, GET_SIGNATURES).signatures);
+                    if (packageSignature.containsAll(systemSignature)) {
+                        return false;
+                    }
+                }
+            } catch (NameNotFoundException e) {
+            }
+        }
+
+        // nothing found above; we can apply policy to UID
+        return true;
+    }
+
+    /**
+     * @hide
+     */
+    public static String uidRulesToString(int uidRules) {
+        final StringBuilder string = new StringBuilder().append(uidRules).append(" (");
+        if (uidRules == RULE_NONE) {
+            string.append("NONE");
+        } else {
+            string.append(DebugUtils.flagsToString(NetworkPolicyManager.class, "RULE_", uidRules));
+        }
+        string.append(")");
+        return string.toString();
+    }
+
+    /**
+     * @hide
+     */
+    public static String uidPoliciesToString(int uidPolicies) {
+        final StringBuilder string = new StringBuilder().append(uidPolicies).append(" (");
+        if (uidPolicies == POLICY_NONE) {
+            string.append("NONE");
+        } else {
+            string.append(DebugUtils.flagsToString(NetworkPolicyManager.class,
+                    "POLICY_", uidPolicies));
+        }
+        string.append(")");
+        return string.toString();
+    }
+
+    /**
+     * Returns true if {@param procState} is considered foreground and as such will be allowed
+     * to access network when the device is idle or in battery saver mode. Otherwise, false.
+     */
+    public static boolean isProcStateAllowedWhileIdleOrPowerSaveMode(int procState) {
+        return procState <= FOREGROUND_THRESHOLD_STATE;
+    }
+
+    /**
+     * Returns true if {@param procState} is considered foreground and as such will be allowed
+     * to access network when the device is in data saver mode. Otherwise, false.
+     */
+    public static boolean isProcStateAllowedWhileOnRestrictBackground(int procState) {
+        return procState <= FOREGROUND_THRESHOLD_STATE;
+    }
+
+    public static String resolveNetworkId(WifiConfiguration config) {
+        return WifiInfo.removeDoubleQuotes(config.isPasspoint()
+                ? config.providerFriendlyName : config.SSID);
+    }
+
+    public static String resolveNetworkId(String ssid) {
+        return WifiInfo.removeDoubleQuotes(ssid);
+    }
+
+    /** {@hide} */
+    public static class Listener extends INetworkPolicyListener.Stub {
+        @Override public void onUidRulesChanged(int uid, int uidRules) { }
+        @Override public void onMeteredIfacesChanged(String[] meteredIfaces) { }
+        @Override public void onRestrictBackgroundChanged(boolean restrictBackground) { }
+        @Override public void onUidPoliciesChanged(int uid, int uidPolicies) { }
+        @Override public void onSubscriptionOverride(int subId, int overrideMask, int overrideValue) { }
+    }
+}
diff --git a/android/net/NetworkQuotaInfo.java b/android/net/NetworkQuotaInfo.java
new file mode 100644
index 0000000..a46cdde
--- /dev/null
+++ b/android/net/NetworkQuotaInfo.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2011 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.UnsupportedAppUsage;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * @deprecated nobody should be using this, but keep it around returning stub
+ *             values to prevent app crashes.
+ * @hide
+ */
+@Deprecated
+public class NetworkQuotaInfo implements Parcelable {
+    public static final long NO_LIMIT = -1;
+
+    /** {@hide} */
+    public NetworkQuotaInfo() {
+    }
+
+    /** {@hide} */
+    public NetworkQuotaInfo(Parcel in) {
+    }
+
+    @UnsupportedAppUsage
+    public long getEstimatedBytes() {
+        return 0;
+    }
+
+    @UnsupportedAppUsage
+    public long getSoftLimitBytes() {
+        return NO_LIMIT;
+    }
+
+    @UnsupportedAppUsage
+    public long getHardLimitBytes() {
+        return NO_LIMIT;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+    }
+
+    @UnsupportedAppUsage
+    public static final @android.annotation.NonNull Creator<NetworkQuotaInfo> CREATOR = new Creator<NetworkQuotaInfo>() {
+        @Override
+        public NetworkQuotaInfo createFromParcel(Parcel in) {
+            return new NetworkQuotaInfo(in);
+        }
+
+        @Override
+        public NetworkQuotaInfo[] newArray(int size) {
+            return new NetworkQuotaInfo[size];
+        }
+    };
+}
diff --git a/android/net/NetworkRecommendationProvider.java b/android/net/NetworkRecommendationProvider.java
new file mode 100644
index 0000000..a70c97b
--- /dev/null
+++ b/android/net/NetworkRecommendationProvider.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.net;
+
+import android.Manifest.permission;
+import android.annotation.SystemApi;
+import android.content.Context;
+import android.os.Build;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.concurrent.Executor;
+
+/**
+ * The base class for implementing a network recommendation provider.
+ * <p>
+ * A network recommendation provider is any application which:
+ * <ul>
+ * <li>Is granted the {@link permission#SCORE_NETWORKS} permission.
+ * <li>Is granted the {@link permission#ACCESS_COARSE_LOCATION} permission.
+ * <li>Includes a Service for the {@link NetworkScoreManager#ACTION_RECOMMEND_NETWORKS} intent
+ *     which is protected by the {@link permission#BIND_NETWORK_RECOMMENDATION_SERVICE} permission.
+ * </ul>
+ * <p>
+ * Implementations are required to implement the abstract methods in this class and return the
+ * result of {@link #getBinder()} from the <code>onBind()</code> method in their Service.
+ * <p>
+ * The default network recommendation provider is controlled via the
+ * <code>config_defaultNetworkRecommendationProviderPackage</code> config key.
+ * @hide
+ */
+@SystemApi
+public abstract class NetworkRecommendationProvider {
+    private static final String TAG = "NetworkRecProvider";
+    private static final boolean VERBOSE = Build.IS_DEBUGGABLE && Log.isLoggable(TAG, Log.VERBOSE);
+    private final IBinder mService;
+
+    /**
+     * Constructs a new instance.
+     * @param context the current context instance. Cannot be {@code null}.
+     * @param executor used to execute the incoming requests. Cannot be {@code null}.
+     */
+    public NetworkRecommendationProvider(Context context, Executor executor) {
+        Preconditions.checkNotNull(context);
+        Preconditions.checkNotNull(executor);
+        mService = new ServiceWrapper(context, executor);
+    }
+
+    /**
+     * Invoked when network scores have been requested.
+     * <p>
+     * Use {@link NetworkScoreManager#updateScores(ScoredNetwork[])} to respond to score requests.
+     *
+     * @param networks a non-empty array of {@link NetworkKey}s to score.
+     */
+    public abstract void onRequestScores(NetworkKey[] networks);
+
+    /**
+     * 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;
+    }
+
+    /**
+     * A wrapper around INetworkRecommendationProvider that dispatches to the provided Handler.
+     */
+    private final class ServiceWrapper extends INetworkRecommendationProvider.Stub {
+        private final Context mContext;
+        private final Executor mExecutor;
+        private final Handler mHandler;
+
+        ServiceWrapper(Context context, Executor executor) {
+            mContext = context;
+            mExecutor = executor;
+            mHandler = null;
+        }
+
+        @Override
+        public void requestScores(final NetworkKey[] networks) throws RemoteException {
+            enforceCallingPermission();
+            if (networks != null && networks.length > 0) {
+                execute(new Runnable() {
+                    @Override
+                    public void run() {
+                        onRequestScores(networks);
+                    }
+                });
+            }
+        }
+
+        private void execute(Runnable command) {
+            if (mExecutor != null) {
+                mExecutor.execute(command);
+            } else {
+                mHandler.post(command);
+            }
+        }
+
+        private void enforceCallingPermission() {
+            if (mContext != null) {
+                mContext.enforceCallingOrSelfPermission(permission.REQUEST_NETWORK_SCORES,
+                        "Permission denied.");
+            }
+        }
+    }
+}
diff --git a/android/net/NetworkRequest.java b/android/net/NetworkRequest.java
new file mode 100644
index 0000000..4270740
--- /dev/null
+++ b/android/net/NetworkRequest.java
@@ -0,0 +1,511 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.annotation.UnsupportedAppUsage;
+import android.net.NetworkCapabilities.NetCapability;
+import android.net.NetworkCapabilities.Transport;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.Process;
+import android.text.TextUtils;
+import android.util.proto.ProtoOutputStream;
+
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Defines a request for a network, made through {@link NetworkRequest.Builder} and used
+ * to request a network via {@link ConnectivityManager#requestNetwork} or listen for changes
+ * via {@link ConnectivityManager#registerNetworkCallback}.
+ */
+public class NetworkRequest implements Parcelable {
+    /**
+     * The {@link NetworkCapabilities} that define this request.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public final @NonNull NetworkCapabilities networkCapabilities;
+
+    /**
+     * Identifies the request.  NetworkRequests should only be constructed by
+     * the Framework and given out to applications as tokens to be used to identify
+     * the request.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public final int requestId;
+
+    /**
+     * Set for legacy requests and the default.  Set to TYPE_NONE for none.
+     * Causes CONNECTIVITY_ACTION broadcasts to be sent.
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    public final int legacyType;
+
+    /**
+     * A NetworkRequest as used by the system can be one of the following types:
+     *
+     *     - LISTEN, for which the framework will issue callbacks about any
+     *       and all networks that match the specified NetworkCapabilities,
+     *
+     *     - REQUEST, capable of causing a specific network to be created
+     *       first (e.g. a telephony DUN request), the framework will issue
+     *       callbacks about the single, highest scoring current network
+     *       (if any) that matches the specified NetworkCapabilities, or
+     *
+     *     - TRACK_DEFAULT, a hybrid of the two designed such that the
+     *       framework will issue callbacks for the single, highest scoring
+     *       current network (if any) that matches the capabilities of the
+     *       default Internet request (mDefaultRequest), but which cannot cause
+     *       the framework to either create or retain the existence of any
+     *       specific network. Note that from the point of view of the request
+     *       matching code, TRACK_DEFAULT is identical to REQUEST: its special
+     *       behaviour is not due to different semantics, but to the fact that
+     *       the system will only ever create a TRACK_DEFAULT with capabilities
+     *       that are identical to the default request's capabilities, thus
+     *       causing it to share fate in every way with the default request.
+     *
+     *     - BACKGROUND_REQUEST, like REQUEST but does not cause any networks
+     *       to retain the NET_CAPABILITY_FOREGROUND capability. A network with
+     *       no foreground requests is in the background. A network that has
+     *       one or more background requests and loses its last foreground
+     *       request to a higher-scoring network will not go into the
+     *       background immediately, but will linger and go into the background
+     *       after the linger timeout.
+     *
+     *     - The value NONE is used only by applications. When an application
+     *       creates a NetworkRequest, it does not have a type; the type is set
+     *       by the system depending on the method used to file the request
+     *       (requestNetwork, registerNetworkCallback, etc.).
+     *
+     * @hide
+     */
+    public static enum Type {
+        NONE,
+        LISTEN,
+        TRACK_DEFAULT,
+        REQUEST,
+        BACKGROUND_REQUEST,
+    };
+
+    /**
+     * The type of the request. This is only used by the system and is always NONE elsewhere.
+     *
+     * @hide
+     */
+    public final Type type;
+
+    /**
+     * @hide
+     */
+    public NetworkRequest(NetworkCapabilities nc, int legacyType, int rId, Type type) {
+        if (nc == null) {
+            throw new NullPointerException();
+        }
+        requestId = rId;
+        networkCapabilities = nc;
+        this.legacyType = legacyType;
+        this.type = type;
+    }
+
+    /**
+     * @hide
+     */
+    public NetworkRequest(NetworkRequest that) {
+        networkCapabilities = new NetworkCapabilities(that.networkCapabilities);
+        requestId = that.requestId;
+        this.legacyType = that.legacyType;
+        this.type = that.type;
+    }
+
+    /**
+     * Builder used to create {@link NetworkRequest} objects.  Specify the Network features
+     * needed in terms of {@link NetworkCapabilities} features
+     */
+    public static class Builder {
+        private final NetworkCapabilities mNetworkCapabilities;
+
+        /**
+         * Default constructor for Builder.
+         */
+        public Builder() {
+            // By default, restrict this request to networks available to this app.
+            // Apps can rescind this restriction, but ConnectivityService will enforce
+            // it for apps that do not have the NETWORK_SETTINGS permission.
+            mNetworkCapabilities = new NetworkCapabilities();
+            mNetworkCapabilities.setSingleUid(Process.myUid());
+        }
+
+        /**
+         * Build {@link NetworkRequest} give the current set of capabilities.
+         */
+        public NetworkRequest build() {
+            // Make a copy of mNetworkCapabilities so we don't inadvertently remove NOT_RESTRICTED
+            // when later an unrestricted capability could be added to mNetworkCapabilities, in
+            // which case NOT_RESTRICTED should be returned to mNetworkCapabilities, which
+            // maybeMarkCapabilitiesRestricted() doesn't add back.
+            final NetworkCapabilities nc = new NetworkCapabilities(mNetworkCapabilities);
+            nc.maybeMarkCapabilitiesRestricted();
+            return new NetworkRequest(nc, ConnectivityManager.TYPE_NONE,
+                    ConnectivityManager.REQUEST_ID_UNSET, Type.NONE);
+        }
+
+        /**
+         * Add the given capability requirement to this builder.  These represent
+         * the requested network's required capabilities.  Note that when searching
+         * for a network to satisfy a request, all capabilities requested must be
+         * satisfied.
+         *
+         * @param capability The capability to add.
+         * @return The builder to facilitate chaining
+         *         {@code builder.addCapability(...).addCapability();}.
+         */
+        public Builder addCapability(@NetworkCapabilities.NetCapability int capability) {
+            mNetworkCapabilities.addCapability(capability);
+            return this;
+        }
+
+        /**
+         * Removes (if found) the given capability from this builder instance.
+         *
+         * @param capability The capability to remove.
+         * @return The builder to facilitate chaining.
+         */
+        public Builder removeCapability(@NetworkCapabilities.NetCapability int capability) {
+            mNetworkCapabilities.removeCapability(capability);
+            return this;
+        }
+
+        /**
+         * Set the {@code NetworkCapabilities} for this builder instance,
+         * overriding any capabilities that had been previously set.
+         *
+         * @param nc The superseding {@code NetworkCapabilities} instance.
+         * @return The builder to facilitate chaining.
+         * @hide
+         */
+        public Builder setCapabilities(NetworkCapabilities nc) {
+            mNetworkCapabilities.set(nc);
+            return this;
+        }
+
+        /**
+         * Set the watched UIDs for this request. This will be reset and wiped out unless
+         * the calling app holds the CHANGE_NETWORK_STATE permission.
+         *
+         * @param uids The watched UIDs as a set of UidRanges, or null for everything.
+         * @return The builder to facilitate chaining.
+         * @hide
+         */
+        public Builder setUids(Set<UidRange> uids) {
+            mNetworkCapabilities.setUids(uids);
+            return this;
+        }
+
+        /**
+         * Add a capability that must not exist in the requested network.
+         * <p>
+         * If the capability was previously added to the list of required capabilities (for
+         * example, it was there by default or added using {@link #addCapability(int)} method), then
+         * it will be removed from the list of required capabilities as well.
+         *
+         * @see #addCapability(int)
+         *
+         * @param capability The capability to add to unwanted capability list.
+         * @return The builder to facilitate chaining.
+         *
+         * @hide
+         */
+        public Builder addUnwantedCapability(@NetworkCapabilities.NetCapability int capability) {
+            mNetworkCapabilities.addUnwantedCapability(capability);
+            return this;
+        }
+
+        /**
+         * Completely clears all the {@code NetworkCapabilities} from this builder instance,
+         * removing even the capabilities that are set by default when the object is constructed.
+         *
+         * @return The builder to facilitate chaining.
+         * @hide
+         */
+        @UnsupportedAppUsage
+        public Builder clearCapabilities() {
+            mNetworkCapabilities.clearAll();
+            return this;
+        }
+
+        /**
+         * Adds the given transport requirement to this builder.  These represent
+         * the set of allowed transports for the request.  Only networks using one
+         * of these transports will satisfy the request.  If no particular transports
+         * are required, none should be specified here.
+         *
+         * @param transportType The transport type to add.
+         * @return The builder to facilitate chaining.
+         */
+        public Builder addTransportType(@NetworkCapabilities.Transport int transportType) {
+            mNetworkCapabilities.addTransportType(transportType);
+            return this;
+        }
+
+        /**
+         * Removes (if found) the given transport from this builder instance.
+         *
+         * @param transportType The transport type to remove.
+         * @return The builder to facilitate chaining.
+         */
+        public Builder removeTransportType(@NetworkCapabilities.Transport int transportType) {
+            mNetworkCapabilities.removeTransportType(transportType);
+            return this;
+        }
+
+        /**
+         * @hide
+         */
+        public Builder setLinkUpstreamBandwidthKbps(int upKbps) {
+            mNetworkCapabilities.setLinkUpstreamBandwidthKbps(upKbps);
+            return this;
+        }
+        /**
+         * @hide
+         */
+        public Builder setLinkDownstreamBandwidthKbps(int downKbps) {
+            mNetworkCapabilities.setLinkDownstreamBandwidthKbps(downKbps);
+            return this;
+        }
+
+        /**
+         * Sets the optional bearer specific network specifier.
+         * This has no meaning if a single transport is also not specified, so calling
+         * this without a single transport set will generate an exception, as will
+         * subsequently adding or removing transports after this is set.
+         * </p>
+         * The interpretation of this {@code String} is bearer specific and bearers that use
+         * it should document their particulars.  For example, Bluetooth may use some sort of
+         * device id while WiFi could used ssid and/or bssid.  Cellular may use carrier spn.
+         *
+         * @param networkSpecifier An {@code String} of opaque format used to specify the bearer
+         *                         specific network specifier where the bearer has a choice of
+         *                         networks.
+         */
+        public Builder setNetworkSpecifier(String networkSpecifier) {
+            /*
+             * A StringNetworkSpecifier does not accept null or empty ("") strings. When network
+             * specifiers were strings a null string and an empty string were considered equivalent.
+             * Hence no meaning is attached to a null or empty ("") string.
+             */
+            return setNetworkSpecifier(TextUtils.isEmpty(networkSpecifier) ? null
+                    : new StringNetworkSpecifier(networkSpecifier));
+        }
+
+        /**
+         * Sets the optional bearer specific network specifier.
+         * This has no meaning if a single transport is also not specified, so calling
+         * this without a single transport set will generate an exception, as will
+         * subsequently adding or removing transports after this is set.
+         * </p>
+         *
+         * @param networkSpecifier A concrete, parcelable framework class that extends
+         *                         NetworkSpecifier.
+         */
+        public Builder setNetworkSpecifier(NetworkSpecifier networkSpecifier) {
+            MatchAllNetworkSpecifier.checkNotMatchAllNetworkSpecifier(networkSpecifier);
+            mNetworkCapabilities.setNetworkSpecifier(networkSpecifier);
+            return this;
+        }
+
+        /**
+         * Sets the signal strength. This is a signed integer, with higher values indicating a
+         * stronger signal. The exact units are bearer-dependent. For example, Wi-Fi uses the same
+         * RSSI units reported by WifiManager.
+         * <p>
+         * Note that when used to register a network callback, this specifies the minimum acceptable
+         * signal strength. When received as the state of an existing network it specifies the
+         * current value. A value of {@code SIGNAL_STRENGTH_UNSPECIFIED} means no value when
+         * received and has no effect when requesting a callback.
+         *
+         * <p>This method requires the caller to hold the
+         * {@link android.Manifest.permission#NETWORK_SIGNAL_STRENGTH_WAKEUP} permission
+         *
+         * @param signalStrength the bearer-specific signal strength.
+         * @hide
+         */
+        @SystemApi
+        @RequiresPermission(android.Manifest.permission.NETWORK_SIGNAL_STRENGTH_WAKEUP)
+        public @NonNull Builder setSignalStrength(int signalStrength) {
+            mNetworkCapabilities.setSignalStrength(signalStrength);
+            return this;
+        }
+    }
+
+    // implement the Parcelable interface
+    public int describeContents() {
+        return 0;
+    }
+    public void writeToParcel(Parcel dest, int flags) {
+        networkCapabilities.writeToParcel(dest, flags);
+        dest.writeInt(legacyType);
+        dest.writeInt(requestId);
+        dest.writeString(type.name());
+    }
+    public static final @android.annotation.NonNull Creator<NetworkRequest> CREATOR =
+        new Creator<NetworkRequest>() {
+            public NetworkRequest createFromParcel(Parcel in) {
+                NetworkCapabilities nc = NetworkCapabilities.CREATOR.createFromParcel(in);
+                int legacyType = in.readInt();
+                int requestId = in.readInt();
+                Type type = Type.valueOf(in.readString());  // IllegalArgumentException if invalid.
+                NetworkRequest result = new NetworkRequest(nc, legacyType, requestId, type);
+                return result;
+            }
+            public NetworkRequest[] newArray(int size) {
+                return new NetworkRequest[size];
+            }
+        };
+
+    /**
+     * Returns true iff. this NetworkRequest is of type LISTEN.
+     *
+     * @hide
+     */
+    public boolean isListen() {
+        return type == Type.LISTEN;
+    }
+
+    /**
+     * Returns true iff. the contained NetworkRequest is one that:
+     *
+     *     - should be associated with at most one satisfying network
+     *       at a time;
+     *
+     *     - should cause a network to be kept up, but not necessarily in
+     *       the foreground, if it is the best network which can satisfy the
+     *       NetworkRequest.
+     *
+     * For full detail of how isRequest() is used for pairing Networks with
+     * NetworkRequests read rematchNetworkAndRequests().
+     *
+     * @hide
+     */
+    public boolean isRequest() {
+        return isForegroundRequest() || isBackgroundRequest();
+    }
+
+    /**
+     * Returns true iff. the contained NetworkRequest is one that:
+     *
+     *     - should be associated with at most one satisfying network
+     *       at a time;
+     *
+     *     - should cause a network to be kept up and in the foreground if
+     *       it is the best network which can satisfy the NetworkRequest.
+     *
+     * For full detail of how isRequest() is used for pairing Networks with
+     * NetworkRequests read rematchNetworkAndRequests().
+     *
+     * @hide
+     */
+    public boolean isForegroundRequest() {
+        return type == Type.TRACK_DEFAULT || type == Type.REQUEST;
+    }
+
+    /**
+     * Returns true iff. this NetworkRequest is of type BACKGROUND_REQUEST.
+     *
+     * @hide
+     */
+    public boolean isBackgroundRequest() {
+        return type == Type.BACKGROUND_REQUEST;
+    }
+
+    /**
+     * @see Builder#addCapability(int)
+     */
+    public boolean hasCapability(@NetCapability int capability) {
+        return networkCapabilities.hasCapability(capability);
+    }
+
+    /**
+     * @see Builder#addUnwantedCapability(int)
+     *
+     * @hide
+     */
+    public boolean hasUnwantedCapability(@NetCapability int capability) {
+        return networkCapabilities.hasUnwantedCapability(capability);
+    }
+
+    /**
+     * @see Builder#addTransportType(int)
+     */
+    public boolean hasTransport(@Transport int transportType) {
+        return networkCapabilities.hasTransport(transportType);
+    }
+
+    public String toString() {
+        return "NetworkRequest [ " + type + " id=" + requestId +
+                (legacyType != ConnectivityManager.TYPE_NONE ? ", legacyType=" + legacyType : "") +
+                ", " + networkCapabilities.toString() + " ]";
+    }
+
+    private int typeToProtoEnum(Type t) {
+        switch (t) {
+            case NONE:
+                return NetworkRequestProto.TYPE_NONE;
+            case LISTEN:
+                return NetworkRequestProto.TYPE_LISTEN;
+            case TRACK_DEFAULT:
+                return NetworkRequestProto.TYPE_TRACK_DEFAULT;
+            case REQUEST:
+                return NetworkRequestProto.TYPE_REQUEST;
+            case BACKGROUND_REQUEST:
+                return NetworkRequestProto.TYPE_BACKGROUND_REQUEST;
+            default:
+                return NetworkRequestProto.TYPE_UNKNOWN;
+        }
+    }
+
+    /** @hide */
+    public void writeToProto(ProtoOutputStream proto, long fieldId) {
+        final long token = proto.start(fieldId);
+
+        proto.write(NetworkRequestProto.TYPE, typeToProtoEnum(type));
+        proto.write(NetworkRequestProto.REQUEST_ID, requestId);
+        proto.write(NetworkRequestProto.LEGACY_TYPE, legacyType);
+        networkCapabilities.writeToProto(proto, NetworkRequestProto.NETWORK_CAPABILITIES);
+
+        proto.end(token);
+    }
+
+    public boolean equals(Object obj) {
+        if (obj instanceof NetworkRequest == false) return false;
+        NetworkRequest that = (NetworkRequest)obj;
+        return (that.legacyType == this.legacyType &&
+                that.requestId == this.requestId &&
+                that.type == this.type &&
+                Objects.equals(that.networkCapabilities, this.networkCapabilities));
+    }
+
+    public int hashCode() {
+        return Objects.hash(requestId, legacyType, networkCapabilities, type);
+    }
+}
diff --git a/android/net/NetworkScoreManager.java b/android/net/NetworkScoreManager.java
new file mode 100644
index 0000000..50dd468
--- /dev/null
+++ b/android/net/NetworkScoreManager.java
@@ -0,0 +1,450 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.net;
+
+import android.Manifest.permission;
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.SystemApi;
+import android.annotation.SystemService;
+import android.content.Context;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.ServiceManager.ServiceNotFoundException;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+
+/**
+ * Class that manages communication between network subsystems and a network scorer.
+ *
+ * <p>A network scorer is any application which:
+ * <ul>
+ * <li>Is granted the {@link permission#SCORE_NETWORKS} permission.
+ * <li>Is granted the {@link permission#ACCESS_COARSE_LOCATION} permission.
+ * <li>Include a Service for the {@link #ACTION_RECOMMEND_NETWORKS} action
+ *     protected by the {@link permission#BIND_NETWORK_RECOMMENDATION_SERVICE}
+ *     permission.
+ * </ul>
+ *
+ * @hide
+ */
+@SystemApi
+@SystemService(Context.NETWORK_SCORE_SERVICE)
+public class NetworkScoreManager {
+    /**
+     * Activity action: ask the user to change the active network scorer. This will show a dialog
+     * that asks the user whether they want to replace the current active scorer with the one
+     * specified in {@link #EXTRA_PACKAGE_NAME}. The activity will finish with RESULT_OK if the
+     * active scorer was changed or RESULT_CANCELED if it failed for any reason.
+     */
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_CHANGE_ACTIVE = "android.net.scoring.CHANGE_ACTIVE";
+
+    /**
+     * Extra used with {@link #ACTION_CHANGE_ACTIVE} to specify the new scorer package. Set with
+     * {@link android.content.Intent#putExtra(String, String)}.
+     */
+    public static final String EXTRA_PACKAGE_NAME = "packageName";
+
+    /**
+     * Broadcast action: new network scores are being requested. This intent will only be delivered
+     * to the current active scorer app. That app is responsible for scoring the networks and
+     * calling {@link #updateScores} when complete. The networks to score are specified in
+     * {@link #EXTRA_NETWORKS_TO_SCORE}, and will generally consist of all networks which have been
+     * configured by the user as well as any open networks.
+     *
+     * <p class="note">This is a protected intent that can only be sent by the system.
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String ACTION_SCORE_NETWORKS = "android.net.scoring.SCORE_NETWORKS";
+
+    /**
+     * Extra used with {@link #ACTION_SCORE_NETWORKS} to specify the networks to be scored, as an
+     * array of {@link NetworkKey}s. Can be obtained with
+     * {@link android.content.Intent#getParcelableArrayExtra(String)}}.
+     */
+    public static final String EXTRA_NETWORKS_TO_SCORE = "networksToScore";
+
+    /**
+     * Activity action: launch an activity for configuring a provider for the feature that connects
+     * and secures open wifi networks available before enabling it. Applications that enable this
+     * feature must provide an activity for this action. The framework will launch this activity
+     * which must return RESULT_OK if the feature should be enabled.
+     */
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_CUSTOM_ENABLE = "android.net.scoring.CUSTOM_ENABLE";
+
+    /**
+     * Meta-data specified on a {@link NetworkRecommendationProvider} that provides a user-visible
+     * label of the recommendation service.
+     * @hide
+     */
+    public static final String RECOMMENDATION_SERVICE_LABEL_META_DATA =
+            "android.net.scoring.recommendation_service_label";
+
+    /**
+     * Meta-data specified on a {@link NetworkRecommendationProvider} that specified the package
+     * name of the application that connects and secures open wifi networks automatically. The
+     * specified package must provide an Activity for {@link #ACTION_CUSTOM_ENABLE}.
+     * @hide
+     */
+    public static final String USE_OPEN_WIFI_PACKAGE_META_DATA =
+            "android.net.wifi.use_open_wifi_package";
+
+    /**
+     * Meta-data specified on a {@link NetworkRecommendationProvider} that specifies the
+     * {@link android.app.NotificationChannel} ID used to post open network notifications.
+     * @hide
+     */
+    public static final String NETWORK_AVAILABLE_NOTIFICATION_CHANNEL_ID_META_DATA =
+            "android.net.wifi.notification_channel_id_network_available";
+
+    /**
+     * Broadcast action: the active scorer has been changed. Scorer apps may listen to this to
+     * perform initialization once selected as the active scorer, or clean up unneeded resources
+     * if another scorer has been selected. This is an explicit broadcast only sent to the
+     * previous scorer and new scorer. Note that it is unnecessary to clear existing scores as
+     * this is handled by the system.
+     *
+     * <p>The new scorer will be specified in {@link #EXTRA_NEW_SCORER}.
+     *
+     * <p class="note">This is a protected intent that can only be sent by the system.
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    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)}.
+     */
+    public static final String EXTRA_NEW_SCORER = "newScorer";
+
+    /** @hide */
+    @IntDef({CACHE_FILTER_NONE, CACHE_FILTER_CURRENT_NETWORK, CACHE_FILTER_SCAN_RESULTS})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface CacheUpdateFilter {}
+
+    /**
+     * Do not filter updates sent to the cache.
+     * @hide
+     */
+    public static final int CACHE_FILTER_NONE = 0;
+
+    /**
+     * Only send cache updates when the network matches the connected network.
+     * @hide
+     */
+    public static final int CACHE_FILTER_CURRENT_NETWORK = 1;
+
+    /**
+     * Only send cache updates when the network is part of the current scan result set.
+     * @hide
+     */
+    public static final int CACHE_FILTER_SCAN_RESULTS = 2;
+
+    /** @hide */
+    @IntDef({RECOMMENDATIONS_ENABLED_FORCED_OFF, RECOMMENDATIONS_ENABLED_OFF,
+            RECOMMENDATIONS_ENABLED_ON})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface RecommendationsEnabledSetting {}
+
+    /**
+     * Recommendations have been forced off.
+     * <p>
+     * This value is never set by any of the NetworkScore classes, it must be set via other means.
+     * This state is also "sticky" and we won't transition out of this state once entered. To move
+     * to a different state this value has to be explicitly set to a different value via
+     * other means.
+     * @hide
+     */
+    public static final int RECOMMENDATIONS_ENABLED_FORCED_OFF = -1;
+
+    /**
+     * Recommendations are not enabled.
+     * <p>
+     * This is a transient state that can be entered when the default recommendation app is enabled
+     * but no longer valid. This state will transition to RECOMMENDATIONS_ENABLED_ON when a valid
+     * recommendation app is enabled.
+     * @hide
+     */
+    public static final int RECOMMENDATIONS_ENABLED_OFF = 0;
+
+    /**
+     * Recommendations are enabled.
+     * <p>
+     * This is a transient state that means a valid recommendation app is active. This state will
+     * transition to RECOMMENDATIONS_ENABLED_OFF if the current and default recommendation apps
+     * become invalid.
+     * @hide
+     */
+    public static final int RECOMMENDATIONS_ENABLED_ON = 1;
+
+    private final Context mContext;
+    private final INetworkScoreService mService;
+
+    /** @hide */
+    public NetworkScoreManager(Context context) throws ServiceNotFoundException {
+        mContext = context;
+        mService = INetworkScoreService.Stub
+                .asInterface(ServiceManager.getServiceOrThrow(Context.NETWORK_SCORE_SERVICE));
+    }
+
+    /**
+     * Obtain the package name of the current active network scorer.
+     *
+     * <p>At any time, only one scorer application will receive {@link #ACTION_SCORE_NETWORKS}
+     * broadcasts and be allowed to call {@link #updateScores}. Applications may use this method to
+     * determine the current scorer and offer the user the ability to select a different scorer via
+     * the {@link #ACTION_CHANGE_ACTIVE} intent.
+     * @return the full package name of the current active scorer, or null if there is no active
+     *         scorer.
+     * @throws SecurityException if the caller doesn't hold either {@link permission#SCORE_NETWORKS}
+     *                           or {@link permission#REQUEST_NETWORK_SCORES} permissions.
+     */
+    @RequiresPermission(anyOf = {android.Manifest.permission.SCORE_NETWORKS,
+                                 android.Manifest.permission.REQUEST_NETWORK_SCORES})
+    public String getActiveScorerPackage() {
+        try {
+            return mService.getActiveScorerPackage();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns metadata about the active scorer or <code>null</code> if there is no active scorer.
+     *
+     * @throws SecurityException if the caller does not hold the
+     *         {@link permission#REQUEST_NETWORK_SCORES} permission.
+     * @hide
+     */
+    @Nullable
+    @RequiresPermission(android.Manifest.permission.REQUEST_NETWORK_SCORES)
+    public NetworkScorerAppData getActiveScorer() {
+        try {
+            return mService.getActiveScorer();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns the list of available scorer apps. The list will be empty if there are
+     * no valid scorers.
+     *
+     * @throws SecurityException if the caller does not hold the
+     *         {@link permission#REQUEST_NETWORK_SCORES} permission.
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.REQUEST_NETWORK_SCORES)
+    public List<NetworkScorerAppData> getAllValidScorers() {
+        try {
+            return mService.getAllValidScorers();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Update network scores.
+     *
+     * <p>This may be called at any time to re-score active networks. Scores will generally be
+     * updated quickly, but if this method is called too frequently, the scores may be held and
+     * applied at a later time.
+     *
+     * @param networks the networks which have been scored by the scorer.
+     * @return whether the update was successful.
+     * @throws SecurityException if the caller is not the active scorer.
+     */
+    @RequiresPermission(android.Manifest.permission.SCORE_NETWORKS)
+    public boolean updateScores(ScoredNetwork[] networks) throws SecurityException {
+        try {
+            return mService.updateScores(networks);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Clear network scores.
+     *
+     * <p>Should be called when all scores need to be invalidated, i.e. because the scoring
+     * algorithm has changed and old scores can no longer be compared to future scores.
+     *
+     * <p>Note that scores will be cleared automatically when the active scorer changes, as scores
+     * from one scorer cannot be compared to those from another scorer.
+     *
+     * @return whether the clear was successful.
+     * @throws SecurityException if the caller is not the active scorer or if the caller doesn't
+     *                           hold the {@link permission#REQUEST_NETWORK_SCORES} permission.
+     */
+    @RequiresPermission(anyOf = {android.Manifest.permission.SCORE_NETWORKS,
+                                 android.Manifest.permission.REQUEST_NETWORK_SCORES})
+    public boolean clearScores() throws SecurityException {
+        try {
+            return mService.clearScores();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Set the active scorer to a new package and clear existing scores.
+     *
+     * <p>Should never be called directly without obtaining user consent. This can be done by using
+     * the {@link #ACTION_CHANGE_ACTIVE} broadcast, or using a custom configuration activity.
+     *
+     * @return true if the operation succeeded, or false if the new package is not a valid scorer.
+     * @throws SecurityException if the caller doesn't hold either {@link permission#SCORE_NETWORKS}
+     *                           or {@link permission#REQUEST_NETWORK_SCORES} permissions.
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(anyOf = {android.Manifest.permission.SCORE_NETWORKS,
+                                 android.Manifest.permission.REQUEST_NETWORK_SCORES})
+    public boolean setActiveScorer(String packageName) throws SecurityException {
+        try {
+            return mService.setActiveScorer(packageName);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Turn off network scoring.
+     *
+     * <p>May only be called by the current scorer app, or the system.
+     *
+     * @throws SecurityException if the caller is not the active scorer or if the caller doesn't
+     *                           hold the {@link permission#REQUEST_NETWORK_SCORES} permission.
+     */
+    @RequiresPermission(anyOf = {android.Manifest.permission.SCORE_NETWORKS,
+                                 android.Manifest.permission.REQUEST_NETWORK_SCORES})
+    public void disableScoring() throws SecurityException {
+        try {
+            mService.disableScoring();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Request scoring for networks.
+     *
+     * @return true if the broadcast was sent, or false if there is no active scorer.
+     * @throws SecurityException if the caller does not hold the
+     *         {@link permission#REQUEST_NETWORK_SCORES} permission.
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.REQUEST_NETWORK_SCORES)
+    public boolean requestScores(NetworkKey[] networks) throws SecurityException {
+        try {
+            return mService.requestScores(networks);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Register a network score cache.
+     *
+     * @param networkType the type of network this cache can handle. See {@link NetworkKey#type}.
+     * @param scoreCache implementation of {@link INetworkScoreCache} to store the scores.
+     * @throws SecurityException if the caller does not hold the
+     *         {@link permission#REQUEST_NETWORK_SCORES} permission.
+     * @throws IllegalArgumentException if a score cache is already registered for this type.
+     * @deprecated equivalent to registering for cache updates with CACHE_FILTER_NONE.
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.REQUEST_NETWORK_SCORES)
+    @Deprecated // migrate to registerNetworkScoreCache(int, INetworkScoreCache, int)
+    public void registerNetworkScoreCache(int networkType, INetworkScoreCache scoreCache) {
+        registerNetworkScoreCache(networkType, scoreCache, CACHE_FILTER_NONE);
+    }
+
+    /**
+     * Register a network score cache.
+     *
+     * @param networkType the type of network this cache can handle. See {@link NetworkKey#type}
+     * @param scoreCache implementation of {@link INetworkScoreCache} to store the scores
+     * @param filterType the {@link CacheUpdateFilter} to apply
+     * @throws SecurityException if the caller does not hold the
+     *         {@link permission#REQUEST_NETWORK_SCORES} permission.
+     * @throws IllegalArgumentException if a score cache is already registered for this type.
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.REQUEST_NETWORK_SCORES)
+    public void registerNetworkScoreCache(int networkType, INetworkScoreCache scoreCache,
+            @CacheUpdateFilter int filterType) {
+        try {
+            mService.registerNetworkScoreCache(networkType, scoreCache, filterType);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Unregister a network score cache.
+     *
+     * @param networkType the type of network this cache can handle. See {@link NetworkKey#type}.
+     * @param scoreCache implementation of {@link INetworkScoreCache} to store the scores.
+     * @throws SecurityException if the caller does not hold the
+     *         {@link permission#REQUEST_NETWORK_SCORES} permission.
+     * @throws IllegalArgumentException if a score cache is already registered for this type.
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.REQUEST_NETWORK_SCORES)
+    public void unregisterNetworkScoreCache(int networkType, INetworkScoreCache scoreCache) {
+        try {
+            mService.unregisterNetworkScoreCache(networkType, scoreCache);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Determine whether the application with the given UID is the enabled scorer.
+     *
+     * @param callingUid the UID to check
+     * @return true if the provided UID is the active scorer, false otherwise.
+     * @throws SecurityException if the caller does not hold the
+     *         {@link permission#REQUEST_NETWORK_SCORES} permission.
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.REQUEST_NETWORK_SCORES)
+    public boolean isCallerActiveScorer(int callingUid) {
+        try {
+            return mService.isCallerActiveScorer(callingUid);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+}
diff --git a/android/net/NetworkScorerAppData.java b/android/net/NetworkScorerAppData.java
new file mode 100644
index 0000000..116e39e
--- /dev/null
+++ b/android/net/NetworkScorerAppData.java
@@ -0,0 +1,130 @@
+package android.net;
+
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+
+/**
+ * Holds metadata about a discovered network scorer/recommendation application.
+ *
+ * @hide
+ */
+public final class NetworkScorerAppData implements Parcelable {
+    /** UID of the scorer app. */
+    public final int packageUid;
+    private final ComponentName mRecommendationService;
+    /** User visible label in Settings for the recommendation service. */
+    private final String mRecommendationServiceLabel;
+    /**
+     * The {@link ComponentName} of the Activity to start before enabling the "connect to open
+     * wifi networks automatically" feature.
+     */
+    private final ComponentName mEnableUseOpenWifiActivity;
+    /**
+     * The {@link android.app.NotificationChannel} ID used by {@link #mRecommendationService} to
+     * post open network notifications.
+     */
+    private final String mNetworkAvailableNotificationChannelId;
+
+    public NetworkScorerAppData(int packageUid, ComponentName recommendationServiceComp,
+            String recommendationServiceLabel, ComponentName enableUseOpenWifiActivity,
+            String networkAvailableNotificationChannelId) {
+        this.packageUid = packageUid;
+        this.mRecommendationService = recommendationServiceComp;
+        this.mRecommendationServiceLabel = recommendationServiceLabel;
+        this.mEnableUseOpenWifiActivity = enableUseOpenWifiActivity;
+        this.mNetworkAvailableNotificationChannelId = networkAvailableNotificationChannelId;
+    }
+
+    protected NetworkScorerAppData(Parcel in) {
+        packageUid = in.readInt();
+        mRecommendationService = ComponentName.readFromParcel(in);
+        mRecommendationServiceLabel = in.readString();
+        mEnableUseOpenWifiActivity = ComponentName.readFromParcel(in);
+        mNetworkAvailableNotificationChannelId = in.readString();
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(packageUid);
+        ComponentName.writeToParcel(mRecommendationService, dest);
+        dest.writeString(mRecommendationServiceLabel);
+        ComponentName.writeToParcel(mEnableUseOpenWifiActivity, dest);
+        dest.writeString(mNetworkAvailableNotificationChannelId);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public static final @android.annotation.NonNull Creator<NetworkScorerAppData> CREATOR =
+            new Creator<NetworkScorerAppData>() {
+                @Override
+                public NetworkScorerAppData createFromParcel(Parcel in) {
+                    return new NetworkScorerAppData(in);
+                }
+
+                @Override
+                public NetworkScorerAppData[] newArray(int size) {
+                    return new NetworkScorerAppData[size];
+                }
+            };
+
+    public String getRecommendationServicePackageName() {
+        return mRecommendationService.getPackageName();
+    }
+
+    public ComponentName getRecommendationServiceComponent() {
+        return mRecommendationService;
+    }
+
+    @Nullable
+    public ComponentName getEnableUseOpenWifiActivity() {
+        return mEnableUseOpenWifiActivity;
+    }
+
+    @Nullable
+    public String getRecommendationServiceLabel() {
+        return mRecommendationServiceLabel;
+    }
+
+    @Nullable
+    public String getNetworkAvailableNotificationChannelId() {
+        return mNetworkAvailableNotificationChannelId;
+    }
+
+    @Override
+    public String toString() {
+        return "NetworkScorerAppData{" +
+                "packageUid=" + packageUid +
+                ", mRecommendationService=" + mRecommendationService +
+                ", mRecommendationServiceLabel=" + mRecommendationServiceLabel +
+                ", mEnableUseOpenWifiActivity=" + mEnableUseOpenWifiActivity +
+                ", mNetworkAvailableNotificationChannelId=" +
+                mNetworkAvailableNotificationChannelId +
+                '}';
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+        NetworkScorerAppData that = (NetworkScorerAppData) o;
+        return packageUid == that.packageUid &&
+                Objects.equals(mRecommendationService, that.mRecommendationService) &&
+                Objects.equals(mRecommendationServiceLabel, that.mRecommendationServiceLabel) &&
+                Objects.equals(mEnableUseOpenWifiActivity, that.mEnableUseOpenWifiActivity) &&
+                Objects.equals(mNetworkAvailableNotificationChannelId,
+                        that.mNetworkAvailableNotificationChannelId);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(packageUid, mRecommendationService, mRecommendationServiceLabel,
+                mEnableUseOpenWifiActivity, mNetworkAvailableNotificationChannelId);
+    }
+}
diff --git a/android/net/NetworkSpecifier.java b/android/net/NetworkSpecifier.java
new file mode 100644
index 0000000..2bc3eb5
--- /dev/null
+++ b/android/net/NetworkSpecifier.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+/**
+ * Describes specific properties of a requested network for use in a {@link NetworkRequest}.
+ *
+ * Applications cannot instantiate this class by themselves, but can obtain instances of
+ * subclasses of this class via other APIs.
+ */
+public abstract class NetworkSpecifier {
+    public NetworkSpecifier() {}
+
+    /**
+     * Returns true if a request with this {@link NetworkSpecifier} is satisfied by a network
+     * with the given NetworkSpecifier.
+     *
+     * @hide
+     */
+    public abstract boolean satisfiedBy(NetworkSpecifier other);
+
+    /**
+     * Optional method which can be overridden by concrete implementations of NetworkSpecifier to
+     * check a self-reported UID. A concrete implementation may contain a UID which would be self-
+     * reported by the caller (since NetworkSpecifier implementations should be non-mutable). This
+     * function is called by ConnectivityService and is passed the actual UID of the caller -
+     * allowing the verification of the self-reported UID. In cases of mismatch the implementation
+     * should throw a SecurityException.
+     *
+     * @param requestorUid The UID of the requestor as obtained from its binder.
+     *
+     * @hide
+     */
+    public void assertValidFromUid(int requestorUid) {
+        // empty
+    }
+
+    /**
+     * Optional method which can be overridden by concrete implementations of NetworkSpecifier to
+     * perform any redaction of information from the NetworkSpecifier, e.g. if it contains
+     * sensitive information. The default implementation simply returns the object itself - i.e.
+     * no information is redacted. A concrete implementation may return a modified (copy) of the
+     * NetworkSpecifier, or even return a null to fully remove all information.
+     * <p>
+     * This method is relevant to NetworkSpecifier objects used by agents - those are shared with
+     * apps by default. Some agents may store sensitive matching information in the specifier,
+     * e.g. a Wi-Fi SSID (which should not be shared since it may leak location). Those classes
+     * can redact to a null. Other agents use the Network Specifier to share public information
+     * with apps - those should not be redacted.
+     * <p>
+     * The default implementation redacts no information.
+     *
+     * @return A NetworkSpecifier object to be passed along to the requesting app.
+     *
+     * @hide
+     */
+    public NetworkSpecifier redact() {
+        // TODO (b/122160111): convert default to null once all platform NetworkSpecifiers
+        // implement this method.
+        return this;
+    }
+}
diff --git a/android/net/NetworkStack.java b/android/net/NetworkStack.java
new file mode 100644
index 0000000..a46c410
--- /dev/null
+++ b/android/net/NetworkStack.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2018 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 static android.Manifest.permission.NETWORK_STACK;
+import static android.content.pm.PackageManager.PERMISSION_GRANTED;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.content.Context;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+/**
+ *
+ * Constants for client code communicating with the network stack service.
+ * @hide
+ */
+@SystemApi
+@TestApi
+public class NetworkStack {
+    /**
+     * Permission granted only to the NetworkStack APK, defined in NetworkStackStub with signature
+     * protection level.
+     * @hide
+     */
+    @SystemApi
+    @TestApi
+    public static final String PERMISSION_MAINLINE_NETWORK_STACK =
+            "android.permission.MAINLINE_NETWORK_STACK";
+
+    private NetworkStack() {}
+
+    /**
+     * If the NetworkStack, MAINLINE_NETWORK_STACK are not allowed for a particular process, throw a
+     * {@link SecurityException}.
+     *
+     * @param context {@link android.content.Context} for the process.
+     *
+     * @hide
+     */
+    public static void checkNetworkStackPermission(final @NonNull Context context) {
+        checkNetworkStackPermissionOr(context);
+    }
+
+    /**
+     * If the NetworkStack, MAINLINE_NETWORK_STACK or other specified permissions are not allowed
+     * for a particular process, throw a {@link SecurityException}.
+     *
+     * @param context {@link android.content.Context} for the process.
+     * @param otherPermissions The set of permissions that could be the candidate permissions , or
+     *                         empty string if none of other permissions needed.
+     * @hide
+     */
+    public static void checkNetworkStackPermissionOr(final @NonNull Context context,
+            final @NonNull String... otherPermissions) {
+        ArrayList<String> permissions = new ArrayList<String>(Arrays.asList(otherPermissions));
+        permissions.add(NETWORK_STACK);
+        permissions.add(PERMISSION_MAINLINE_NETWORK_STACK);
+        enforceAnyPermissionOf(context, permissions.toArray(new String[0]));
+    }
+
+    private static void enforceAnyPermissionOf(final @NonNull Context context,
+            final @NonNull String... permissions) {
+        if (!checkAnyPermissionOf(context, permissions)) {
+            throw new SecurityException("Requires one of the following permissions: "
+                + String.join(", ", permissions) + ".");
+        }
+    }
+
+    private static boolean checkAnyPermissionOf(final @NonNull Context context,
+            final @NonNull String... permissions) {
+        for (String permission : permissions) {
+            if (context.checkCallingOrSelfPermission(permission) == PERMISSION_GRANTED) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+}
diff --git a/android/net/NetworkStackClient.java b/android/net/NetworkStackClient.java
new file mode 100644
index 0000000..99da637
--- /dev/null
+++ b/android/net/NetworkStackClient.java
@@ -0,0 +1,534 @@
+/*
+ * Copyright (C) 2019 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 static android.content.pm.PackageManager.PERMISSION_GRANTED;
+import static android.net.NetworkStack.PERMISSION_MAINLINE_NETWORK_STACK;
+import static android.os.IServiceManager.DUMP_FLAG_PRIORITY_HIGH;
+import static android.os.IServiceManager.DUMP_FLAG_PRIORITY_NORMAL;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.net.dhcp.DhcpServingParamsParcel;
+import android.net.dhcp.IDhcpServerCallbacks;
+import android.net.ip.IIpClientCallbacks;
+import android.net.util.SharedLog;
+import android.os.Binder;
+import android.os.Build;
+import android.os.Environment;
+import android.os.IBinder;
+import android.os.Process;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.provider.DeviceConfig;
+import android.text.format.DateUtils;
+import android.util.ArraySet;
+import android.util.Slog;
+
+import com.android.internal.annotations.GuardedBy;
+
+import java.io.File;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+
+/**
+ * Service used to communicate with the network stack, which is running in a separate module.
+ * @hide
+ */
+public class NetworkStackClient {
+    private static final String TAG = NetworkStackClient.class.getSimpleName();
+
+    private static final int NETWORKSTACK_TIMEOUT_MS = 10_000;
+    private static final String IN_PROCESS_SUFFIX = ".InProcess";
+    private static final String PREFS_FILE = "NetworkStackClientPrefs.xml";
+    private static final String PREF_KEY_LAST_CRASH_TIME = "lastcrash_time";
+    private static final String CONFIG_MIN_CRASH_INTERVAL_MS = "min_crash_interval";
+    private static final String CONFIG_MIN_UPTIME_BEFORE_CRASH_MS = "min_uptime_before_crash";
+    private static final String CONFIG_ALWAYS_RATELIMIT_NETWORKSTACK_CRASH =
+            "always_ratelimit_networkstack_crash";
+
+    // Even if the network stack is lost, do not crash the system more often than this.
+    // Connectivity would be broken, but if the user needs the device for something urgent
+    // (like calling emergency services) we should not bootloop the device.
+    // This is the default value: the actual value can be adjusted via device config.
+    private static final long DEFAULT_MIN_CRASH_INTERVAL_MS = 6 * DateUtils.HOUR_IN_MILLIS;
+
+    // Even if the network stack is lost, do not crash the system server if it was less than
+    // this much after boot. This avoids bootlooping the device, and crashes should address very
+    // infrequent failures, not failures on boot.
+    private static final long DEFAULT_MIN_UPTIME_BEFORE_CRASH_MS = 30 * DateUtils.MINUTE_IN_MILLIS;
+
+    private static NetworkStackClient sInstance;
+
+    @NonNull
+    @GuardedBy("mPendingNetStackRequests")
+    private final ArrayList<NetworkStackCallback> mPendingNetStackRequests = new ArrayList<>();
+    @Nullable
+    @GuardedBy("mPendingNetStackRequests")
+    private INetworkStackConnector mConnector;
+
+    @GuardedBy("mLog")
+    private final SharedLog mLog = new SharedLog(TAG);
+
+    private volatile boolean mWasSystemServerInitialized = false;
+
+    @GuardedBy("mHealthListeners")
+    private final ArraySet<NetworkStackHealthListener> mHealthListeners = new ArraySet<>();
+
+    private interface NetworkStackCallback {
+        void onNetworkStackConnected(INetworkStackConnector connector);
+    }
+
+    /**
+     * Callback interface for severe failures of the NetworkStack.
+     *
+     * <p>Useful for health monitors such as PackageWatchdog.
+     */
+    public interface NetworkStackHealthListener {
+        /**
+         * Called when there is a severe failure of the network stack.
+         * @param packageName Package name of the network stack.
+         */
+        void onNetworkStackFailure(@NonNull String packageName);
+    }
+
+    private NetworkStackClient() { }
+
+    /**
+     * Get the NetworkStackClient singleton instance.
+     */
+    public static synchronized NetworkStackClient getInstance() {
+        if (sInstance == null) {
+            sInstance = new NetworkStackClient();
+        }
+        return sInstance;
+    }
+
+    /**
+     * Add a {@link NetworkStackHealthListener} to listen to network stack health events.
+     */
+    public void registerHealthListener(@NonNull NetworkStackHealthListener listener) {
+        synchronized (mHealthListeners) {
+            mHealthListeners.add(listener);
+        }
+    }
+
+    /**
+     * Create a DHCP server according to the specified parameters.
+     *
+     * <p>The server will be returned asynchronously through the provided callbacks.
+     */
+    public void makeDhcpServer(final String ifName, final DhcpServingParamsParcel params,
+            final IDhcpServerCallbacks cb) {
+        requestConnector(connector -> {
+            try {
+                connector.makeDhcpServer(ifName, params, cb);
+            } catch (RemoteException e) {
+                e.rethrowFromSystemServer();
+            }
+        });
+    }
+
+    /**
+     * Create an IpClient on the specified interface.
+     *
+     * <p>The IpClient will be returned asynchronously through the provided callbacks.
+     */
+    public void makeIpClient(String ifName, IIpClientCallbacks cb) {
+        requestConnector(connector -> {
+            try {
+                connector.makeIpClient(ifName, cb);
+            } catch (RemoteException e) {
+                e.rethrowFromSystemServer();
+            }
+        });
+    }
+
+    /**
+     * Create a NetworkMonitor.
+     *
+     * <p>The INetworkMonitor will be returned asynchronously through the provided callbacks.
+     */
+    public void makeNetworkMonitor(Network network, String name, INetworkMonitorCallbacks cb) {
+        requestConnector(connector -> {
+            try {
+                connector.makeNetworkMonitor(network, name, cb);
+            } catch (RemoteException e) {
+                e.rethrowFromSystemServer();
+            }
+        });
+    }
+
+    /**
+     * Get an instance of the IpMemoryStore.
+     *
+     * <p>The IpMemoryStore will be returned asynchronously through the provided callbacks.
+     */
+    public void fetchIpMemoryStore(IIpMemoryStoreCallbacks cb) {
+        requestConnector(connector -> {
+            try {
+                connector.fetchIpMemoryStore(cb);
+            } catch (RemoteException e) {
+                e.rethrowFromSystemServer();
+            }
+        });
+    }
+
+    private class NetworkStackConnection implements ServiceConnection {
+        @NonNull
+        private final Context mContext;
+        @NonNull
+        private final String mPackageName;
+
+        private NetworkStackConnection(@NonNull Context context, @NonNull String packageName) {
+            mContext = context;
+            mPackageName = packageName;
+        }
+
+        @Override
+        public void onServiceConnected(ComponentName name, IBinder service) {
+            logi("Network stack service connected");
+            registerNetworkStackService(service);
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName name) {
+            // onServiceDisconnected is not being called on device shutdown, so this method being
+            // called always indicates a bad state for the system server.
+            // This code path is only run by the system server: only the system server binds
+            // to the NetworkStack as a service. Other processes get the NetworkStack from
+            // the ServiceManager.
+            maybeCrashWithTerribleFailure("Lost network stack", mContext, mPackageName);
+        }
+    }
+
+    private void registerNetworkStackService(@NonNull IBinder service) {
+        final INetworkStackConnector connector = INetworkStackConnector.Stub.asInterface(service);
+
+        ServiceManager.addService(Context.NETWORK_STACK_SERVICE, service, false /* allowIsolated */,
+                DUMP_FLAG_PRIORITY_HIGH | DUMP_FLAG_PRIORITY_NORMAL);
+        log("Network stack service registered");
+
+        final ArrayList<NetworkStackCallback> requests;
+        synchronized (mPendingNetStackRequests) {
+            requests = new ArrayList<>(mPendingNetStackRequests);
+            mPendingNetStackRequests.clear();
+            mConnector = connector;
+        }
+
+        for (NetworkStackCallback r : requests) {
+            r.onNetworkStackConnected(connector);
+        }
+    }
+
+    /**
+     * Initialize the network stack. Should be called only once on device startup, before any
+     * client attempts to use the network stack.
+     */
+    public void init() {
+        log("Network stack init");
+        mWasSystemServerInitialized = true;
+    }
+
+    /**
+     * Start the network stack. Should be called only once on device startup.
+     *
+     * <p>This method will start the network stack either in the network stack process, or inside
+     * the system server on devices that do not support the network stack module. The network stack
+     * connector will then be delivered asynchronously to clients that requested it before it was
+     * started.
+     */
+    public void start(Context context) {
+        log("Starting network stack");
+
+        final PackageManager pm = context.getPackageManager();
+
+        // Try to bind in-process if the device was shipped with an in-process version
+        Intent intent = getNetworkStackIntent(pm, true /* inSystemProcess */);
+
+        // Otherwise use the updatable module version
+        if (intent == null) {
+            intent = getNetworkStackIntent(pm, false /* inSystemProcess */);
+            log("Starting network stack process");
+        } else {
+            log("Starting network stack in-process");
+        }
+
+        if (intent == null) {
+            maybeCrashWithTerribleFailure("Could not resolve the network stack", context, null);
+            return;
+        }
+
+        final String packageName = intent.getComponent().getPackageName();
+
+        // Start the network stack. The service will be added to the service manager in
+        // NetworkStackConnection.onServiceConnected().
+        if (!context.bindServiceAsUser(intent, new NetworkStackConnection(context, packageName),
+                Context.BIND_AUTO_CREATE | Context.BIND_IMPORTANT, UserHandle.SYSTEM)) {
+            maybeCrashWithTerribleFailure(
+                    "Could not bind to network stack in-process, or in app with " + intent,
+                    context, packageName);
+            return;
+        }
+
+        log("Network stack service start requested");
+    }
+
+    @Nullable
+    private Intent getNetworkStackIntent(@NonNull PackageManager pm, boolean inSystemProcess) {
+        final String baseAction = INetworkStackConnector.class.getName();
+        final Intent intent =
+                new Intent(inSystemProcess ? baseAction + IN_PROCESS_SUFFIX : baseAction);
+        final ComponentName comp = intent.resolveSystemService(pm, 0);
+
+        if (comp == null) {
+            return null;
+        }
+        intent.setComponent(comp);
+
+        int uid = -1;
+        try {
+            uid = pm.getPackageUidAsUser(comp.getPackageName(), UserHandle.USER_SYSTEM);
+        } catch (PackageManager.NameNotFoundException e) {
+            logWtf("Network stack package not found", e);
+            // Fall through
+        }
+
+        final int expectedUid = inSystemProcess ? Process.SYSTEM_UID : Process.NETWORK_STACK_UID;
+        if (uid != expectedUid) {
+            throw new SecurityException("Invalid network stack UID: " + uid);
+        }
+
+        if (!inSystemProcess) {
+            checkNetworkStackPermission(pm, comp);
+        }
+
+        return intent;
+    }
+
+    private void checkNetworkStackPermission(
+            @NonNull PackageManager pm, @NonNull ComponentName comp) {
+        final int hasPermission =
+                pm.checkPermission(PERMISSION_MAINLINE_NETWORK_STACK, comp.getPackageName());
+        if (hasPermission != PERMISSION_GRANTED) {
+            throw new SecurityException(
+                    "Network stack does not have permission " + PERMISSION_MAINLINE_NETWORK_STACK);
+        }
+    }
+
+    private void maybeCrashWithTerribleFailure(@NonNull String message,
+            @NonNull Context context, @Nullable String packageName) {
+        logWtf(message, null);
+        // uptime is monotonic even after a framework restart
+        final long uptime = SystemClock.elapsedRealtime();
+        final long now = System.currentTimeMillis();
+        final long minCrashIntervalMs = DeviceConfig.getLong(DeviceConfig.NAMESPACE_CONNECTIVITY,
+                CONFIG_MIN_CRASH_INTERVAL_MS, DEFAULT_MIN_CRASH_INTERVAL_MS);
+        final long minUptimeBeforeCrash = DeviceConfig.getLong(DeviceConfig.NAMESPACE_CONNECTIVITY,
+                CONFIG_MIN_UPTIME_BEFORE_CRASH_MS, DEFAULT_MIN_UPTIME_BEFORE_CRASH_MS);
+        final boolean alwaysRatelimit = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_CONNECTIVITY,
+                CONFIG_ALWAYS_RATELIMIT_NETWORKSTACK_CRASH, false);
+
+        final SharedPreferences prefs = getSharedPreferences(context);
+        final long lastCrashTime = tryGetLastCrashTime(prefs);
+
+        // Only crash if there was enough time since boot, and (if known) enough time passed since
+        // the last crash.
+        // time and lastCrashTime may be unreliable if devices have incorrect clock time, but they
+        // are only used to limit the number of crashes compared to only using the time since boot,
+        // which would also be OK behavior by itself.
+        // - If lastCrashTime is incorrectly more than the current time, only look at uptime
+        // - If it is much less than current time, only look at uptime
+        // - If current time is during the next few hours after last crash time, don't crash.
+        //   Considering that this only matters if last boot was some time ago, it's likely that
+        //   time will be set correctly. Otherwise, not crashing is not a big problem anyway. Being
+        //   in this last state would also not last for long since the window is only a few hours.
+        final boolean alwaysCrash = Build.IS_DEBUGGABLE && !alwaysRatelimit;
+        final boolean justBooted = uptime < minUptimeBeforeCrash;
+        final boolean haveLastCrashTime = (lastCrashTime != 0) && (lastCrashTime < now);
+        final boolean haveKnownRecentCrash =
+                haveLastCrashTime && (now < lastCrashTime + minCrashIntervalMs);
+        if (alwaysCrash || (!justBooted && !haveKnownRecentCrash)) {
+            // The system is not bound to its network stack (for example due to a crash in the
+            // network stack process): better crash rather than stay in a bad state where all
+            // networking is broken.
+            // Using device-encrypted SharedPreferences as DeviceConfig does not have a synchronous
+            // API to persist settings before a crash.
+            tryWriteLastCrashTime(prefs, now);
+            throw new IllegalStateException(message);
+        }
+
+        // Here the system crashed recently already. Inform listeners that something is
+        // definitely wrong.
+        if (packageName != null) {
+            final ArraySet<NetworkStackHealthListener> listeners;
+            synchronized (mHealthListeners) {
+                listeners = new ArraySet<>(mHealthListeners);
+            }
+            for (NetworkStackHealthListener listener : listeners) {
+                listener.onNetworkStackFailure(packageName);
+            }
+        }
+    }
+
+    @Nullable
+    private SharedPreferences getSharedPreferences(@NonNull Context context) {
+        try {
+            final File prefsFile = new File(
+                    Environment.getDataSystemDeDirectory(UserHandle.USER_SYSTEM), PREFS_FILE);
+            return context.createDeviceProtectedStorageContext()
+                    .getSharedPreferences(prefsFile, Context.MODE_PRIVATE);
+        } catch (Throwable e) {
+            logWtf("Error loading shared preferences", e);
+            return null;
+        }
+    }
+
+    private long tryGetLastCrashTime(@Nullable SharedPreferences prefs) {
+        if (prefs == null) return 0L;
+        try {
+            return prefs.getLong(PREF_KEY_LAST_CRASH_TIME, 0L);
+        } catch (Throwable e) {
+            logWtf("Error getting last crash time", e);
+            return 0L;
+        }
+    }
+
+    private void tryWriteLastCrashTime(@Nullable SharedPreferences prefs, long value) {
+        if (prefs == null) return;
+        try {
+            prefs.edit().putLong(PREF_KEY_LAST_CRASH_TIME, value).commit();
+        } catch (Throwable e) {
+            logWtf("Error writing last crash time", e);
+        }
+    }
+
+    /**
+     * Log a message in the local log.
+     */
+    private void log(@NonNull String message) {
+        synchronized (mLog) {
+            mLog.log(message);
+        }
+    }
+
+    private void logWtf(@NonNull String message, @Nullable Throwable e) {
+        Slog.wtf(TAG, message);
+        synchronized (mLog) {
+            mLog.e(message, e);
+        }
+    }
+
+    private void loge(@NonNull String message, @Nullable Throwable e) {
+        synchronized (mLog) {
+            mLog.e(message, e);
+        }
+    }
+
+    /**
+     * Log a message in the local and system logs.
+     */
+    private void logi(@NonNull String message) {
+        synchronized (mLog) {
+            mLog.i(message);
+        }
+    }
+
+    /**
+     * For non-system server clients, get the connector registered by the system server.
+     */
+    private INetworkStackConnector getRemoteConnector() {
+        // Block until the NetworkStack connector is registered in ServiceManager.
+        // <p>This is only useful for non-system processes that do not have a way to be notified of
+        // registration completion. Adding a callback system would be too heavy weight considering
+        // that the connector is registered on boot, so it is unlikely that a client would request
+        // it before it is registered.
+        // TODO: consider blocking boot on registration and simplify much of the logic in this class
+        IBinder connector;
+        try {
+            final long before = System.currentTimeMillis();
+            while ((connector = ServiceManager.getService(Context.NETWORK_STACK_SERVICE)) == null) {
+                Thread.sleep(20);
+                if (System.currentTimeMillis() - before > NETWORKSTACK_TIMEOUT_MS) {
+                    loge("Timeout waiting for NetworkStack connector", null);
+                    return null;
+                }
+            }
+        } catch (InterruptedException e) {
+            loge("Error waiting for NetworkStack connector", e);
+            return null;
+        }
+
+        return INetworkStackConnector.Stub.asInterface(connector);
+    }
+
+    private void requestConnector(@NonNull NetworkStackCallback request) {
+        // TODO: PID check.
+        final int caller = Binder.getCallingUid();
+        if (caller != Process.SYSTEM_UID && !UserHandle.isSameApp(caller, Process.BLUETOOTH_UID)
+                && !UserHandle.isSameApp(caller, Process.PHONE_UID)) {
+            // Don't even attempt to obtain the connector and give a nice error message
+            throw new SecurityException(
+                    "Only the system server should try to bind to the network stack.");
+        }
+
+        if (!mWasSystemServerInitialized) {
+            // The network stack is not being started in this process, e.g. this process is not
+            // the system server. Get a remote connector registered by the system server.
+            final INetworkStackConnector connector = getRemoteConnector();
+            synchronized (mPendingNetStackRequests) {
+                mConnector = connector;
+            }
+            request.onNetworkStackConnected(connector);
+            return;
+        }
+
+        final INetworkStackConnector connector;
+        synchronized (mPendingNetStackRequests) {
+            connector = mConnector;
+            if (connector == null) {
+                mPendingNetStackRequests.add(request);
+                return;
+            }
+        }
+
+        request.onNetworkStackConnected(connector);
+    }
+
+    /**
+     * Dump NetworkStackClient logs to the specified {@link PrintWriter}.
+     */
+    public void dump(PrintWriter pw) {
+        // dump is thread-safe on SharedLog
+        mLog.dump(null, pw, null);
+
+        final int requestsQueueLength;
+        synchronized (mPendingNetStackRequests) {
+            requestsQueueLength = mPendingNetStackRequests.size();
+        }
+
+        pw.println();
+        pw.println("pendingNetStackRequests length: " + requestsQueueLength);
+    }
+}
diff --git a/android/net/NetworkState.java b/android/net/NetworkState.java
new file mode 100644
index 0000000..292cf50
--- /dev/null
+++ b/android/net/NetworkState.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2011 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.UnsupportedAppUsage;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Slog;
+
+/**
+ * Snapshot of network state.
+ *
+ * @hide
+ */
+public class NetworkState implements Parcelable {
+    private static final boolean SANITY_CHECK_ROAMING = false;
+
+    public static final NetworkState EMPTY = new NetworkState(null, null, null, null, null, null);
+
+    public final NetworkInfo networkInfo;
+    public final LinkProperties linkProperties;
+    public final NetworkCapabilities networkCapabilities;
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    public final Network network;
+    public final String subscriberId;
+    public final String networkId;
+
+    public NetworkState(NetworkInfo networkInfo, LinkProperties linkProperties,
+            NetworkCapabilities networkCapabilities, Network network, String subscriberId,
+            String networkId) {
+        this.networkInfo = networkInfo;
+        this.linkProperties = linkProperties;
+        this.networkCapabilities = networkCapabilities;
+        this.network = network;
+        this.subscriberId = subscriberId;
+        this.networkId = networkId;
+
+        // This object is an atomic view of a network, so the various components
+        // should always agree on roaming state.
+        if (SANITY_CHECK_ROAMING && networkInfo != null && networkCapabilities != null) {
+            if (networkInfo.isRoaming() == networkCapabilities
+                    .hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)) {
+                Slog.wtf("NetworkState", "Roaming state disagreement between " + networkInfo
+                        + " and " + networkCapabilities);
+            }
+        }
+    }
+
+    @UnsupportedAppUsage
+    public NetworkState(Parcel in) {
+        networkInfo = in.readParcelable(null);
+        linkProperties = in.readParcelable(null);
+        networkCapabilities = in.readParcelable(null);
+        network = in.readParcelable(null);
+        subscriberId = in.readString();
+        networkId = in.readString();
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeParcelable(networkInfo, flags);
+        out.writeParcelable(linkProperties, flags);
+        out.writeParcelable(networkCapabilities, flags);
+        out.writeParcelable(network, flags);
+        out.writeString(subscriberId);
+        out.writeString(networkId);
+    }
+
+    @UnsupportedAppUsage
+    public static final @android.annotation.NonNull Creator<NetworkState> CREATOR = new Creator<NetworkState>() {
+        @Override
+        public NetworkState createFromParcel(Parcel in) {
+            return new NetworkState(in);
+        }
+
+        @Override
+        public NetworkState[] newArray(int size) {
+            return new NetworkState[size];
+        }
+    };
+}
diff --git a/android/net/NetworkStats.java b/android/net/NetworkStats.java
new file mode 100644
index 0000000..e892b65
--- /dev/null
+++ b/android/net/NetworkStats.java
@@ -0,0 +1,1362 @@
+/*
+ * Copyright (C) 2011 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 static android.os.Process.CLAT_UID;
+
+import android.annotation.UnsupportedAppUsage;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.util.Slog;
+import android.util.SparseBooleanArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ArrayUtils;
+
+import libcore.util.EmptyArray;
+
+import java.io.CharArrayWriter;
+import java.io.PrintWriter;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Collection of active network statistics. Can contain summary details across
+ * all interfaces, or details with per-UID granularity. Internally stores data
+ * as a large table, closely matching {@code /proc/} data format. This structure
+ * optimizes for rapid in-memory comparison, but consider using
+ * {@link NetworkStatsHistory} when persisting.
+ *
+ * @hide
+ */
+// @NotThreadSafe
+public class NetworkStats implements Parcelable {
+    private static final String TAG = "NetworkStats";
+    /** {@link #iface} value when interface details unavailable. */
+    public static final String IFACE_ALL = null;
+    /** {@link #uid} value when UID details unavailable. */
+    public static final int UID_ALL = -1;
+    /** {@link #tag} value matching any tag. */
+    // TODO: Rename TAG_ALL to TAG_ANY.
+    public static final int TAG_ALL = -1;
+    /** {@link #set} value for all sets combined, not including debug sets. */
+    public static final int SET_ALL = -1;
+    /** {@link #set} value where background data is accounted. */
+    public static final int SET_DEFAULT = 0;
+    /** {@link #set} value where foreground data is accounted. */
+    public static final int SET_FOREGROUND = 1;
+    /** All {@link #set} value greater than SET_DEBUG_START are debug {@link #set} values. */
+    public static final int SET_DEBUG_START = 1000;
+    /** Debug {@link #set} value when the VPN stats are moved in. */
+    public static final int SET_DBG_VPN_IN = 1001;
+    /** Debug {@link #set} value when the VPN stats are moved out of a vpn UID. */
+    public static final int SET_DBG_VPN_OUT = 1002;
+
+    /** Include all interfaces when filtering */
+    public static final String[] INTERFACES_ALL = null;
+
+    /** {@link #tag} value for total data across all tags. */
+    // TODO: Rename TAG_NONE to TAG_ALL.
+    public static final int TAG_NONE = 0;
+
+    /** {@link #metered} value to account for all metered states. */
+    public static final int METERED_ALL = -1;
+    /** {@link #metered} value where native, unmetered data is accounted. */
+    public static final int METERED_NO = 0;
+    /** {@link #metered} value where metered data is accounted. */
+    public static final int METERED_YES = 1;
+
+    /** {@link #roaming} value to account for all roaming states. */
+    public static final int ROAMING_ALL = -1;
+    /** {@link #roaming} value where native, non-roaming data is accounted. */
+    public static final int ROAMING_NO = 0;
+    /** {@link #roaming} value where roaming data is accounted. */
+    public static final int ROAMING_YES = 1;
+
+    /** {@link #onDefaultNetwork} value to account for all default network states. */
+    public static final int DEFAULT_NETWORK_ALL = -1;
+    /** {@link #onDefaultNetwork} value to account for usage while not the default network. */
+    public static final int DEFAULT_NETWORK_NO = 0;
+    /** {@link #onDefaultNetwork} value to account for usage while the default network. */
+    public static final int DEFAULT_NETWORK_YES = 1;
+
+    /** Denotes a request for stats at the interface level. */
+    public static final int STATS_PER_IFACE = 0;
+    /** Denotes a request for stats at the interface and UID level. */
+    public static final int STATS_PER_UID = 1;
+
+    private static final String CLATD_INTERFACE_PREFIX = "v4-";
+    // Delta between IPv4 header (20b) and IPv6 header (40b).
+    // Used for correct stats accounting on clatd interfaces.
+    private static final int IPV4V6_HEADER_DELTA = 20;
+
+    // TODO: move fields to "mVariable" notation
+
+    /**
+     * {@link SystemClock#elapsedRealtime()} timestamp when this data was
+     * generated.
+     */
+    private long elapsedRealtime;
+    @UnsupportedAppUsage
+    private int size;
+    @UnsupportedAppUsage
+    private int capacity;
+    @UnsupportedAppUsage
+    private String[] iface;
+    @UnsupportedAppUsage
+    private int[] uid;
+    @UnsupportedAppUsage
+    private int[] set;
+    @UnsupportedAppUsage
+    private int[] tag;
+    @UnsupportedAppUsage
+    private int[] metered;
+    @UnsupportedAppUsage
+    private int[] roaming;
+    @UnsupportedAppUsage
+    private int[] defaultNetwork;
+    @UnsupportedAppUsage
+    private long[] rxBytes;
+    @UnsupportedAppUsage
+    private long[] rxPackets;
+    @UnsupportedAppUsage
+    private long[] txBytes;
+    @UnsupportedAppUsage
+    private long[] txPackets;
+    @UnsupportedAppUsage
+    private long[] operations;
+
+    public static class Entry {
+        @UnsupportedAppUsage
+        public String iface;
+        @UnsupportedAppUsage
+        public int uid;
+        @UnsupportedAppUsage
+        public int set;
+        @UnsupportedAppUsage
+        public int tag;
+        /**
+         * Note that this is only populated w/ the default value when read from /proc or written
+         * to disk. We merge in the correct value when reporting this value to clients of
+         * getSummary().
+         */
+        public int metered;
+        /**
+         * Note that this is only populated w/ the default value when read from /proc or written
+         * to disk. We merge in the correct value when reporting this value to clients of
+         * getSummary().
+         */
+        public int roaming;
+        /**
+         * Note that this is only populated w/ the default value when read from /proc or written
+         * to disk. We merge in the correct value when reporting this value to clients of
+         * getSummary().
+         */
+        public int defaultNetwork;
+        @UnsupportedAppUsage
+        public long rxBytes;
+        @UnsupportedAppUsage
+        public long rxPackets;
+        @UnsupportedAppUsage
+        public long txBytes;
+        @UnsupportedAppUsage
+        public long txPackets;
+        public long operations;
+
+        @UnsupportedAppUsage
+        public Entry() {
+            this(IFACE_ALL, UID_ALL, SET_DEFAULT, TAG_NONE, 0L, 0L, 0L, 0L, 0L);
+        }
+
+        public Entry(long rxBytes, long rxPackets, long txBytes, long txPackets, long operations) {
+            this(IFACE_ALL, UID_ALL, SET_DEFAULT, TAG_NONE, rxBytes, rxPackets, txBytes, txPackets,
+                    operations);
+        }
+
+        public Entry(String iface, int uid, int set, int tag, long rxBytes, long rxPackets,
+                long txBytes, long txPackets, long operations) {
+            this(iface, uid, set, tag, METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO,
+                    rxBytes, rxPackets, txBytes, txPackets, operations);
+        }
+
+        public Entry(String iface, int uid, int set, int tag, int metered, int roaming,
+                 int defaultNetwork, long rxBytes, long rxPackets, long txBytes, long txPackets,
+                 long operations) {
+            this.iface = iface;
+            this.uid = uid;
+            this.set = set;
+            this.tag = tag;
+            this.metered = metered;
+            this.roaming = roaming;
+            this.defaultNetwork = defaultNetwork;
+            this.rxBytes = rxBytes;
+            this.rxPackets = rxPackets;
+            this.txBytes = txBytes;
+            this.txPackets = txPackets;
+            this.operations = operations;
+        }
+
+        public boolean isNegative() {
+            return rxBytes < 0 || rxPackets < 0 || txBytes < 0 || txPackets < 0 || operations < 0;
+        }
+
+        public boolean isEmpty() {
+            return rxBytes == 0 && rxPackets == 0 && txBytes == 0 && txPackets == 0
+                    && operations == 0;
+        }
+
+        public void add(Entry another) {
+            this.rxBytes += another.rxBytes;
+            this.rxPackets += another.rxPackets;
+            this.txBytes += another.txBytes;
+            this.txPackets += another.txPackets;
+            this.operations += another.operations;
+        }
+
+        @Override
+        public String toString() {
+            final StringBuilder builder = new StringBuilder();
+            builder.append("iface=").append(iface);
+            builder.append(" uid=").append(uid);
+            builder.append(" set=").append(setToString(set));
+            builder.append(" tag=").append(tagToString(tag));
+            builder.append(" metered=").append(meteredToString(metered));
+            builder.append(" roaming=").append(roamingToString(roaming));
+            builder.append(" defaultNetwork=").append(defaultNetworkToString(defaultNetwork));
+            builder.append(" rxBytes=").append(rxBytes);
+            builder.append(" rxPackets=").append(rxPackets);
+            builder.append(" txBytes=").append(txBytes);
+            builder.append(" txPackets=").append(txPackets);
+            builder.append(" operations=").append(operations);
+            return builder.toString();
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (o instanceof Entry) {
+                final Entry e = (Entry) o;
+                return uid == e.uid && set == e.set && tag == e.tag && metered == e.metered
+                        && roaming == e.roaming && defaultNetwork == e.defaultNetwork
+                        && rxBytes == e.rxBytes && rxPackets == e.rxPackets
+                        && txBytes == e.txBytes && txPackets == e.txPackets
+                        && operations == e.operations && iface.equals(e.iface);
+            }
+            return false;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(uid, set, tag, metered, roaming, defaultNetwork, iface);
+        }
+    }
+
+    @UnsupportedAppUsage
+    public NetworkStats(long elapsedRealtime, int initialSize) {
+        this.elapsedRealtime = elapsedRealtime;
+        this.size = 0;
+        if (initialSize > 0) {
+            this.capacity = initialSize;
+            this.iface = new String[initialSize];
+            this.uid = new int[initialSize];
+            this.set = new int[initialSize];
+            this.tag = new int[initialSize];
+            this.metered = new int[initialSize];
+            this.roaming = new int[initialSize];
+            this.defaultNetwork = new int[initialSize];
+            this.rxBytes = new long[initialSize];
+            this.rxPackets = new long[initialSize];
+            this.txBytes = new long[initialSize];
+            this.txPackets = new long[initialSize];
+            this.operations = new long[initialSize];
+        } else {
+            // Special case for use by NetworkStatsFactory to start out *really* empty.
+            clear();
+        }
+    }
+
+    @UnsupportedAppUsage
+    public NetworkStats(Parcel parcel) {
+        elapsedRealtime = parcel.readLong();
+        size = parcel.readInt();
+        capacity = parcel.readInt();
+        iface = parcel.createStringArray();
+        uid = parcel.createIntArray();
+        set = parcel.createIntArray();
+        tag = parcel.createIntArray();
+        metered = parcel.createIntArray();
+        roaming = parcel.createIntArray();
+        defaultNetwork = parcel.createIntArray();
+        rxBytes = parcel.createLongArray();
+        rxPackets = parcel.createLongArray();
+        txBytes = parcel.createLongArray();
+        txPackets = parcel.createLongArray();
+        operations = parcel.createLongArray();
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeLong(elapsedRealtime);
+        dest.writeInt(size);
+        dest.writeInt(capacity);
+        dest.writeStringArray(iface);
+        dest.writeIntArray(uid);
+        dest.writeIntArray(set);
+        dest.writeIntArray(tag);
+        dest.writeIntArray(metered);
+        dest.writeIntArray(roaming);
+        dest.writeIntArray(defaultNetwork);
+        dest.writeLongArray(rxBytes);
+        dest.writeLongArray(rxPackets);
+        dest.writeLongArray(txBytes);
+        dest.writeLongArray(txPackets);
+        dest.writeLongArray(operations);
+    }
+
+    @Override
+    public NetworkStats clone() {
+        final NetworkStats clone = new NetworkStats(elapsedRealtime, size);
+        NetworkStats.Entry entry = null;
+        for (int i = 0; i < size; i++) {
+            entry = getValues(i, entry);
+            clone.addValues(entry);
+        }
+        return clone;
+    }
+
+    /**
+     * Clear all data stored in this object.
+     */
+    public void clear() {
+        this.capacity = 0;
+        this.iface = EmptyArray.STRING;
+        this.uid = EmptyArray.INT;
+        this.set = EmptyArray.INT;
+        this.tag = EmptyArray.INT;
+        this.metered = EmptyArray.INT;
+        this.roaming = EmptyArray.INT;
+        this.defaultNetwork = EmptyArray.INT;
+        this.rxBytes = EmptyArray.LONG;
+        this.rxPackets = EmptyArray.LONG;
+        this.txBytes = EmptyArray.LONG;
+        this.txPackets = EmptyArray.LONG;
+        this.operations = EmptyArray.LONG;
+    }
+
+    @VisibleForTesting
+    public NetworkStats addIfaceValues(
+            String iface, long rxBytes, long rxPackets, long txBytes, long txPackets) {
+        return addValues(
+                iface, UID_ALL, SET_DEFAULT, TAG_NONE, rxBytes, rxPackets, txBytes, txPackets, 0L);
+    }
+
+    @VisibleForTesting
+    public NetworkStats addValues(String iface, int uid, int set, int tag, long rxBytes,
+            long rxPackets, long txBytes, long txPackets, long operations) {
+        return addValues(new Entry(
+                iface, uid, set, tag, rxBytes, rxPackets, txBytes, txPackets, operations));
+    }
+
+    @VisibleForTesting
+    public NetworkStats addValues(String iface, int uid, int set, int tag, int metered, int roaming,
+            int defaultNetwork, long rxBytes, long rxPackets, long txBytes, long txPackets,
+            long operations) {
+        return addValues(new Entry(
+                iface, uid, set, tag, metered, roaming, defaultNetwork, rxBytes, rxPackets,
+                txBytes, txPackets, operations));
+    }
+
+    /**
+     * Add new stats entry, copying from given {@link Entry}. The {@link Entry}
+     * object can be recycled across multiple calls.
+     */
+    public NetworkStats addValues(Entry entry) {
+        if (size >= capacity) {
+            final int newLength = Math.max(size, 10) * 3 / 2;
+            iface = Arrays.copyOf(iface, newLength);
+            uid = Arrays.copyOf(uid, newLength);
+            set = Arrays.copyOf(set, newLength);
+            tag = Arrays.copyOf(tag, newLength);
+            metered = Arrays.copyOf(metered, newLength);
+            roaming = Arrays.copyOf(roaming, newLength);
+            defaultNetwork = Arrays.copyOf(defaultNetwork, newLength);
+            rxBytes = Arrays.copyOf(rxBytes, newLength);
+            rxPackets = Arrays.copyOf(rxPackets, newLength);
+            txBytes = Arrays.copyOf(txBytes, newLength);
+            txPackets = Arrays.copyOf(txPackets, newLength);
+            operations = Arrays.copyOf(operations, newLength);
+            capacity = newLength;
+        }
+
+        setValues(size, entry);
+        size++;
+
+        return this;
+    }
+
+    private void setValues(int i, Entry entry) {
+        iface[i] = entry.iface;
+        uid[i] = entry.uid;
+        set[i] = entry.set;
+        tag[i] = entry.tag;
+        metered[i] = entry.metered;
+        roaming[i] = entry.roaming;
+        defaultNetwork[i] = entry.defaultNetwork;
+        rxBytes[i] = entry.rxBytes;
+        rxPackets[i] = entry.rxPackets;
+        txBytes[i] = entry.txBytes;
+        txPackets[i] = entry.txPackets;
+        operations[i] = entry.operations;
+    }
+
+    /**
+     * Return specific stats entry.
+     */
+    @UnsupportedAppUsage
+    public Entry getValues(int i, Entry recycle) {
+        final Entry entry = recycle != null ? recycle : new Entry();
+        entry.iface = iface[i];
+        entry.uid = uid[i];
+        entry.set = set[i];
+        entry.tag = tag[i];
+        entry.metered = metered[i];
+        entry.roaming = roaming[i];
+        entry.defaultNetwork = defaultNetwork[i];
+        entry.rxBytes = rxBytes[i];
+        entry.rxPackets = rxPackets[i];
+        entry.txBytes = txBytes[i];
+        entry.txPackets = txPackets[i];
+        entry.operations = operations[i];
+        return entry;
+    }
+
+    /**
+     * If @{code dest} is not equal to @{code src}, copy entry from index @{code src} to index
+     * @{code dest}.
+     */
+    private void maybeCopyEntry(int dest, int src) {
+        if (dest == src) return;
+        iface[dest] = iface[src];
+        uid[dest] = uid[src];
+        set[dest] = set[src];
+        tag[dest] = tag[src];
+        metered[dest] = metered[src];
+        roaming[dest] = roaming[src];
+        defaultNetwork[dest] = defaultNetwork[src];
+        rxBytes[dest] = rxBytes[src];
+        rxPackets[dest] = rxPackets[src];
+        txBytes[dest] = txBytes[src];
+        txPackets[dest] = txPackets[src];
+        operations[dest] = operations[src];
+    }
+
+    public long getElapsedRealtime() {
+        return elapsedRealtime;
+    }
+
+    public void setElapsedRealtime(long time) {
+        elapsedRealtime = time;
+    }
+
+    /**
+     * Return age of this {@link NetworkStats} object with respect to
+     * {@link SystemClock#elapsedRealtime()}.
+     */
+    public long getElapsedRealtimeAge() {
+        return SystemClock.elapsedRealtime() - elapsedRealtime;
+    }
+
+    @UnsupportedAppUsage
+    public int size() {
+        return size;
+    }
+
+    @VisibleForTesting
+    public int internalSize() {
+        return capacity;
+    }
+
+    @Deprecated
+    public NetworkStats combineValues(String iface, int uid, int tag, long rxBytes, long rxPackets,
+            long txBytes, long txPackets, long operations) {
+        return combineValues(
+                iface, uid, SET_DEFAULT, tag, rxBytes, rxPackets, txBytes,
+                txPackets, operations);
+    }
+
+    public NetworkStats combineValues(String iface, int uid, int set, int tag,
+            long rxBytes, long rxPackets, long txBytes, long txPackets, long operations) {
+        return combineValues(new Entry(
+                iface, uid, set, tag, rxBytes, rxPackets, txBytes, txPackets, operations));
+    }
+
+    /**
+     * Combine given values with an existing row, or create a new row if
+     * {@link #findIndex(String, int, int, int, int)} is unable to find match. Can
+     * also be used to subtract values from existing rows.
+     */
+    @UnsupportedAppUsage
+    public NetworkStats combineValues(Entry entry) {
+        final int i = findIndex(entry.iface, entry.uid, entry.set, entry.tag, entry.metered,
+                entry.roaming, entry.defaultNetwork);
+        if (i == -1) {
+            // only create new entry when positive contribution
+            addValues(entry);
+        } else {
+            rxBytes[i] += entry.rxBytes;
+            rxPackets[i] += entry.rxPackets;
+            txBytes[i] += entry.txBytes;
+            txPackets[i] += entry.txPackets;
+            operations[i] += entry.operations;
+        }
+        return this;
+    }
+
+    /**
+     * Combine all values from another {@link NetworkStats} into this object.
+     */
+    @UnsupportedAppUsage
+    public void combineAllValues(NetworkStats another) {
+        NetworkStats.Entry entry = null;
+        for (int i = 0; i < another.size; i++) {
+            entry = another.getValues(i, entry);
+            combineValues(entry);
+        }
+    }
+
+    /**
+     * Find first stats index that matches the requested parameters.
+     */
+    public int findIndex(String iface, int uid, int set, int tag, int metered, int roaming,
+            int defaultNetwork) {
+        for (int i = 0; i < size; i++) {
+            if (uid == this.uid[i] && set == this.set[i] && tag == this.tag[i]
+                    && metered == this.metered[i] && roaming == this.roaming[i]
+                    && defaultNetwork == this.defaultNetwork[i]
+                    && Objects.equals(iface, this.iface[i])) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * Find first stats index that matches the requested parameters, starting
+     * search around the hinted index as an optimization.
+     */
+    @VisibleForTesting
+    public int findIndexHinted(String iface, int uid, int set, int tag, int metered, int roaming,
+            int defaultNetwork, int hintIndex) {
+        for (int offset = 0; offset < size; offset++) {
+            final int halfOffset = offset / 2;
+
+            // search outwards from hint index, alternating forward and backward
+            final int i;
+            if (offset % 2 == 0) {
+                i = (hintIndex + halfOffset) % size;
+            } else {
+                i = (size + hintIndex - halfOffset - 1) % size;
+            }
+
+            if (uid == this.uid[i] && set == this.set[i] && tag == this.tag[i]
+                    && metered == this.metered[i] && roaming == this.roaming[i]
+                    && defaultNetwork == this.defaultNetwork[i]
+                    && Objects.equals(iface, this.iface[i])) {
+                return i;
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * Splice in {@link #operations} from the given {@link NetworkStats} based
+     * on matching {@link #uid} and {@link #tag} rows. Ignores {@link #iface},
+     * since operation counts are at data layer.
+     */
+    public void spliceOperationsFrom(NetworkStats stats) {
+        for (int i = 0; i < size; i++) {
+            final int j = stats.findIndex(iface[i], uid[i], set[i], tag[i], metered[i], roaming[i],
+                    defaultNetwork[i]);
+            if (j == -1) {
+                operations[i] = 0;
+            } else {
+                operations[i] = stats.operations[j];
+            }
+        }
+    }
+
+    /**
+     * Return list of unique interfaces known by this data structure.
+     */
+    public String[] getUniqueIfaces() {
+        final HashSet<String> ifaces = new HashSet<String>();
+        for (String iface : this.iface) {
+            if (iface != IFACE_ALL) {
+                ifaces.add(iface);
+            }
+        }
+        return ifaces.toArray(new String[ifaces.size()]);
+    }
+
+    /**
+     * Return list of unique UIDs known by this data structure.
+     */
+    @UnsupportedAppUsage
+    public int[] getUniqueUids() {
+        final SparseBooleanArray uids = new SparseBooleanArray();
+        for (int uid : this.uid) {
+            uids.put(uid, true);
+        }
+
+        final int size = uids.size();
+        final int[] result = new int[size];
+        for (int i = 0; i < size; i++) {
+            result[i] = uids.keyAt(i);
+        }
+        return result;
+    }
+
+    /**
+     * Return total bytes represented by this snapshot object, usually used when
+     * checking if a {@link #subtract(NetworkStats)} delta passes a threshold.
+     */
+    @UnsupportedAppUsage
+    public long getTotalBytes() {
+        final Entry entry = getTotal(null);
+        return entry.rxBytes + entry.txBytes;
+    }
+
+    /**
+     * Return total of all fields represented by this snapshot object.
+     */
+    @UnsupportedAppUsage
+    public Entry getTotal(Entry recycle) {
+        return getTotal(recycle, null, UID_ALL, false);
+    }
+
+    /**
+     * Return total of all fields represented by this snapshot object matching
+     * the requested {@link #uid}.
+     */
+    @UnsupportedAppUsage
+    public Entry getTotal(Entry recycle, int limitUid) {
+        return getTotal(recycle, null, limitUid, false);
+    }
+
+    /**
+     * Return total of all fields represented by this snapshot object matching
+     * the requested {@link #iface}.
+     */
+    public Entry getTotal(Entry recycle, HashSet<String> limitIface) {
+        return getTotal(recycle, limitIface, UID_ALL, false);
+    }
+
+    @UnsupportedAppUsage
+    public Entry getTotalIncludingTags(Entry recycle) {
+        return getTotal(recycle, null, UID_ALL, true);
+    }
+
+    /**
+     * Return total of all fields represented by this snapshot object matching
+     * the requested {@link #iface} and {@link #uid}.
+     *
+     * @param limitIface Set of {@link #iface} to include in total; or {@code
+     *            null} to include all ifaces.
+     */
+    private Entry getTotal(
+            Entry recycle, HashSet<String> limitIface, int limitUid, boolean includeTags) {
+        final Entry entry = recycle != null ? recycle : new Entry();
+
+        entry.iface = IFACE_ALL;
+        entry.uid = limitUid;
+        entry.set = SET_ALL;
+        entry.tag = TAG_NONE;
+        entry.metered = METERED_ALL;
+        entry.roaming = ROAMING_ALL;
+        entry.defaultNetwork = DEFAULT_NETWORK_ALL;
+        entry.rxBytes = 0;
+        entry.rxPackets = 0;
+        entry.txBytes = 0;
+        entry.txPackets = 0;
+        entry.operations = 0;
+
+        for (int i = 0; i < size; i++) {
+            final boolean matchesUid = (limitUid == UID_ALL) || (limitUid == uid[i]);
+            final boolean matchesIface = (limitIface == null) || (limitIface.contains(iface[i]));
+
+            if (matchesUid && matchesIface) {
+                // skip specific tags, since already counted in TAG_NONE
+                if (tag[i] != TAG_NONE && !includeTags) continue;
+
+                entry.rxBytes += rxBytes[i];
+                entry.rxPackets += rxPackets[i];
+                entry.txBytes += txBytes[i];
+                entry.txPackets += txPackets[i];
+                entry.operations += operations[i];
+            }
+        }
+        return entry;
+    }
+
+    /**
+     * Fast path for battery stats.
+     */
+    public long getTotalPackets() {
+        long total = 0;
+        for (int i = size-1; i >= 0; i--) {
+            total += rxPackets[i] + txPackets[i];
+        }
+        return total;
+    }
+
+    /**
+     * Subtract the given {@link NetworkStats}, effectively leaving the delta
+     * between two snapshots in time. Assumes that statistics rows collect over
+     * time, and that none of them have disappeared.
+     */
+    public NetworkStats subtract(NetworkStats right) {
+        return subtract(this, right, null, null);
+    }
+
+    /**
+     * Subtract the two given {@link NetworkStats} objects, returning the delta
+     * between two snapshots in time. Assumes that statistics rows collect over
+     * time, and that none of them have disappeared.
+     * <p>
+     * If counters have rolled backwards, they are clamped to {@code 0} and
+     * reported to the given {@link NonMonotonicObserver}.
+     */
+    public static <C> NetworkStats subtract(NetworkStats left, NetworkStats right,
+            NonMonotonicObserver<C> observer, C cookie) {
+        return subtract(left, right, observer, cookie, null);
+    }
+
+    /**
+     * Subtract the two given {@link NetworkStats} objects, returning the delta
+     * between two snapshots in time. Assumes that statistics rows collect over
+     * time, and that none of them have disappeared.
+     * <p>
+     * If counters have rolled backwards, they are clamped to {@code 0} and
+     * reported to the given {@link NonMonotonicObserver}.
+     * <p>
+     * If <var>recycle</var> is supplied, this NetworkStats object will be
+     * reused (and returned) as the result if it is large enough to contain
+     * the data.
+     */
+    public static <C> NetworkStats subtract(NetworkStats left, NetworkStats right,
+            NonMonotonicObserver<C> observer, C cookie, NetworkStats recycle) {
+        long deltaRealtime = left.elapsedRealtime - right.elapsedRealtime;
+        if (deltaRealtime < 0) {
+            if (observer != null) {
+                observer.foundNonMonotonic(left, -1, right, -1, cookie);
+            }
+            deltaRealtime = 0;
+        }
+
+        // result will have our rows, and elapsed time between snapshots
+        final Entry entry = new Entry();
+        final NetworkStats result;
+        if (recycle != null && recycle.capacity >= left.size) {
+            result = recycle;
+            result.size = 0;
+            result.elapsedRealtime = deltaRealtime;
+        } else {
+            result = new NetworkStats(deltaRealtime, left.size);
+        }
+        for (int i = 0; i < left.size; i++) {
+            entry.iface = left.iface[i];
+            entry.uid = left.uid[i];
+            entry.set = left.set[i];
+            entry.tag = left.tag[i];
+            entry.metered = left.metered[i];
+            entry.roaming = left.roaming[i];
+            entry.defaultNetwork = left.defaultNetwork[i];
+            entry.rxBytes = left.rxBytes[i];
+            entry.rxPackets = left.rxPackets[i];
+            entry.txBytes = left.txBytes[i];
+            entry.txPackets = left.txPackets[i];
+            entry.operations = left.operations[i];
+
+            // find remote row that matches, and subtract
+            final int j = right.findIndexHinted(entry.iface, entry.uid, entry.set, entry.tag,
+                    entry.metered, entry.roaming, entry.defaultNetwork, i);
+            if (j != -1) {
+                // Found matching row, subtract remote value.
+                entry.rxBytes -= right.rxBytes[j];
+                entry.rxPackets -= right.rxPackets[j];
+                entry.txBytes -= right.txBytes[j];
+                entry.txPackets -= right.txPackets[j];
+                entry.operations -= right.operations[j];
+            }
+
+            if (entry.isNegative()) {
+                if (observer != null) {
+                    observer.foundNonMonotonic(left, i, right, j, cookie);
+                }
+                entry.rxBytes = Math.max(entry.rxBytes, 0);
+                entry.rxPackets = Math.max(entry.rxPackets, 0);
+                entry.txBytes = Math.max(entry.txBytes, 0);
+                entry.txPackets = Math.max(entry.txPackets, 0);
+                entry.operations = Math.max(entry.operations, 0);
+            }
+
+            result.addValues(entry);
+        }
+
+        return result;
+    }
+
+    /**
+     * Calculate and apply adjustments to captured statistics for 464xlat traffic counted twice.
+     *
+     * <p>This mutates both base and stacked traffic stats, to account respectively for
+     * double-counted traffic and IPv4/IPv6 header size difference.
+     *
+     * <p>For 464xlat traffic, xt_qtaguid sees every IPv4 packet twice, once as a native IPv4
+     * packet on the stacked interface, and once as translated to an IPv6 packet on the
+     * base interface. For correct stats accounting on the base interface, if using xt_qtaguid,
+     * every rx 464xlat packet needs to be subtracted from the root UID on the base interface
+     * (http://b/12249687, http:/b/33681750), and every tx 464xlat packet which was counted onto
+     * clat uid should be ignored.
+     *
+     * As for eBPF, the per uid stats is collected by different hook, the rx packets on base
+     * interface will not be counted. Thus, the adjustment on root uid is not needed. However, the
+     * tx traffic counted in the same way xt_qtaguid does, so the traffic on clat uid still
+     * needs to be ignored.
+     *
+     * <p>This method will behave fine if {@code stackedIfaces} is an non-synchronized but add-only
+     * {@code ConcurrentHashMap}
+     * @param baseTraffic Traffic on the base interfaces. Will be mutated.
+     * @param stackedTraffic Stats with traffic stacked on top of our ifaces. Will also be mutated.
+     * @param stackedIfaces Mapping ipv6if -> ipv4if interface where traffic is counted on both.
+     * @param useBpfStats True if eBPF is in use.
+     */
+    public static void apply464xlatAdjustments(NetworkStats baseTraffic,
+            NetworkStats stackedTraffic, Map<String, String> stackedIfaces, boolean useBpfStats) {
+        // Total 464xlat traffic to subtract from uid 0 on all base interfaces.
+        // stackedIfaces may grow afterwards, but NetworkStats will just be resized automatically.
+        final NetworkStats adjustments = new NetworkStats(0, stackedIfaces.size());
+
+        // For recycling
+        Entry entry = null;
+        Entry adjust = new NetworkStats.Entry(IFACE_ALL, 0, 0, 0, 0, 0, 0, 0L, 0L, 0L, 0L, 0L);
+
+        for (int i = 0; i < stackedTraffic.size; i++) {
+            entry = stackedTraffic.getValues(i, entry);
+            if (entry.iface == null || !entry.iface.startsWith(CLATD_INTERFACE_PREFIX)) {
+                continue;
+            }
+            final String baseIface = stackedIfaces.get(entry.iface);
+            if (baseIface == null) {
+                continue;
+            }
+            // Subtract xt_qtaguid 464lat rx traffic seen for the root UID on the current base
+            // interface. As for eBPF, the per uid stats is collected by different hook, the rx
+            // packets on base interface will not be counted.
+            adjust.iface = baseIface;
+            if (!useBpfStats) {
+                adjust.rxBytes = -(entry.rxBytes + entry.rxPackets * IPV4V6_HEADER_DELTA);
+                adjust.rxPackets = -entry.rxPackets;
+            }
+            adjustments.combineValues(adjust);
+
+            // For 464xlat traffic, per uid stats only counts the bytes of the native IPv4 packet
+            // sent on the stacked interface with prefix "v4-" and drops the IPv6 header size after
+            // unwrapping. To account correctly for on-the-wire traffic, add the 20 additional bytes
+            // difference for all packets (http://b/12249687, http:/b/33681750).
+            entry.rxBytes += entry.rxPackets * IPV4V6_HEADER_DELTA;
+            entry.txBytes += entry.txPackets * IPV4V6_HEADER_DELTA;
+            stackedTraffic.setValues(i, entry);
+        }
+
+        // Traffic on clat uid is v6 tx traffic that is already counted with app uid on the stacked
+        // v4 interface, so it needs to be removed to avoid double-counting.
+        baseTraffic.removeUids(new int[] {CLAT_UID});
+        baseTraffic.combineAllValues(adjustments);
+    }
+
+    /**
+     * Calculate and apply adjustments to captured statistics for 464xlat traffic counted twice.
+     *
+     * <p>This mutates the object this method is called on. Equivalent to calling
+     * {@link #apply464xlatAdjustments(NetworkStats, NetworkStats, Map)} with {@code this} as
+     * base and stacked traffic.
+     * @param stackedIfaces Mapping ipv6if -> ipv4if interface where traffic is counted on both.
+     */
+    public void apply464xlatAdjustments(Map<String, String> stackedIfaces, boolean useBpfStats) {
+        apply464xlatAdjustments(this, this, stackedIfaces, useBpfStats);
+    }
+
+    /**
+     * Return total statistics grouped by {@link #iface}; doesn't mutate the
+     * original structure.
+     */
+    public NetworkStats groupedByIface() {
+        final NetworkStats stats = new NetworkStats(elapsedRealtime, 10);
+
+        final Entry entry = new Entry();
+        entry.uid = UID_ALL;
+        entry.set = SET_ALL;
+        entry.tag = TAG_NONE;
+        entry.metered = METERED_ALL;
+        entry.roaming = ROAMING_ALL;
+        entry.defaultNetwork = DEFAULT_NETWORK_ALL;
+        entry.operations = 0L;
+
+        for (int i = 0; i < size; i++) {
+            // skip specific tags, since already counted in TAG_NONE
+            if (tag[i] != TAG_NONE) continue;
+
+            entry.iface = iface[i];
+            entry.rxBytes = rxBytes[i];
+            entry.rxPackets = rxPackets[i];
+            entry.txBytes = txBytes[i];
+            entry.txPackets = txPackets[i];
+            stats.combineValues(entry);
+        }
+
+        return stats;
+    }
+
+    /**
+     * Return total statistics grouped by {@link #uid}; doesn't mutate the
+     * original structure.
+     */
+    public NetworkStats groupedByUid() {
+        final NetworkStats stats = new NetworkStats(elapsedRealtime, 10);
+
+        final Entry entry = new Entry();
+        entry.iface = IFACE_ALL;
+        entry.set = SET_ALL;
+        entry.tag = TAG_NONE;
+        entry.metered = METERED_ALL;
+        entry.roaming = ROAMING_ALL;
+        entry.defaultNetwork = DEFAULT_NETWORK_ALL;
+
+        for (int i = 0; i < size; i++) {
+            // skip specific tags, since already counted in TAG_NONE
+            if (tag[i] != TAG_NONE) continue;
+
+            entry.uid = uid[i];
+            entry.rxBytes = rxBytes[i];
+            entry.rxPackets = rxPackets[i];
+            entry.txBytes = txBytes[i];
+            entry.txPackets = txPackets[i];
+            entry.operations = operations[i];
+            stats.combineValues(entry);
+        }
+
+        return stats;
+    }
+
+    /**
+     * Remove all rows that match one of specified UIDs.
+     */
+    public void removeUids(int[] uids) {
+        int nextOutputEntry = 0;
+        for (int i = 0; i < size; i++) {
+            if (!ArrayUtils.contains(uids, uid[i])) {
+                maybeCopyEntry(nextOutputEntry, i);
+                nextOutputEntry++;
+            }
+        }
+
+        size = nextOutputEntry;
+    }
+
+    /**
+     * Only keep entries that match all specified filters.
+     *
+     * <p>This mutates the original structure in place. After this method is called,
+     * size is the number of matching entries, and capacity is the previous capacity.
+     * @param limitUid UID to filter for, or {@link #UID_ALL}.
+     * @param limitIfaces Interfaces to filter for, or {@link #INTERFACES_ALL}.
+     * @param limitTag Tag to filter for, or {@link #TAG_ALL}.
+     */
+    public void filter(int limitUid, String[] limitIfaces, int limitTag) {
+        if (limitUid == UID_ALL && limitTag == TAG_ALL && limitIfaces == INTERFACES_ALL) {
+            return;
+        }
+
+        Entry entry = new Entry();
+        int nextOutputEntry = 0;
+        for (int i = 0; i < size; i++) {
+            entry = getValues(i, entry);
+            final boolean matches =
+                    (limitUid == UID_ALL || limitUid == entry.uid)
+                    && (limitTag == TAG_ALL || limitTag == entry.tag)
+                    && (limitIfaces == INTERFACES_ALL
+                            || ArrayUtils.contains(limitIfaces, entry.iface));
+
+            if (matches) {
+                setValues(nextOutputEntry, entry);
+                nextOutputEntry++;
+            }
+        }
+
+        size = nextOutputEntry;
+    }
+
+    public void dump(String prefix, PrintWriter pw) {
+        pw.print(prefix);
+        pw.print("NetworkStats: elapsedRealtime="); pw.println(elapsedRealtime);
+        for (int i = 0; i < size; i++) {
+            pw.print(prefix);
+            pw.print("  ["); pw.print(i); pw.print("]");
+            pw.print(" iface="); pw.print(iface[i]);
+            pw.print(" uid="); pw.print(uid[i]);
+            pw.print(" set="); pw.print(setToString(set[i]));
+            pw.print(" tag="); pw.print(tagToString(tag[i]));
+            pw.print(" metered="); pw.print(meteredToString(metered[i]));
+            pw.print(" roaming="); pw.print(roamingToString(roaming[i]));
+            pw.print(" defaultNetwork="); pw.print(defaultNetworkToString(defaultNetwork[i]));
+            pw.print(" rxBytes="); pw.print(rxBytes[i]);
+            pw.print(" rxPackets="); pw.print(rxPackets[i]);
+            pw.print(" txBytes="); pw.print(txBytes[i]);
+            pw.print(" txPackets="); pw.print(txPackets[i]);
+            pw.print(" operations="); pw.println(operations[i]);
+        }
+    }
+
+    /**
+     * Return text description of {@link #set} value.
+     */
+    public static String setToString(int set) {
+        switch (set) {
+            case SET_ALL:
+                return "ALL";
+            case SET_DEFAULT:
+                return "DEFAULT";
+            case SET_FOREGROUND:
+                return "FOREGROUND";
+            case SET_DBG_VPN_IN:
+                return "DBG_VPN_IN";
+            case SET_DBG_VPN_OUT:
+                return "DBG_VPN_OUT";
+            default:
+                return "UNKNOWN";
+        }
+    }
+
+    /**
+     * Return text description of {@link #set} value.
+     */
+    public static String setToCheckinString(int set) {
+        switch (set) {
+            case SET_ALL:
+                return "all";
+            case SET_DEFAULT:
+                return "def";
+            case SET_FOREGROUND:
+                return "fg";
+            case SET_DBG_VPN_IN:
+                return "vpnin";
+            case SET_DBG_VPN_OUT:
+                return "vpnout";
+            default:
+                return "unk";
+        }
+    }
+
+    /**
+     * @return true if the querySet matches the dataSet.
+     */
+    public static boolean setMatches(int querySet, int dataSet) {
+        if (querySet == dataSet) {
+            return true;
+        }
+        // SET_ALL matches all non-debugging sets.
+        return querySet == SET_ALL && dataSet < SET_DEBUG_START;
+    }
+
+    /**
+     * Return text description of {@link #tag} value.
+     */
+    public static String tagToString(int tag) {
+        return "0x" + Integer.toHexString(tag);
+    }
+
+    /**
+     * Return text description of {@link #metered} value.
+     */
+    public static String meteredToString(int metered) {
+        switch (metered) {
+            case METERED_ALL:
+                return "ALL";
+            case METERED_NO:
+                return "NO";
+            case METERED_YES:
+                return "YES";
+            default:
+                return "UNKNOWN";
+        }
+    }
+
+    /**
+     * Return text description of {@link #roaming} value.
+     */
+    public static String roamingToString(int roaming) {
+        switch (roaming) {
+            case ROAMING_ALL:
+                return "ALL";
+            case ROAMING_NO:
+                return "NO";
+            case ROAMING_YES:
+                return "YES";
+            default:
+                return "UNKNOWN";
+        }
+    }
+
+    /**
+     * Return text description of {@link #defaultNetwork} value.
+     */
+    public static String defaultNetworkToString(int defaultNetwork) {
+        switch (defaultNetwork) {
+            case DEFAULT_NETWORK_ALL:
+                return "ALL";
+            case DEFAULT_NETWORK_NO:
+                return "NO";
+            case DEFAULT_NETWORK_YES:
+                return "YES";
+            default:
+                return "UNKNOWN";
+        }
+    }
+
+    @Override
+    public String toString() {
+        final CharArrayWriter writer = new CharArrayWriter();
+        dump("", new PrintWriter(writer));
+        return writer.toString();
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @UnsupportedAppUsage
+    public static final @android.annotation.NonNull Creator<NetworkStats> CREATOR = new Creator<NetworkStats>() {
+        @Override
+        public NetworkStats createFromParcel(Parcel in) {
+            return new NetworkStats(in);
+        }
+
+        @Override
+        public NetworkStats[] newArray(int size) {
+            return new NetworkStats[size];
+        }
+    };
+
+    public interface NonMonotonicObserver<C> {
+        public void foundNonMonotonic(
+                NetworkStats left, int leftIndex, NetworkStats right, int rightIndex, C cookie);
+        public void foundNonMonotonic(
+                NetworkStats stats, int statsIndex, C cookie);
+    }
+
+    /**
+     * VPN accounting. Move some VPN's underlying traffic to other UIDs that use tun0 iface.
+     *
+     * This method should only be called on delta NetworkStats. Do not call this method on a
+     * snapshot {@link NetworkStats} object because the tunUid and/or the underlyingIface may
+     * change over time.
+     *
+     * This method performs adjustments for one active VPN package and one VPN iface at a time.
+     *
+     * It is possible for the VPN software to use multiple underlying networks. This method
+     * only migrates traffic for the primary underlying network.
+     *
+     * @param tunUid uid of the VPN application
+     * @param tunIface iface of the vpn tunnel
+     * @param underlyingIface the primary underlying network iface used by the VPN application
+     * @return true if it successfully adjusts the accounting for VPN, false otherwise
+     */
+    public boolean migrateTun(int tunUid, String tunIface, String underlyingIface) {
+        Entry tunIfaceTotal = new Entry();
+        Entry underlyingIfaceTotal = new Entry();
+
+        tunAdjustmentInit(tunUid, tunIface, underlyingIface, tunIfaceTotal, underlyingIfaceTotal);
+
+        // If tunIface < underlyingIface, it leaves the overhead traffic in the VPN app.
+        // If tunIface > underlyingIface, the VPN app doesn't get credit for data compression.
+        // Negative stats should be avoided.
+        Entry pool = tunGetPool(tunIfaceTotal, underlyingIfaceTotal);
+        if (pool.isEmpty()) {
+            return true;
+        }
+        Entry moved =
+                addTrafficToApplications(tunUid, tunIface, underlyingIface, tunIfaceTotal, pool);
+        deductTrafficFromVpnApp(tunUid, underlyingIface, moved);
+
+        if (!moved.isEmpty()) {
+            Slog.wtf(TAG, "Failed to deduct underlying network traffic from VPN package. Moved="
+                    + moved);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Initializes the data used by the migrateTun() method.
+     *
+     * This is the first pass iteration which does the following work:
+     * (1) Adds up all the traffic through the tunUid's underlyingIface
+     *     (both foreground and background).
+     * (2) Adds up all the traffic through tun0 excluding traffic from the vpn app itself.
+     */
+    private void tunAdjustmentInit(int tunUid, String tunIface, String underlyingIface,
+            Entry tunIfaceTotal, Entry underlyingIfaceTotal) {
+        Entry recycle = new Entry();
+        for (int i = 0; i < size; i++) {
+            getValues(i, recycle);
+            if (recycle.uid == UID_ALL) {
+                throw new IllegalStateException(
+                        "Cannot adjust VPN accounting on an iface aggregated NetworkStats.");
+            } if (recycle.set == SET_DBG_VPN_IN || recycle.set == SET_DBG_VPN_OUT) {
+                throw new IllegalStateException(
+                        "Cannot adjust VPN accounting on a NetworkStats containing SET_DBG_VPN_*");
+            }
+
+            if (recycle.uid == tunUid && recycle.tag == TAG_NONE
+                    && Objects.equals(underlyingIface, recycle.iface)) {
+                underlyingIfaceTotal.add(recycle);
+            }
+
+            if (recycle.uid != tunUid && recycle.tag == TAG_NONE
+                    && Objects.equals(tunIface, recycle.iface)) {
+                // Add up all tunIface traffic excluding traffic from the vpn app itself.
+                tunIfaceTotal.add(recycle);
+            }
+        }
+    }
+
+    private static Entry tunGetPool(Entry tunIfaceTotal, Entry underlyingIfaceTotal) {
+        Entry pool = new Entry();
+        pool.rxBytes = Math.min(tunIfaceTotal.rxBytes, underlyingIfaceTotal.rxBytes);
+        pool.rxPackets = Math.min(tunIfaceTotal.rxPackets, underlyingIfaceTotal.rxPackets);
+        pool.txBytes = Math.min(tunIfaceTotal.txBytes, underlyingIfaceTotal.txBytes);
+        pool.txPackets = Math.min(tunIfaceTotal.txPackets, underlyingIfaceTotal.txPackets);
+        pool.operations = Math.min(tunIfaceTotal.operations, underlyingIfaceTotal.operations);
+        return pool;
+    }
+
+    private Entry addTrafficToApplications(int tunUid, String tunIface, String underlyingIface,
+            Entry tunIfaceTotal, Entry pool) {
+        Entry moved = new Entry();
+        Entry tmpEntry = new Entry();
+        tmpEntry.iface = underlyingIface;
+        for (int i = 0; i < size; i++) {
+            // the vpn app is excluded from the redistribution but all moved traffic will be
+            // deducted from the vpn app (see deductTrafficFromVpnApp below).
+            if (Objects.equals(iface[i], tunIface) && uid[i] != tunUid) {
+                if (tunIfaceTotal.rxBytes > 0) {
+                    tmpEntry.rxBytes = pool.rxBytes * rxBytes[i] / tunIfaceTotal.rxBytes;
+                } else {
+                    tmpEntry.rxBytes = 0;
+                }
+                if (tunIfaceTotal.rxPackets > 0) {
+                    tmpEntry.rxPackets = pool.rxPackets * rxPackets[i] / tunIfaceTotal.rxPackets;
+                } else {
+                    tmpEntry.rxPackets = 0;
+                }
+                if (tunIfaceTotal.txBytes > 0) {
+                    tmpEntry.txBytes = pool.txBytes * txBytes[i] / tunIfaceTotal.txBytes;
+                } else {
+                    tmpEntry.txBytes = 0;
+                }
+                if (tunIfaceTotal.txPackets > 0) {
+                    tmpEntry.txPackets = pool.txPackets * txPackets[i] / tunIfaceTotal.txPackets;
+                } else {
+                    tmpEntry.txPackets = 0;
+                }
+                if (tunIfaceTotal.operations > 0) {
+                    tmpEntry.operations =
+                            pool.operations * operations[i] / tunIfaceTotal.operations;
+                } else {
+                    tmpEntry.operations = 0;
+                }
+                tmpEntry.uid = uid[i];
+                tmpEntry.tag = tag[i];
+                tmpEntry.set = set[i];
+                tmpEntry.metered = metered[i];
+                tmpEntry.roaming = roaming[i];
+                tmpEntry.defaultNetwork = defaultNetwork[i];
+                combineValues(tmpEntry);
+                if (tag[i] == TAG_NONE) {
+                    moved.add(tmpEntry);
+                    // Add debug info
+                    tmpEntry.set = SET_DBG_VPN_IN;
+                    combineValues(tmpEntry);
+                }
+            }
+        }
+        return moved;
+    }
+
+    private void deductTrafficFromVpnApp(int tunUid, String underlyingIface, Entry moved) {
+        // Add debug info
+        moved.uid = tunUid;
+        moved.set = SET_DBG_VPN_OUT;
+        moved.tag = TAG_NONE;
+        moved.iface = underlyingIface;
+        moved.metered = METERED_ALL;
+        moved.roaming = ROAMING_ALL;
+        moved.defaultNetwork = DEFAULT_NETWORK_ALL;
+        combineValues(moved);
+
+        // Caveat: if the vpn software uses tag, the total tagged traffic may be greater than
+        // the TAG_NONE traffic.
+        //
+        // Relies on the fact that the underlying traffic only has state ROAMING_NO and METERED_NO,
+        // which should be the case as it comes directly from the /proc file. We only blend in the
+        // roaming data after applying these adjustments, by checking the NetworkIdentity of the
+        // underlying iface.
+        int idxVpnBackground = findIndex(underlyingIface, tunUid, SET_DEFAULT, TAG_NONE,
+                METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO);
+        if (idxVpnBackground != -1) {
+            tunSubtract(idxVpnBackground, this, moved);
+        }
+
+        int idxVpnForeground = findIndex(underlyingIface, tunUid, SET_FOREGROUND, TAG_NONE,
+                METERED_NO, ROAMING_NO, DEFAULT_NETWORK_NO);
+        if (idxVpnForeground != -1) {
+            tunSubtract(idxVpnForeground, this, moved);
+        }
+    }
+
+    private static void tunSubtract(int i, NetworkStats left, Entry right) {
+        long rxBytes = Math.min(left.rxBytes[i], right.rxBytes);
+        left.rxBytes[i] -= rxBytes;
+        right.rxBytes -= rxBytes;
+
+        long rxPackets = Math.min(left.rxPackets[i], right.rxPackets);
+        left.rxPackets[i] -= rxPackets;
+        right.rxPackets -= rxPackets;
+
+        long txBytes = Math.min(left.txBytes[i], right.txBytes);
+        left.txBytes[i] -= txBytes;
+        right.txBytes -= txBytes;
+
+        long txPackets = Math.min(left.txPackets[i], right.txPackets);
+        left.txPackets[i] -= txPackets;
+        right.txPackets -= txPackets;
+    }
+}
diff --git a/android/net/NetworkStatsHistory.java b/android/net/NetworkStatsHistory.java
new file mode 100644
index 0000000..f61260e
--- /dev/null
+++ b/android/net/NetworkStatsHistory.java
@@ -0,0 +1,854 @@
+/*
+ * Copyright (C) 2011 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 static android.net.NetworkStats.IFACE_ALL;
+import static android.net.NetworkStats.SET_DEFAULT;
+import static android.net.NetworkStats.TAG_NONE;
+import static android.net.NetworkStats.UID_ALL;
+import static android.net.NetworkStatsHistory.DataStreamUtils.readFullLongArray;
+import static android.net.NetworkStatsHistory.DataStreamUtils.readVarLongArray;
+import static android.net.NetworkStatsHistory.DataStreamUtils.writeVarLongArray;
+import static android.net.NetworkStatsHistory.Entry.UNKNOWN;
+import static android.net.NetworkStatsHistory.ParcelUtils.readLongArray;
+import static android.net.NetworkStatsHistory.ParcelUtils.writeLongArray;
+import static android.text.format.DateUtils.SECOND_IN_MILLIS;
+
+import static com.android.internal.util.ArrayUtils.total;
+
+import android.annotation.UnsupportedAppUsage;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.service.NetworkStatsHistoryBucketProto;
+import android.service.NetworkStatsHistoryProto;
+import android.util.MathUtils;
+import android.util.proto.ProtoOutputStream;
+
+import com.android.internal.util.IndentingPrintWriter;
+
+import libcore.util.EmptyArray;
+
+import java.io.CharArrayWriter;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.net.ProtocolException;
+import java.util.Arrays;
+import java.util.Random;
+
+/**
+ * Collection of historical network statistics, recorded into equally-sized
+ * "buckets" in time. Internally it stores data in {@code long} series for more
+ * efficient persistence.
+ * <p>
+ * Each bucket is defined by a {@link #bucketStart} timestamp, and lasts for
+ * {@link #bucketDuration}. Internally assumes that {@link #bucketStart} is
+ * sorted at all times.
+ *
+ * @hide
+ */
+public class NetworkStatsHistory implements Parcelable {
+    private static final int VERSION_INIT = 1;
+    private static final int VERSION_ADD_PACKETS = 2;
+    private static final int VERSION_ADD_ACTIVE = 3;
+
+    public static final int FIELD_ACTIVE_TIME = 0x01;
+    public static final int FIELD_RX_BYTES = 0x02;
+    public static final int FIELD_RX_PACKETS = 0x04;
+    public static final int FIELD_TX_BYTES = 0x08;
+    public static final int FIELD_TX_PACKETS = 0x10;
+    public static final int FIELD_OPERATIONS = 0x20;
+
+    public static final int FIELD_ALL = 0xFFFFFFFF;
+
+    private long bucketDuration;
+    private int bucketCount;
+    private long[] bucketStart;
+    private long[] activeTime;
+    private long[] rxBytes;
+    private long[] rxPackets;
+    private long[] txBytes;
+    private long[] txPackets;
+    private long[] operations;
+    private long totalBytes;
+
+    public static class Entry {
+        public static final long UNKNOWN = -1;
+
+        @UnsupportedAppUsage
+        public long bucketDuration;
+        @UnsupportedAppUsage
+        public long bucketStart;
+        public long activeTime;
+        @UnsupportedAppUsage
+        public long rxBytes;
+        @UnsupportedAppUsage
+        public long rxPackets;
+        @UnsupportedAppUsage
+        public long txBytes;
+        @UnsupportedAppUsage
+        public long txPackets;
+        public long operations;
+    }
+
+    @UnsupportedAppUsage
+    public NetworkStatsHistory(long bucketDuration) {
+        this(bucketDuration, 10, FIELD_ALL);
+    }
+
+    public NetworkStatsHistory(long bucketDuration, int initialSize) {
+        this(bucketDuration, initialSize, FIELD_ALL);
+    }
+
+    public NetworkStatsHistory(long bucketDuration, int initialSize, int fields) {
+        this.bucketDuration = bucketDuration;
+        bucketStart = new long[initialSize];
+        if ((fields & FIELD_ACTIVE_TIME) != 0) activeTime = new long[initialSize];
+        if ((fields & FIELD_RX_BYTES) != 0) rxBytes = new long[initialSize];
+        if ((fields & FIELD_RX_PACKETS) != 0) rxPackets = new long[initialSize];
+        if ((fields & FIELD_TX_BYTES) != 0) txBytes = new long[initialSize];
+        if ((fields & FIELD_TX_PACKETS) != 0) txPackets = new long[initialSize];
+        if ((fields & FIELD_OPERATIONS) != 0) operations = new long[initialSize];
+        bucketCount = 0;
+        totalBytes = 0;
+    }
+
+    public NetworkStatsHistory(NetworkStatsHistory existing, long bucketDuration) {
+        this(bucketDuration, existing.estimateResizeBuckets(bucketDuration));
+        recordEntireHistory(existing);
+    }
+
+    @UnsupportedAppUsage
+    public NetworkStatsHistory(Parcel in) {
+        bucketDuration = in.readLong();
+        bucketStart = readLongArray(in);
+        activeTime = readLongArray(in);
+        rxBytes = readLongArray(in);
+        rxPackets = readLongArray(in);
+        txBytes = readLongArray(in);
+        txPackets = readLongArray(in);
+        operations = readLongArray(in);
+        bucketCount = bucketStart.length;
+        totalBytes = in.readLong();
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeLong(bucketDuration);
+        writeLongArray(out, bucketStart, bucketCount);
+        writeLongArray(out, activeTime, bucketCount);
+        writeLongArray(out, rxBytes, bucketCount);
+        writeLongArray(out, rxPackets, bucketCount);
+        writeLongArray(out, txBytes, bucketCount);
+        writeLongArray(out, txPackets, bucketCount);
+        writeLongArray(out, operations, bucketCount);
+        out.writeLong(totalBytes);
+    }
+
+    public NetworkStatsHistory(DataInputStream in) throws IOException {
+        final int version = in.readInt();
+        switch (version) {
+            case VERSION_INIT: {
+                bucketDuration = in.readLong();
+                bucketStart = readFullLongArray(in);
+                rxBytes = readFullLongArray(in);
+                rxPackets = new long[bucketStart.length];
+                txBytes = readFullLongArray(in);
+                txPackets = new long[bucketStart.length];
+                operations = new long[bucketStart.length];
+                bucketCount = bucketStart.length;
+                totalBytes = total(rxBytes) + total(txBytes);
+                break;
+            }
+            case VERSION_ADD_PACKETS:
+            case VERSION_ADD_ACTIVE: {
+                bucketDuration = in.readLong();
+                bucketStart = readVarLongArray(in);
+                activeTime = (version >= VERSION_ADD_ACTIVE) ? readVarLongArray(in)
+                        : new long[bucketStart.length];
+                rxBytes = readVarLongArray(in);
+                rxPackets = readVarLongArray(in);
+                txBytes = readVarLongArray(in);
+                txPackets = readVarLongArray(in);
+                operations = readVarLongArray(in);
+                bucketCount = bucketStart.length;
+                totalBytes = total(rxBytes) + total(txBytes);
+                break;
+            }
+            default: {
+                throw new ProtocolException("unexpected version: " + version);
+            }
+        }
+
+        if (bucketStart.length != bucketCount || rxBytes.length != bucketCount
+                || rxPackets.length != bucketCount || txBytes.length != bucketCount
+                || txPackets.length != bucketCount || operations.length != bucketCount) {
+            throw new ProtocolException("Mismatched history lengths");
+        }
+    }
+
+    public void writeToStream(DataOutputStream out) throws IOException {
+        out.writeInt(VERSION_ADD_ACTIVE);
+        out.writeLong(bucketDuration);
+        writeVarLongArray(out, bucketStart, bucketCount);
+        writeVarLongArray(out, activeTime, bucketCount);
+        writeVarLongArray(out, rxBytes, bucketCount);
+        writeVarLongArray(out, rxPackets, bucketCount);
+        writeVarLongArray(out, txBytes, bucketCount);
+        writeVarLongArray(out, txPackets, bucketCount);
+        writeVarLongArray(out, operations, bucketCount);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @UnsupportedAppUsage
+    public int size() {
+        return bucketCount;
+    }
+
+    public long getBucketDuration() {
+        return bucketDuration;
+    }
+
+    @UnsupportedAppUsage
+    public long getStart() {
+        if (bucketCount > 0) {
+            return bucketStart[0];
+        } else {
+            return Long.MAX_VALUE;
+        }
+    }
+
+    @UnsupportedAppUsage
+    public long getEnd() {
+        if (bucketCount > 0) {
+            return bucketStart[bucketCount - 1] + bucketDuration;
+        } else {
+            return Long.MIN_VALUE;
+        }
+    }
+
+    /**
+     * Return total bytes represented by this history.
+     */
+    public long getTotalBytes() {
+        return totalBytes;
+    }
+
+    /**
+     * Return index of bucket that contains or is immediately before the
+     * requested time.
+     */
+    @UnsupportedAppUsage
+    public int getIndexBefore(long time) {
+        int index = Arrays.binarySearch(bucketStart, 0, bucketCount, time);
+        if (index < 0) {
+            index = (~index) - 1;
+        } else {
+            index -= 1;
+        }
+        return MathUtils.constrain(index, 0, bucketCount - 1);
+    }
+
+    /**
+     * Return index of bucket that contains or is immediately after the
+     * requested time.
+     */
+    public int getIndexAfter(long time) {
+        int index = Arrays.binarySearch(bucketStart, 0, bucketCount, time);
+        if (index < 0) {
+            index = ~index;
+        } else {
+            index += 1;
+        }
+        return MathUtils.constrain(index, 0, bucketCount - 1);
+    }
+
+    /**
+     * Return specific stats entry.
+     */
+    @UnsupportedAppUsage
+    public Entry getValues(int i, Entry recycle) {
+        final Entry entry = recycle != null ? recycle : new Entry();
+        entry.bucketStart = bucketStart[i];
+        entry.bucketDuration = bucketDuration;
+        entry.activeTime = getLong(activeTime, i, UNKNOWN);
+        entry.rxBytes = getLong(rxBytes, i, UNKNOWN);
+        entry.rxPackets = getLong(rxPackets, i, UNKNOWN);
+        entry.txBytes = getLong(txBytes, i, UNKNOWN);
+        entry.txPackets = getLong(txPackets, i, UNKNOWN);
+        entry.operations = getLong(operations, i, UNKNOWN);
+        return entry;
+    }
+
+    public void setValues(int i, Entry entry) {
+        // Unwind old values
+        if (rxBytes != null) totalBytes -= rxBytes[i];
+        if (txBytes != null) totalBytes -= txBytes[i];
+
+        bucketStart[i] = entry.bucketStart;
+        setLong(activeTime, i, entry.activeTime);
+        setLong(rxBytes, i, entry.rxBytes);
+        setLong(rxPackets, i, entry.rxPackets);
+        setLong(txBytes, i, entry.txBytes);
+        setLong(txPackets, i, entry.txPackets);
+        setLong(operations, i, entry.operations);
+
+        // Apply new values
+        if (rxBytes != null) totalBytes += rxBytes[i];
+        if (txBytes != null) totalBytes += txBytes[i];
+    }
+
+    /**
+     * Record that data traffic occurred in the given time range. Will
+     * distribute across internal buckets, creating new buckets as needed.
+     */
+    @Deprecated
+    public void recordData(long start, long end, long rxBytes, long txBytes) {
+        recordData(start, end, new NetworkStats.Entry(
+                IFACE_ALL, UID_ALL, SET_DEFAULT, TAG_NONE, rxBytes, 0L, txBytes, 0L, 0L));
+    }
+
+    /**
+     * Record that data traffic occurred in the given time range. Will
+     * distribute across internal buckets, creating new buckets as needed.
+     */
+    public void recordData(long start, long end, NetworkStats.Entry entry) {
+        long rxBytes = entry.rxBytes;
+        long rxPackets = entry.rxPackets;
+        long txBytes = entry.txBytes;
+        long txPackets = entry.txPackets;
+        long operations = entry.operations;
+
+        if (entry.isNegative()) {
+            throw new IllegalArgumentException("tried recording negative data");
+        }
+        if (entry.isEmpty()) {
+            return;
+        }
+
+        // create any buckets needed by this range
+        ensureBuckets(start, end);
+
+        // distribute data usage into buckets
+        long duration = end - start;
+        final int startIndex = getIndexAfter(end);
+        for (int i = startIndex; i >= 0; i--) {
+            final long curStart = bucketStart[i];
+            final long curEnd = curStart + bucketDuration;
+
+            // bucket is older than record; we're finished
+            if (curEnd < start) break;
+            // bucket is newer than record; keep looking
+            if (curStart > end) continue;
+
+            final long overlap = Math.min(curEnd, end) - Math.max(curStart, start);
+            if (overlap <= 0) continue;
+
+            // integer math each time is faster than floating point
+            final long fracRxBytes = rxBytes * overlap / duration;
+            final long fracRxPackets = rxPackets * overlap / duration;
+            final long fracTxBytes = txBytes * overlap / duration;
+            final long fracTxPackets = txPackets * overlap / duration;
+            final long fracOperations = operations * overlap / duration;
+
+            addLong(activeTime, i, overlap);
+            addLong(this.rxBytes, i, fracRxBytes); rxBytes -= fracRxBytes;
+            addLong(this.rxPackets, i, fracRxPackets); rxPackets -= fracRxPackets;
+            addLong(this.txBytes, i, fracTxBytes); txBytes -= fracTxBytes;
+            addLong(this.txPackets, i, fracTxPackets); txPackets -= fracTxPackets;
+            addLong(this.operations, i, fracOperations); operations -= fracOperations;
+
+            duration -= overlap;
+        }
+
+        totalBytes += entry.rxBytes + entry.txBytes;
+    }
+
+    /**
+     * Record an entire {@link NetworkStatsHistory} into this history. Usually
+     * for combining together stats for external reporting.
+     */
+    @UnsupportedAppUsage
+    public void recordEntireHistory(NetworkStatsHistory input) {
+        recordHistory(input, Long.MIN_VALUE, Long.MAX_VALUE);
+    }
+
+    /**
+     * Record given {@link NetworkStatsHistory} into this history, copying only
+     * buckets that atomically occur in the inclusive time range. Doesn't
+     * interpolate across partial buckets.
+     */
+    public void recordHistory(NetworkStatsHistory input, long start, long end) {
+        final NetworkStats.Entry entry = new NetworkStats.Entry(
+                IFACE_ALL, UID_ALL, SET_DEFAULT, TAG_NONE, 0L, 0L, 0L, 0L, 0L);
+        for (int i = 0; i < input.bucketCount; i++) {
+            final long bucketStart = input.bucketStart[i];
+            final long bucketEnd = bucketStart + input.bucketDuration;
+
+            // skip when bucket is outside requested range
+            if (bucketStart < start || bucketEnd > end) continue;
+
+            entry.rxBytes = getLong(input.rxBytes, i, 0L);
+            entry.rxPackets = getLong(input.rxPackets, i, 0L);
+            entry.txBytes = getLong(input.txBytes, i, 0L);
+            entry.txPackets = getLong(input.txPackets, i, 0L);
+            entry.operations = getLong(input.operations, i, 0L);
+
+            recordData(bucketStart, bucketEnd, entry);
+        }
+    }
+
+    /**
+     * Ensure that buckets exist for given time range, creating as needed.
+     */
+    private void ensureBuckets(long start, long end) {
+        // normalize incoming range to bucket boundaries
+        start -= start % bucketDuration;
+        end += (bucketDuration - (end % bucketDuration)) % bucketDuration;
+
+        for (long now = start; now < end; now += bucketDuration) {
+            // try finding existing bucket
+            final int index = Arrays.binarySearch(bucketStart, 0, bucketCount, now);
+            if (index < 0) {
+                // bucket missing, create and insert
+                insertBucket(~index, now);
+            }
+        }
+    }
+
+    /**
+     * Insert new bucket at requested index and starting time.
+     */
+    private void insertBucket(int index, long start) {
+        // create more buckets when needed
+        if (bucketCount >= bucketStart.length) {
+            final int newLength = Math.max(bucketStart.length, 10) * 3 / 2;
+            bucketStart = Arrays.copyOf(bucketStart, newLength);
+            if (activeTime != null) activeTime = Arrays.copyOf(activeTime, newLength);
+            if (rxBytes != null) rxBytes = Arrays.copyOf(rxBytes, newLength);
+            if (rxPackets != null) rxPackets = Arrays.copyOf(rxPackets, newLength);
+            if (txBytes != null) txBytes = Arrays.copyOf(txBytes, newLength);
+            if (txPackets != null) txPackets = Arrays.copyOf(txPackets, newLength);
+            if (operations != null) operations = Arrays.copyOf(operations, newLength);
+        }
+
+        // create gap when inserting bucket in middle
+        if (index < bucketCount) {
+            final int dstPos = index + 1;
+            final int length = bucketCount - index;
+
+            System.arraycopy(bucketStart, index, bucketStart, dstPos, length);
+            if (activeTime != null) System.arraycopy(activeTime, index, activeTime, dstPos, length);
+            if (rxBytes != null) System.arraycopy(rxBytes, index, rxBytes, dstPos, length);
+            if (rxPackets != null) System.arraycopy(rxPackets, index, rxPackets, dstPos, length);
+            if (txBytes != null) System.arraycopy(txBytes, index, txBytes, dstPos, length);
+            if (txPackets != null) System.arraycopy(txPackets, index, txPackets, dstPos, length);
+            if (operations != null) System.arraycopy(operations, index, operations, dstPos, length);
+        }
+
+        bucketStart[index] = start;
+        setLong(activeTime, index, 0L);
+        setLong(rxBytes, index, 0L);
+        setLong(rxPackets, index, 0L);
+        setLong(txBytes, index, 0L);
+        setLong(txPackets, index, 0L);
+        setLong(operations, index, 0L);
+        bucketCount++;
+    }
+
+    /**
+     * Clear all data stored in this object.
+     */
+    public void clear() {
+        bucketStart = EmptyArray.LONG;
+        if (activeTime != null) activeTime = EmptyArray.LONG;
+        if (rxBytes != null) rxBytes = EmptyArray.LONG;
+        if (rxPackets != null) rxPackets = EmptyArray.LONG;
+        if (txBytes != null) txBytes = EmptyArray.LONG;
+        if (txPackets != null) txPackets = EmptyArray.LONG;
+        if (operations != null) operations = EmptyArray.LONG;
+        bucketCount = 0;
+        totalBytes = 0;
+    }
+
+    /**
+     * Remove buckets older than requested cutoff.
+     */
+    @Deprecated
+    public void removeBucketsBefore(long cutoff) {
+        int i;
+        for (i = 0; i < bucketCount; i++) {
+            final long curStart = bucketStart[i];
+            final long curEnd = curStart + bucketDuration;
+
+            // cutoff happens before or during this bucket; everything before
+            // this bucket should be removed.
+            if (curEnd > cutoff) break;
+        }
+
+        if (i > 0) {
+            final int length = bucketStart.length;
+            bucketStart = Arrays.copyOfRange(bucketStart, i, length);
+            if (activeTime != null) activeTime = Arrays.copyOfRange(activeTime, i, length);
+            if (rxBytes != null) rxBytes = Arrays.copyOfRange(rxBytes, i, length);
+            if (rxPackets != null) rxPackets = Arrays.copyOfRange(rxPackets, i, length);
+            if (txBytes != null) txBytes = Arrays.copyOfRange(txBytes, i, length);
+            if (txPackets != null) txPackets = Arrays.copyOfRange(txPackets, i, length);
+            if (operations != null) operations = Arrays.copyOfRange(operations, i, length);
+            bucketCount -= i;
+
+            // TODO: subtract removed values from totalBytes
+        }
+    }
+
+    /**
+     * Return interpolated data usage across the requested range. Interpolates
+     * across buckets, so values may be rounded slightly.
+     */
+    @UnsupportedAppUsage
+    public Entry getValues(long start, long end, Entry recycle) {
+        return getValues(start, end, Long.MAX_VALUE, recycle);
+    }
+
+    /**
+     * Return interpolated data usage across the requested range. Interpolates
+     * across buckets, so values may be rounded slightly.
+     */
+    @UnsupportedAppUsage
+    public Entry getValues(long start, long end, long now, Entry recycle) {
+        final Entry entry = recycle != null ? recycle : new Entry();
+        entry.bucketDuration = end - start;
+        entry.bucketStart = start;
+        entry.activeTime = activeTime != null ? 0 : UNKNOWN;
+        entry.rxBytes = rxBytes != null ? 0 : UNKNOWN;
+        entry.rxPackets = rxPackets != null ? 0 : UNKNOWN;
+        entry.txBytes = txBytes != null ? 0 : UNKNOWN;
+        entry.txPackets = txPackets != null ? 0 : UNKNOWN;
+        entry.operations = operations != null ? 0 : UNKNOWN;
+
+        final int startIndex = getIndexAfter(end);
+        for (int i = startIndex; i >= 0; i--) {
+            final long curStart = bucketStart[i];
+            final long curEnd = curStart + bucketDuration;
+
+            // bucket is older than request; we're finished
+            if (curEnd <= start) break;
+            // bucket is newer than request; keep looking
+            if (curStart >= end) continue;
+
+            // include full value for active buckets, otherwise only fractional
+            final boolean activeBucket = curStart < now && curEnd > now;
+            final long overlap;
+            if (activeBucket) {
+                overlap = bucketDuration;
+            } else {
+                final long overlapEnd = curEnd < end ? curEnd : end;
+                final long overlapStart = curStart > start ? curStart : start;
+                overlap = overlapEnd - overlapStart;
+            }
+            if (overlap <= 0) continue;
+
+            // integer math each time is faster than floating point
+            if (activeTime != null) entry.activeTime += activeTime[i] * overlap / bucketDuration;
+            if (rxBytes != null) entry.rxBytes += rxBytes[i] * overlap / bucketDuration;
+            if (rxPackets != null) entry.rxPackets += rxPackets[i] * overlap / bucketDuration;
+            if (txBytes != null) entry.txBytes += txBytes[i] * overlap / bucketDuration;
+            if (txPackets != null) entry.txPackets += txPackets[i] * overlap / bucketDuration;
+            if (operations != null) entry.operations += operations[i] * overlap / bucketDuration;
+        }
+        return entry;
+    }
+
+    /**
+     * @deprecated only for temporary testing
+     */
+    @Deprecated
+    public void generateRandom(long start, long end, long bytes) {
+        final Random r = new Random();
+
+        final float fractionRx = r.nextFloat();
+        final long rxBytes = (long) (bytes * fractionRx);
+        final long txBytes = (long) (bytes * (1 - fractionRx));
+
+        final long rxPackets = rxBytes / 1024;
+        final long txPackets = txBytes / 1024;
+        final long operations = rxBytes / 2048;
+
+        generateRandom(start, end, rxBytes, rxPackets, txBytes, txPackets, operations, r);
+    }
+
+    /**
+     * @deprecated only for temporary testing
+     */
+    @Deprecated
+    public void generateRandom(long start, long end, long rxBytes, long rxPackets, long txBytes,
+            long txPackets, long operations, Random r) {
+        ensureBuckets(start, end);
+
+        final NetworkStats.Entry entry = new NetworkStats.Entry(
+                IFACE_ALL, UID_ALL, SET_DEFAULT, TAG_NONE, 0L, 0L, 0L, 0L, 0L);
+        while (rxBytes > 1024 || rxPackets > 128 || txBytes > 1024 || txPackets > 128
+                || operations > 32) {
+            final long curStart = randomLong(r, start, end);
+            final long curEnd = curStart + randomLong(r, 0, (end - curStart) / 2);
+
+            entry.rxBytes = randomLong(r, 0, rxBytes);
+            entry.rxPackets = randomLong(r, 0, rxPackets);
+            entry.txBytes = randomLong(r, 0, txBytes);
+            entry.txPackets = randomLong(r, 0, txPackets);
+            entry.operations = randomLong(r, 0, operations);
+
+            rxBytes -= entry.rxBytes;
+            rxPackets -= entry.rxPackets;
+            txBytes -= entry.txBytes;
+            txPackets -= entry.txPackets;
+            operations -= entry.operations;
+
+            recordData(curStart, curEnd, entry);
+        }
+    }
+
+    public static long randomLong(Random r, long start, long end) {
+        return (long) (start + (r.nextFloat() * (end - start)));
+    }
+
+    /**
+     * Quickly determine if this history intersects with given window.
+     */
+    public boolean intersects(long start, long end) {
+        final long dataStart = getStart();
+        final long dataEnd = getEnd();
+        if (start >= dataStart && start <= dataEnd) return true;
+        if (end >= dataStart && end <= dataEnd) return true;
+        if (dataStart >= start && dataStart <= end) return true;
+        if (dataEnd >= start && dataEnd <= end) return true;
+        return false;
+    }
+
+    public void dump(IndentingPrintWriter pw, boolean fullHistory) {
+        pw.print("NetworkStatsHistory: bucketDuration=");
+        pw.println(bucketDuration / SECOND_IN_MILLIS);
+        pw.increaseIndent();
+
+        final int start = fullHistory ? 0 : Math.max(0, bucketCount - 32);
+        if (start > 0) {
+            pw.print("(omitting "); pw.print(start); pw.println(" buckets)");
+        }
+
+        for (int i = start; i < bucketCount; i++) {
+            pw.print("st="); pw.print(bucketStart[i] / SECOND_IN_MILLIS);
+            if (rxBytes != null) { pw.print(" rb="); pw.print(rxBytes[i]); }
+            if (rxPackets != null) { pw.print(" rp="); pw.print(rxPackets[i]); }
+            if (txBytes != null) { pw.print(" tb="); pw.print(txBytes[i]); }
+            if (txPackets != null) { pw.print(" tp="); pw.print(txPackets[i]); }
+            if (operations != null) { pw.print(" op="); pw.print(operations[i]); }
+            pw.println();
+        }
+
+        pw.decreaseIndent();
+    }
+
+    public void dumpCheckin(PrintWriter pw) {
+        pw.print("d,");
+        pw.print(bucketDuration / SECOND_IN_MILLIS);
+        pw.println();
+
+        for (int i = 0; i < bucketCount; i++) {
+            pw.print("b,");
+            pw.print(bucketStart[i] / SECOND_IN_MILLIS); pw.print(',');
+            if (rxBytes != null) { pw.print(rxBytes[i]); } else { pw.print("*"); } pw.print(',');
+            if (rxPackets != null) { pw.print(rxPackets[i]); } else { pw.print("*"); } pw.print(',');
+            if (txBytes != null) { pw.print(txBytes[i]); } else { pw.print("*"); } pw.print(',');
+            if (txPackets != null) { pw.print(txPackets[i]); } else { pw.print("*"); } pw.print(',');
+            if (operations != null) { pw.print(operations[i]); } else { pw.print("*"); }
+            pw.println();
+        }
+    }
+
+    public void writeToProto(ProtoOutputStream proto, long tag) {
+        final long start = proto.start(tag);
+
+        proto.write(NetworkStatsHistoryProto.BUCKET_DURATION_MS, bucketDuration);
+
+        for (int i = 0; i < bucketCount; i++) {
+            final long startBucket = proto.start(NetworkStatsHistoryProto.BUCKETS);
+
+            proto.write(NetworkStatsHistoryBucketProto.BUCKET_START_MS, bucketStart[i]);
+            writeToProto(proto, NetworkStatsHistoryBucketProto.RX_BYTES, rxBytes, i);
+            writeToProto(proto, NetworkStatsHistoryBucketProto.RX_PACKETS, rxPackets, i);
+            writeToProto(proto, NetworkStatsHistoryBucketProto.TX_BYTES, txBytes, i);
+            writeToProto(proto, NetworkStatsHistoryBucketProto.TX_PACKETS, txPackets, i);
+            writeToProto(proto, NetworkStatsHistoryBucketProto.OPERATIONS, operations, i);
+
+            proto.end(startBucket);
+        }
+
+        proto.end(start);
+    }
+
+    private static void writeToProto(ProtoOutputStream proto, long tag, long[] array, int index) {
+        if (array != null) {
+            proto.write(tag, array[index]);
+        }
+    }
+
+    @Override
+    public String toString() {
+        final CharArrayWriter writer = new CharArrayWriter();
+        dump(new IndentingPrintWriter(writer, "  "), false);
+        return writer.toString();
+    }
+
+    @UnsupportedAppUsage
+    public static final @android.annotation.NonNull Creator<NetworkStatsHistory> CREATOR = new Creator<NetworkStatsHistory>() {
+        @Override
+        public NetworkStatsHistory createFromParcel(Parcel in) {
+            return new NetworkStatsHistory(in);
+        }
+
+        @Override
+        public NetworkStatsHistory[] newArray(int size) {
+            return new NetworkStatsHistory[size];
+        }
+    };
+
+    private static long getLong(long[] array, int i, long value) {
+        return array != null ? array[i] : value;
+    }
+
+    private static void setLong(long[] array, int i, long value) {
+        if (array != null) array[i] = value;
+    }
+
+    private static void addLong(long[] array, int i, long value) {
+        if (array != null) array[i] += value;
+    }
+
+    public int estimateResizeBuckets(long newBucketDuration) {
+        return (int) (size() * getBucketDuration() / newBucketDuration);
+    }
+
+    /**
+     * Utility methods for interacting with {@link DataInputStream} and
+     * {@link DataOutputStream}, mostly dealing with writing partial arrays.
+     */
+    public static class DataStreamUtils {
+        @Deprecated
+        public static long[] readFullLongArray(DataInputStream in) throws IOException {
+            final int size = in.readInt();
+            if (size < 0) throw new ProtocolException("negative array size");
+            final long[] values = new long[size];
+            for (int i = 0; i < values.length; i++) {
+                values[i] = in.readLong();
+            }
+            return values;
+        }
+
+        /**
+         * Read variable-length {@link Long} using protobuf-style approach.
+         */
+        public static long readVarLong(DataInputStream in) throws IOException {
+            int shift = 0;
+            long result = 0;
+            while (shift < 64) {
+                byte b = in.readByte();
+                result |= (long) (b & 0x7F) << shift;
+                if ((b & 0x80) == 0)
+                    return result;
+                shift += 7;
+            }
+            throw new ProtocolException("malformed long");
+        }
+
+        /**
+         * Write variable-length {@link Long} using protobuf-style approach.
+         */
+        public static void writeVarLong(DataOutputStream out, long value) throws IOException {
+            while (true) {
+                if ((value & ~0x7FL) == 0) {
+                    out.writeByte((int) value);
+                    return;
+                } else {
+                    out.writeByte(((int) value & 0x7F) | 0x80);
+                    value >>>= 7;
+                }
+            }
+        }
+
+        public static long[] readVarLongArray(DataInputStream in) throws IOException {
+            final int size = in.readInt();
+            if (size == -1) return null;
+            if (size < 0) throw new ProtocolException("negative array size");
+            final long[] values = new long[size];
+            for (int i = 0; i < values.length; i++) {
+                values[i] = readVarLong(in);
+            }
+            return values;
+        }
+
+        public static void writeVarLongArray(DataOutputStream out, long[] values, int size)
+                throws IOException {
+            if (values == null) {
+                out.writeInt(-1);
+                return;
+            }
+            if (size > values.length) {
+                throw new IllegalArgumentException("size larger than length");
+            }
+            out.writeInt(size);
+            for (int i = 0; i < size; i++) {
+                writeVarLong(out, values[i]);
+            }
+        }
+    }
+
+    /**
+     * Utility methods for interacting with {@link Parcel} structures, mostly
+     * dealing with writing partial arrays.
+     */
+    public static class ParcelUtils {
+        public static long[] readLongArray(Parcel in) {
+            final int size = in.readInt();
+            if (size == -1) return null;
+            final long[] values = new long[size];
+            for (int i = 0; i < values.length; i++) {
+                values[i] = in.readLong();
+            }
+            return values;
+        }
+
+        public static void writeLongArray(Parcel out, long[] values, int size) {
+            if (values == null) {
+                out.writeInt(-1);
+                return;
+            }
+            if (size > values.length) {
+                throw new IllegalArgumentException("size larger than length");
+            }
+            out.writeInt(size);
+            for (int i = 0; i < size; i++) {
+                out.writeLong(values[i]);
+            }
+        }
+    }
+
+}
diff --git a/android/net/NetworkTemplate.java b/android/net/NetworkTemplate.java
new file mode 100644
index 0000000..ae421a4
--- /dev/null
+++ b/android/net/NetworkTemplate.java
@@ -0,0 +1,541 @@
+/*
+ * Copyright (C) 2011 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 static android.net.ConnectivityManager.TYPE_BLUETOOTH;
+import static android.net.ConnectivityManager.TYPE_ETHERNET;
+import static android.net.ConnectivityManager.TYPE_MOBILE;
+import static android.net.ConnectivityManager.TYPE_PROXY;
+import static android.net.ConnectivityManager.TYPE_WIFI;
+import static android.net.ConnectivityManager.TYPE_WIFI_P2P;
+import static android.net.ConnectivityManager.TYPE_WIMAX;
+import static android.net.NetworkStats.DEFAULT_NETWORK_ALL;
+import static android.net.NetworkStats.DEFAULT_NETWORK_NO;
+import static android.net.NetworkStats.DEFAULT_NETWORK_YES;
+import static android.net.NetworkStats.METERED_ALL;
+import static android.net.NetworkStats.METERED_NO;
+import static android.net.NetworkStats.METERED_YES;
+import static android.net.NetworkStats.ROAMING_ALL;
+import static android.net.NetworkStats.ROAMING_NO;
+import static android.net.NetworkStats.ROAMING_YES;
+import static android.net.wifi.WifiInfo.removeDoubleQuotes;
+
+import android.annotation.UnsupportedAppUsage;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.BackupUtils;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.ArrayUtils;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Predicate used to match {@link NetworkIdentity}, usually when collecting
+ * statistics. (It should probably have been named {@code NetworkPredicate}.)
+ *
+ * @hide
+ */
+public class NetworkTemplate implements Parcelable {
+    private static final String TAG = "NetworkTemplate";
+
+    /**
+     * Current Version of the Backup Serializer.
+     */
+    private static final int BACKUP_VERSION = 1;
+
+    public static final int MATCH_MOBILE = 1;
+    public static final int MATCH_WIFI = 4;
+    public static final int MATCH_ETHERNET = 5;
+    public static final int MATCH_MOBILE_WILDCARD = 6;
+    public static final int MATCH_WIFI_WILDCARD = 7;
+    public static final int MATCH_BLUETOOTH = 8;
+    public static final int MATCH_PROXY = 9;
+
+    private static boolean isKnownMatchRule(final int rule) {
+        switch (rule) {
+            case MATCH_MOBILE:
+            case MATCH_WIFI:
+            case MATCH_ETHERNET:
+            case MATCH_MOBILE_WILDCARD:
+            case MATCH_WIFI_WILDCARD:
+            case MATCH_BLUETOOTH:
+            case MATCH_PROXY:
+                return true;
+
+            default:
+                return false;
+        }
+    }
+
+    private static boolean sForceAllNetworkTypes = false;
+
+    /**
+     * Results in matching against all mobile network types.
+     *
+     * <p>See {@link #matchesMobile} and {@link matchesMobileWildcard}.
+     */
+    @VisibleForTesting
+    public static void forceAllNetworkTypes() {
+        sForceAllNetworkTypes = true;
+    }
+
+    /** Resets the affect of {@link #forceAllNetworkTypes}. */
+    @VisibleForTesting
+    public static void resetForceAllNetworkTypes() {
+        sForceAllNetworkTypes = false;
+    }
+
+    /**
+     * Template to match {@link ConnectivityManager#TYPE_MOBILE} networks with
+     * the given IMSI.
+     */
+    @UnsupportedAppUsage
+    public static NetworkTemplate buildTemplateMobileAll(String subscriberId) {
+        return new NetworkTemplate(MATCH_MOBILE, subscriberId, null);
+    }
+
+    /**
+     * Template to match {@link ConnectivityManager#TYPE_MOBILE} networks,
+     * regardless of IMSI.
+     */
+    @UnsupportedAppUsage
+    public static NetworkTemplate buildTemplateMobileWildcard() {
+        return new NetworkTemplate(MATCH_MOBILE_WILDCARD, null, null);
+    }
+
+    /**
+     * Template to match all {@link ConnectivityManager#TYPE_WIFI} networks,
+     * regardless of SSID.
+     */
+    @UnsupportedAppUsage
+    public static NetworkTemplate buildTemplateWifiWildcard() {
+        return new NetworkTemplate(MATCH_WIFI_WILDCARD, null, null);
+    }
+
+    @Deprecated
+    @UnsupportedAppUsage
+    public static NetworkTemplate buildTemplateWifi() {
+        return buildTemplateWifiWildcard();
+    }
+
+    /**
+     * Template to match {@link ConnectivityManager#TYPE_WIFI} networks with the
+     * given SSID.
+     */
+    public static NetworkTemplate buildTemplateWifi(String networkId) {
+        return new NetworkTemplate(MATCH_WIFI, null, networkId);
+    }
+
+    /**
+     * Template to combine all {@link ConnectivityManager#TYPE_ETHERNET} style
+     * networks together.
+     */
+    @UnsupportedAppUsage
+    public static NetworkTemplate buildTemplateEthernet() {
+        return new NetworkTemplate(MATCH_ETHERNET, null, null);
+    }
+
+    /**
+     * Template to combine all {@link ConnectivityManager#TYPE_BLUETOOTH} style
+     * networks together.
+     */
+    public static NetworkTemplate buildTemplateBluetooth() {
+        return new NetworkTemplate(MATCH_BLUETOOTH, null, null);
+    }
+
+    /**
+     * Template to combine all {@link ConnectivityManager#TYPE_PROXY} style
+     * networks together.
+     */
+    public static NetworkTemplate buildTemplateProxy() {
+        return new NetworkTemplate(MATCH_PROXY, null, null);
+    }
+
+    private final int mMatchRule;
+    private final String mSubscriberId;
+
+    /**
+     * Ugh, templates are designed to target a single subscriber, but we might
+     * need to match several "merged" subscribers. These are the subscribers
+     * that should be considered to match this template.
+     * <p>
+     * Since the merge set is dynamic, it should <em>not</em> be persisted or
+     * used for determining equality.
+     */
+    private final String[] mMatchSubscriberIds;
+
+    private final String mNetworkId;
+
+    // Matches for the NetworkStats constants METERED_*, ROAMING_* and DEFAULT_NETWORK_*.
+    private final int mMetered;
+    private final int mRoaming;
+    private final int mDefaultNetwork;
+
+    @UnsupportedAppUsage
+    public NetworkTemplate(int matchRule, String subscriberId, String networkId) {
+        this(matchRule, subscriberId, new String[] { subscriberId }, networkId);
+    }
+
+    public NetworkTemplate(int matchRule, String subscriberId, String[] matchSubscriberIds,
+            String networkId) {
+        this(matchRule, subscriberId, matchSubscriberIds, networkId, METERED_ALL, ROAMING_ALL,
+                DEFAULT_NETWORK_ALL);
+    }
+
+    public NetworkTemplate(int matchRule, String subscriberId, String[] matchSubscriberIds,
+            String networkId, int metered, int roaming, int defaultNetwork) {
+        mMatchRule = matchRule;
+        mSubscriberId = subscriberId;
+        mMatchSubscriberIds = matchSubscriberIds;
+        mNetworkId = networkId;
+        mMetered = metered;
+        mRoaming = roaming;
+        mDefaultNetwork = defaultNetwork;
+
+        if (!isKnownMatchRule(matchRule)) {
+            Log.e(TAG, "Unknown network template rule " + matchRule
+                    + " will not match any identity.");
+        }
+    }
+
+    private NetworkTemplate(Parcel in) {
+        mMatchRule = in.readInt();
+        mSubscriberId = in.readString();
+        mMatchSubscriberIds = in.createStringArray();
+        mNetworkId = in.readString();
+        mMetered = in.readInt();
+        mRoaming = in.readInt();
+        mDefaultNetwork = in.readInt();
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(mMatchRule);
+        dest.writeString(mSubscriberId);
+        dest.writeStringArray(mMatchSubscriberIds);
+        dest.writeString(mNetworkId);
+        dest.writeInt(mMetered);
+        dest.writeInt(mRoaming);
+        dest.writeInt(mDefaultNetwork);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public String toString() {
+        final StringBuilder builder = new StringBuilder("NetworkTemplate: ");
+        builder.append("matchRule=").append(getMatchRuleName(mMatchRule));
+        if (mSubscriberId != null) {
+            builder.append(", subscriberId=").append(
+                    NetworkIdentity.scrubSubscriberId(mSubscriberId));
+        }
+        if (mMatchSubscriberIds != null) {
+            builder.append(", matchSubscriberIds=").append(
+                    Arrays.toString(NetworkIdentity.scrubSubscriberId(mMatchSubscriberIds)));
+        }
+        if (mNetworkId != null) {
+            builder.append(", networkId=").append(mNetworkId);
+        }
+        if (mMetered != METERED_ALL) {
+            builder.append(", metered=").append(NetworkStats.meteredToString(mMetered));
+        }
+        if (mRoaming != ROAMING_ALL) {
+            builder.append(", roaming=").append(NetworkStats.roamingToString(mRoaming));
+        }
+        if (mDefaultNetwork != DEFAULT_NETWORK_ALL) {
+            builder.append(", defaultNetwork=").append(NetworkStats.defaultNetworkToString(
+                    mDefaultNetwork));
+        }
+        return builder.toString();
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mMatchRule, mSubscriberId, mNetworkId, mMetered, mRoaming,
+                mDefaultNetwork);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj instanceof NetworkTemplate) {
+            final NetworkTemplate other = (NetworkTemplate) obj;
+            return mMatchRule == other.mMatchRule
+                    && Objects.equals(mSubscriberId, other.mSubscriberId)
+                    && Objects.equals(mNetworkId, other.mNetworkId)
+                    && mMetered == other.mMetered
+                    && mRoaming == other.mRoaming
+                    && mDefaultNetwork == other.mDefaultNetwork;
+        }
+        return false;
+    }
+
+    public boolean isMatchRuleMobile() {
+        switch (mMatchRule) {
+            case MATCH_MOBILE:
+            case MATCH_MOBILE_WILDCARD:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    public boolean isPersistable() {
+        switch (mMatchRule) {
+            case MATCH_MOBILE_WILDCARD:
+            case MATCH_WIFI_WILDCARD:
+                return false;
+            default:
+                return true;
+        }
+    }
+
+    @UnsupportedAppUsage
+    public int getMatchRule() {
+        return mMatchRule;
+    }
+
+    @UnsupportedAppUsage
+    public String getSubscriberId() {
+        return mSubscriberId;
+    }
+
+    public String getNetworkId() {
+        return mNetworkId;
+    }
+
+    /**
+     * Test if given {@link NetworkIdentity} matches this template.
+     */
+    public boolean matches(NetworkIdentity ident) {
+        if (!matchesMetered(ident)) return false;
+        if (!matchesRoaming(ident)) return false;
+        if (!matchesDefaultNetwork(ident)) return false;
+
+        switch (mMatchRule) {
+            case MATCH_MOBILE:
+                return matchesMobile(ident);
+            case MATCH_WIFI:
+                return matchesWifi(ident);
+            case MATCH_ETHERNET:
+                return matchesEthernet(ident);
+            case MATCH_MOBILE_WILDCARD:
+                return matchesMobileWildcard(ident);
+            case MATCH_WIFI_WILDCARD:
+                return matchesWifiWildcard(ident);
+            case MATCH_BLUETOOTH:
+                return matchesBluetooth(ident);
+            case MATCH_PROXY:
+                return matchesProxy(ident);
+            default:
+                // We have no idea what kind of network template we are, so we
+                // just claim not to match anything.
+                return false;
+        }
+    }
+
+    private boolean matchesMetered(NetworkIdentity ident) {
+        return (mMetered == METERED_ALL)
+            || (mMetered == METERED_YES && ident.mMetered)
+            || (mMetered == METERED_NO && !ident.mMetered);
+    }
+
+    private boolean matchesRoaming(NetworkIdentity ident) {
+        return (mRoaming == ROAMING_ALL)
+            || (mRoaming == ROAMING_YES && ident.mRoaming)
+            || (mRoaming == ROAMING_NO && !ident.mRoaming);
+    }
+
+    private boolean matchesDefaultNetwork(NetworkIdentity ident) {
+        return (mDefaultNetwork == DEFAULT_NETWORK_ALL)
+            || (mDefaultNetwork == DEFAULT_NETWORK_YES && ident.mDefaultNetwork)
+            || (mDefaultNetwork == DEFAULT_NETWORK_NO && !ident.mDefaultNetwork);
+    }
+
+    public boolean matchesSubscriberId(String subscriberId) {
+        return ArrayUtils.contains(mMatchSubscriberIds, subscriberId);
+    }
+
+    /**
+     * Check if mobile network with matching IMSI.
+     */
+    private boolean matchesMobile(NetworkIdentity ident) {
+        if (ident.mType == TYPE_WIMAX) {
+            // TODO: consider matching against WiMAX subscriber identity
+            return true;
+        } else {
+            return (sForceAllNetworkTypes || (ident.mType == TYPE_MOBILE && ident.mMetered))
+                    && !ArrayUtils.isEmpty(mMatchSubscriberIds)
+                    && ArrayUtils.contains(mMatchSubscriberIds, ident.mSubscriberId);
+        }
+    }
+
+    /**
+     * Check if matches Wi-Fi network template.
+     */
+    private boolean matchesWifi(NetworkIdentity ident) {
+        switch (ident.mType) {
+            case TYPE_WIFI:
+                return Objects.equals(
+                        removeDoubleQuotes(mNetworkId), removeDoubleQuotes(ident.mNetworkId));
+            default:
+                return false;
+        }
+    }
+
+    /**
+     * Check if matches Ethernet network template.
+     */
+    private boolean matchesEthernet(NetworkIdentity ident) {
+        if (ident.mType == TYPE_ETHERNET) {
+            return true;
+        }
+        return false;
+    }
+
+    private boolean matchesMobileWildcard(NetworkIdentity ident) {
+        if (ident.mType == TYPE_WIMAX) {
+            return true;
+        } else {
+            return sForceAllNetworkTypes || (ident.mType == TYPE_MOBILE && ident.mMetered);
+        }
+    }
+
+    private boolean matchesWifiWildcard(NetworkIdentity ident) {
+        switch (ident.mType) {
+            case TYPE_WIFI:
+            case TYPE_WIFI_P2P:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    /**
+     * Check if matches Bluetooth network template.
+     */
+    private boolean matchesBluetooth(NetworkIdentity ident) {
+        if (ident.mType == TYPE_BLUETOOTH) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Check if matches Proxy network template.
+     */
+    private boolean matchesProxy(NetworkIdentity ident) {
+        return ident.mType == TYPE_PROXY;
+    }
+
+    private static String getMatchRuleName(int matchRule) {
+        switch (matchRule) {
+            case MATCH_MOBILE:
+                return "MOBILE";
+            case MATCH_WIFI:
+                return "WIFI";
+            case MATCH_ETHERNET:
+                return "ETHERNET";
+            case MATCH_MOBILE_WILDCARD:
+                return "MOBILE_WILDCARD";
+            case MATCH_WIFI_WILDCARD:
+                return "WIFI_WILDCARD";
+            case MATCH_BLUETOOTH:
+                return "BLUETOOTH";
+            case MATCH_PROXY:
+                return "PROXY";
+            default:
+                return "UNKNOWN(" + matchRule + ")";
+        }
+    }
+
+    /**
+     * Examine the given template and normalize if it refers to a "merged"
+     * mobile subscriber. We pick the "lowest" merged subscriber as the primary
+     * for key purposes, and expand the template to match all other merged
+     * subscribers.
+     * <p>
+     * For example, given an incoming template matching B, and the currently
+     * active merge set [A,B], we'd return a new template that primarily matches
+     * A, but also matches B.
+     */
+    @UnsupportedAppUsage
+    public static NetworkTemplate normalize(NetworkTemplate template, String[] merged) {
+        if (template.isMatchRuleMobile() && ArrayUtils.contains(merged, template.mSubscriberId)) {
+            // Requested template subscriber is part of the merge group; return
+            // a template that matches all merged subscribers.
+            return new NetworkTemplate(template.mMatchRule, merged[0], merged,
+                    template.mNetworkId);
+        } else {
+            return template;
+        }
+    }
+
+    @UnsupportedAppUsage
+    public static final @android.annotation.NonNull Creator<NetworkTemplate> CREATOR = new Creator<NetworkTemplate>() {
+        @Override
+        public NetworkTemplate createFromParcel(Parcel in) {
+            return new NetworkTemplate(in);
+        }
+
+        @Override
+        public NetworkTemplate[] newArray(int size) {
+            return new NetworkTemplate[size];
+        }
+    };
+
+    public byte[] getBytesForBackup() throws IOException {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        DataOutputStream out = new DataOutputStream(baos);
+
+        out.writeInt(BACKUP_VERSION);
+
+        out.writeInt(mMatchRule);
+        BackupUtils.writeString(out, mSubscriberId);
+        BackupUtils.writeString(out, mNetworkId);
+
+        return baos.toByteArray();
+    }
+
+    public static NetworkTemplate getNetworkTemplateFromBackup(DataInputStream in)
+            throws IOException, BackupUtils.BadVersionException {
+        int version = in.readInt();
+        if (version < 1 || version > BACKUP_VERSION) {
+            throw new BackupUtils.BadVersionException("Unknown Backup Serialization Version");
+        }
+
+        int matchRule = in.readInt();
+        String subscriberId = BackupUtils.readString(in);
+        String networkId = BackupUtils.readString(in);
+
+        if (!isKnownMatchRule(matchRule)) {
+            throw new BackupUtils.BadVersionException(
+                    "Restored network template contains unknown match rule " + matchRule);
+        }
+
+        return new NetworkTemplate(matchRule, subscriberId, networkId);
+    }
+}
diff --git a/android/net/NetworkUtils.java b/android/net/NetworkUtils.java
new file mode 100644
index 0000000..d0f54b4
--- /dev/null
+++ b/android/net/NetworkUtils.java
@@ -0,0 +1,488 @@
+/*
+ * Copyright (C) 2008 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 static android.system.OsConstants.AF_INET;
+import static android.system.OsConstants.AF_INET6;
+
+import android.annotation.NonNull;
+import android.annotation.UnsupportedAppUsage;
+import android.net.shared.Inet4AddressUtils;
+import android.os.Build;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+import android.util.Pair;
+
+import java.io.FileDescriptor;
+import java.math.BigInteger;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.Collection;
+import java.util.Locale;
+import java.util.TreeSet;
+
+/**
+ * Native methods for managing network interfaces.
+ *
+ * {@hide}
+ */
+public class NetworkUtils {
+
+    private static final String TAG = "NetworkUtils";
+
+    /**
+     * Attaches a socket filter that drops all of incoming packets.
+     * @param fd the socket's {@link FileDescriptor}.
+     */
+    public static native void attachDropAllBPFFilter(FileDescriptor fd) throws SocketException;
+
+    /**
+     * Detaches a socket filter.
+     * @param fd the socket's {@link FileDescriptor}.
+     */
+    public static native void detachBPFFilter(FileDescriptor fd) throws SocketException;
+
+    /**
+     * Configures a socket for receiving ICMPv6 router solicitations and sending advertisements.
+     * @param fd the socket's {@link FileDescriptor}.
+     * @param ifIndex the interface index.
+     */
+    public native static void setupRaSocket(FileDescriptor fd, int ifIndex) throws SocketException;
+
+    /**
+     * Binds the current process to the network designated by {@code netId}.  All sockets created
+     * in the future (and not explicitly bound via a bound {@link SocketFactory} (see
+     * {@link Network#getSocketFactory}) will be bound to this network.  Note that if this
+     * {@code Network} ever disconnects all sockets created in this way will cease to work.  This
+     * is by design so an application doesn't accidentally use sockets it thinks are still bound to
+     * a particular {@code Network}.  Passing NETID_UNSET clears the binding.
+     */
+    public native static boolean bindProcessToNetwork(int netId);
+
+    /**
+     * Return the netId last passed to {@link #bindProcessToNetwork}, or NETID_UNSET if
+     * {@link #unbindProcessToNetwork} has been called since {@link #bindProcessToNetwork}.
+     */
+    public native static int getBoundNetworkForProcess();
+
+    /**
+     * Binds host resolutions performed by this process to the network designated by {@code netId}.
+     * {@link #bindProcessToNetwork} takes precedence over this setting.  Passing NETID_UNSET clears
+     * the binding.
+     *
+     * @deprecated This is strictly for legacy usage to support startUsingNetworkFeature().
+     */
+    @Deprecated
+    public native static boolean bindProcessToNetworkForHostResolution(int netId);
+
+    /**
+     * Explicitly binds {@code socketfd} to the network designated by {@code netId}.  This
+     * overrides any binding via {@link #bindProcessToNetwork}.
+     * @return 0 on success or negative errno on failure.
+     */
+    public native static int bindSocketToNetwork(int socketfd, int netId);
+
+    /**
+     * Protect {@code fd} from VPN connections.  After protecting, data sent through
+     * this socket will go directly to the underlying network, so its traffic will not be
+     * forwarded through the VPN.
+     */
+    @UnsupportedAppUsage
+    public static boolean protectFromVpn(FileDescriptor fd) {
+        return protectFromVpn(fd.getInt$());
+    }
+
+    /**
+     * Protect {@code socketfd} from VPN connections.  After protecting, data sent through
+     * this socket will go directly to the underlying network, so its traffic will not be
+     * forwarded through the VPN.
+     */
+    public native static boolean protectFromVpn(int socketfd);
+
+    /**
+     * Determine if {@code uid} can access network designated by {@code netId}.
+     * @return {@code true} if {@code uid} can access network, {@code false} otherwise.
+     */
+    public native static boolean queryUserAccess(int uid, int netId);
+
+    /**
+     * DNS resolver series jni method.
+     * Issue the query {@code msg} on the network designated by {@code netId}.
+     * {@code flags} is an additional config to control actual querying behavior.
+     * @return a file descriptor to watch for read events
+     */
+    public static native FileDescriptor resNetworkSend(
+            int netId, byte[] msg, int msglen, int flags) throws ErrnoException;
+
+    /**
+     * DNS resolver series jni method.
+     * Look up the {@code nsClass} {@code nsType} Resource Record (RR) associated
+     * with Domain Name {@code dname} on the network designated by {@code netId}.
+     * {@code flags} is an additional config to control actual querying behavior.
+     * @return a file descriptor to watch for read events
+     */
+    public static native FileDescriptor resNetworkQuery(
+            int netId, String dname, int nsClass, int nsType, int flags) throws ErrnoException;
+
+    /**
+     * DNS resolver series jni method.
+     * Read a result for the query associated with the {@code fd}.
+     * @return DnsResponse containing blob answer and rcode
+     */
+    public static native DnsResolver.DnsResponse resNetworkResult(FileDescriptor fd)
+            throws ErrnoException;
+
+    /**
+     * DNS resolver series jni method.
+     * Attempts to cancel the in-progress query associated with the {@code fd}.
+     */
+    public static native void resNetworkCancel(FileDescriptor fd);
+
+    /**
+     * DNS resolver series jni method.
+     * Attempts to get network which resolver will use if no network is explicitly selected.
+     */
+    public static native Network getDnsNetwork() throws ErrnoException;
+
+    /**
+     * Get the tcp repair window associated with the {@code fd}.
+     *
+     * @param fd the tcp socket's {@link FileDescriptor}.
+     * @return a {@link TcpRepairWindow} object indicates tcp window size.
+     */
+    public static native TcpRepairWindow getTcpRepairWindow(FileDescriptor fd)
+            throws ErrnoException;
+
+    /**
+     * @see Inet4AddressUtils#intToInet4AddressHTL(int)
+     * @deprecated Use either {@link Inet4AddressUtils#intToInet4AddressHTH(int)}
+     *             or {@link Inet4AddressUtils#intToInet4AddressHTL(int)}
+     */
+    @Deprecated
+    @UnsupportedAppUsage
+    public static InetAddress intToInetAddress(int hostAddress) {
+        return Inet4AddressUtils.intToInet4AddressHTL(hostAddress);
+    }
+
+    /**
+     * @see Inet4AddressUtils#inet4AddressToIntHTL(Inet4Address)
+     * @deprecated Use either {@link Inet4AddressUtils#inet4AddressToIntHTH(Inet4Address)}
+     *             or {@link Inet4AddressUtils#inet4AddressToIntHTL(Inet4Address)}
+     */
+    @Deprecated
+    public static int inetAddressToInt(Inet4Address inetAddr)
+            throws IllegalArgumentException {
+        return Inet4AddressUtils.inet4AddressToIntHTL(inetAddr);
+    }
+
+    /**
+     * @see Inet4AddressUtils#prefixLengthToV4NetmaskIntHTL(int)
+     * @deprecated Use either {@link Inet4AddressUtils#prefixLengthToV4NetmaskIntHTH(int)}
+     *             or {@link Inet4AddressUtils#prefixLengthToV4NetmaskIntHTL(int)}
+     */
+    @Deprecated
+    @UnsupportedAppUsage
+    public static int prefixLengthToNetmaskInt(int prefixLength)
+            throws IllegalArgumentException {
+        return Inet4AddressUtils.prefixLengthToV4NetmaskIntHTL(prefixLength);
+    }
+
+    /**
+     * Convert a IPv4 netmask integer to a prefix length
+     * @param netmask as an integer (0xff000000 for a /8 subnet)
+     * @return the network prefix length
+     */
+    public static int netmaskIntToPrefixLength(int netmask) {
+        return Integer.bitCount(netmask);
+    }
+
+    /**
+     * Convert an IPv4 netmask to a prefix length, checking that the netmask is contiguous.
+     * @param netmask as a {@code Inet4Address}.
+     * @return the network prefix length
+     * @throws IllegalArgumentException the specified netmask was not contiguous.
+     * @hide
+     * @deprecated use {@link Inet4AddressUtils#netmaskToPrefixLength(Inet4Address)}
+     */
+    @UnsupportedAppUsage
+    @Deprecated
+    public static int netmaskToPrefixLength(Inet4Address netmask) {
+        // This is only here because some apps seem to be using it (@UnsupportedAppUsage).
+        return Inet4AddressUtils.netmaskToPrefixLength(netmask);
+    }
+
+
+    /**
+     * Create an InetAddress from a string where the string must be a standard
+     * representation of a V4 or V6 address.  Avoids doing a DNS lookup on failure
+     * but it will throw an IllegalArgumentException in that case.
+     * @param addrString
+     * @return the InetAddress
+     * @hide
+     * @deprecated Use {@link InetAddresses#parseNumericAddress(String)}, if possible.
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P)
+    @Deprecated
+    public static InetAddress numericToInetAddress(String addrString)
+            throws IllegalArgumentException {
+        return InetAddress.parseNumericAddress(addrString);
+    }
+
+    /**
+     *  Masks a raw IP address byte array with the specified prefix length.
+     */
+    public static void maskRawAddress(byte[] array, int prefixLength) {
+        if (prefixLength < 0 || prefixLength > array.length * 8) {
+            throw new RuntimeException("IP address with " + array.length +
+                    " bytes has invalid prefix length " + prefixLength);
+        }
+
+        int offset = prefixLength / 8;
+        int remainder = prefixLength % 8;
+        byte mask = (byte)(0xFF << (8 - remainder));
+
+        if (offset < array.length) array[offset] = (byte)(array[offset] & mask);
+
+        offset++;
+
+        for (; offset < array.length; offset++) {
+            array[offset] = 0;
+        }
+    }
+
+    /**
+     * Get InetAddress masked with prefixLength.  Will never return null.
+     * @param address the IP address to mask with
+     * @param prefixLength the prefixLength used to mask the IP
+     */
+    public static InetAddress getNetworkPart(InetAddress address, int prefixLength) {
+        byte[] array = address.getAddress();
+        maskRawAddress(array, prefixLength);
+
+        InetAddress netPart = null;
+        try {
+            netPart = InetAddress.getByAddress(array);
+        } catch (UnknownHostException e) {
+            throw new RuntimeException("getNetworkPart error - " + e.toString());
+        }
+        return netPart;
+    }
+
+    /**
+     * Returns the implicit netmask of an IPv4 address, as was the custom before 1993.
+     */
+    @UnsupportedAppUsage
+    public static int getImplicitNetmask(Inet4Address address) {
+        // Only here because it seems to be used by apps
+        return Inet4AddressUtils.getImplicitNetmask(address);
+    }
+
+    /**
+     * Utility method to parse strings such as "192.0.2.5/24" or "2001:db8::cafe:d00d/64".
+     * @hide
+     */
+    public static Pair<InetAddress, Integer> parseIpAndMask(String ipAndMaskString) {
+        InetAddress address = null;
+        int prefixLength = -1;
+        try {
+            String[] pieces = ipAndMaskString.split("/", 2);
+            prefixLength = Integer.parseInt(pieces[1]);
+            address = InetAddress.parseNumericAddress(pieces[0]);
+        } catch (NullPointerException e) {            // Null string.
+        } catch (ArrayIndexOutOfBoundsException e) {  // No prefix length.
+        } catch (NumberFormatException e) {           // Non-numeric prefix.
+        } catch (IllegalArgumentException e) {        // Invalid IP address.
+        }
+
+        if (address == null || prefixLength == -1) {
+            throw new IllegalArgumentException("Invalid IP address and mask " + ipAndMaskString);
+        }
+
+        return new Pair<InetAddress, Integer>(address, prefixLength);
+    }
+
+    /**
+     * Check if IP address type is consistent between two InetAddress.
+     * @return true if both are the same type.  False otherwise.
+     */
+    public static boolean addressTypeMatches(InetAddress left, InetAddress right) {
+        return (((left instanceof Inet4Address) && (right instanceof Inet4Address)) ||
+                ((left instanceof Inet6Address) && (right instanceof Inet6Address)));
+    }
+
+    /**
+     * Convert a 32 char hex string into a Inet6Address.
+     * throws a runtime exception if the string isn't 32 chars, isn't hex or can't be
+     * made into an Inet6Address
+     * @param addrHexString a 32 character hex string representing an IPv6 addr
+     * @return addr an InetAddress representation for the string
+     */
+    public static InetAddress hexToInet6Address(String addrHexString)
+            throws IllegalArgumentException {
+        try {
+            return numericToInetAddress(String.format(Locale.US, "%s:%s:%s:%s:%s:%s:%s:%s",
+                    addrHexString.substring(0,4),   addrHexString.substring(4,8),
+                    addrHexString.substring(8,12),  addrHexString.substring(12,16),
+                    addrHexString.substring(16,20), addrHexString.substring(20,24),
+                    addrHexString.substring(24,28), addrHexString.substring(28,32)));
+        } catch (Exception e) {
+            Log.e("NetworkUtils", "error in hexToInet6Address(" + addrHexString + "): " + e);
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+    /**
+     * Create a string array of host addresses from a collection of InetAddresses
+     * @param addrs a Collection of InetAddresses
+     * @return an array of Strings containing their host addresses
+     */
+    public static String[] makeStrings(Collection<InetAddress> addrs) {
+        String[] result = new String[addrs.size()];
+        int i = 0;
+        for (InetAddress addr : addrs) {
+            result[i++] = addr.getHostAddress();
+        }
+        return result;
+    }
+
+    /**
+     * Trim leading zeros from IPv4 address strings
+     * Our base libraries will interpret that as octel..
+     * Must leave non v4 addresses and host names alone.
+     * For example, 192.168.000.010 -> 192.168.0.10
+     * TODO - fix base libraries and remove this function
+     * @param addr a string representing an ip addr
+     * @return a string propertly trimmed
+     */
+    @UnsupportedAppUsage
+    public static String trimV4AddrZeros(String addr) {
+        if (addr == null) return null;
+        String[] octets = addr.split("\\.");
+        if (octets.length != 4) return addr;
+        StringBuilder builder = new StringBuilder(16);
+        String result = null;
+        for (int i = 0; i < 4; i++) {
+            try {
+                if (octets[i].length() > 3) return addr;
+                builder.append(Integer.parseInt(octets[i]));
+            } catch (NumberFormatException e) {
+                return addr;
+            }
+            if (i < 3) builder.append('.');
+        }
+        result = builder.toString();
+        return result;
+    }
+
+    /**
+     * Returns a prefix set without overlaps.
+     *
+     * This expects the src set to be sorted from shorter to longer. Results are undefined
+     * failing this condition. The returned prefix set is sorted in the same order as the
+     * passed set, with the same comparator.
+     */
+    private static TreeSet<IpPrefix> deduplicatePrefixSet(final TreeSet<IpPrefix> src) {
+        final TreeSet<IpPrefix> dst = new TreeSet<>(src.comparator());
+        // Prefixes match addresses that share their upper part up to their length, therefore
+        // the only kind of possible overlap in two prefixes is strict inclusion of the longer
+        // (more restrictive) in the shorter (including equivalence if they have the same
+        // length).
+        // Because prefixes in the src set are sorted from shorter to longer, deduplicating
+        // is done by simply iterating in order, and not adding any longer prefix that is
+        // already covered by a shorter one.
+        newPrefixes:
+        for (IpPrefix newPrefix : src) {
+            for (IpPrefix existingPrefix : dst) {
+                if (existingPrefix.containsPrefix(newPrefix)) {
+                    continue newPrefixes;
+                }
+            }
+            dst.add(newPrefix);
+        }
+        return dst;
+    }
+
+    /**
+     * Returns how many IPv4 addresses match any of the prefixes in the passed ordered set.
+     *
+     * Obviously this returns an integral value between 0 and 2**32.
+     * The behavior is undefined if any of the prefixes is not an IPv4 prefix or if the
+     * set is not ordered smallest prefix to longer prefix.
+     *
+     * @param prefixes the set of prefixes, ordered by length
+     */
+    public static long routedIPv4AddressCount(final TreeSet<IpPrefix> prefixes) {
+        long routedIPCount = 0;
+        for (final IpPrefix prefix : deduplicatePrefixSet(prefixes)) {
+            if (!prefix.isIPv4()) {
+                Log.wtf(TAG, "Non-IPv4 prefix in routedIPv4AddressCount");
+            }
+            int rank = 32 - prefix.getPrefixLength();
+            routedIPCount += 1L << rank;
+        }
+        return routedIPCount;
+    }
+
+    /**
+     * Returns how many IPv6 addresses match any of the prefixes in the passed ordered set.
+     *
+     * This returns a BigInteger between 0 and 2**128.
+     * The behavior is undefined if any of the prefixes is not an IPv6 prefix or if the
+     * set is not ordered smallest prefix to longer prefix.
+     */
+    public static BigInteger routedIPv6AddressCount(final TreeSet<IpPrefix> prefixes) {
+        BigInteger routedIPCount = BigInteger.ZERO;
+        for (final IpPrefix prefix : deduplicatePrefixSet(prefixes)) {
+            if (!prefix.isIPv6()) {
+                Log.wtf(TAG, "Non-IPv6 prefix in routedIPv6AddressCount");
+            }
+            int rank = 128 - prefix.getPrefixLength();
+            routedIPCount = routedIPCount.add(BigInteger.ONE.shiftLeft(rank));
+        }
+        return routedIPCount;
+    }
+
+    private static final int[] ADDRESS_FAMILIES = new int[] {AF_INET, AF_INET6};
+
+    /**
+     * Returns true if the hostname is weakly validated.
+     * @param hostname Name of host to validate.
+     * @return True if it's a valid-ish hostname.
+     *
+     * @hide
+     */
+    public static boolean isWeaklyValidatedHostname(@NonNull String hostname) {
+        // TODO(b/34953048): Use a validation method that permits more accurate,
+        // but still inexpensive, checking of likely valid DNS hostnames.
+        final String weakHostnameRegex = "^[a-zA-Z0-9_.-]+$";
+        if (!hostname.matches(weakHostnameRegex)) {
+            return false;
+        }
+
+        for (int address_family : ADDRESS_FAMILIES) {
+            if (Os.inet_pton(address_family, hostname) != null) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+}
diff --git a/android/net/NetworkWatchlistManager.java b/android/net/NetworkWatchlistManager.java
new file mode 100644
index 0000000..49047d3
--- /dev/null
+++ b/android/net/NetworkWatchlistManager.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.annotation.SystemService;
+import android.content.Context;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.Log;
+
+import com.android.internal.net.INetworkWatchlistManager;
+import com.android.internal.util.Preconditions;
+
+/**
+ * Class that manage network watchlist in system.
+ * @hide
+ */
+@SystemService(Context.NETWORK_WATCHLIST_SERVICE)
+public class NetworkWatchlistManager {
+
+    private static final String TAG = "NetworkWatchlistManager";
+    private static final String SHARED_MEMORY_TAG = "NETWORK_WATCHLIST_SHARED_MEMORY";
+
+    private final Context mContext;
+    private final INetworkWatchlistManager mNetworkWatchlistManager;
+
+    /**
+     * @hide
+     */
+    public NetworkWatchlistManager(Context context, INetworkWatchlistManager manager) {
+        mContext = context;
+        mNetworkWatchlistManager = manager;
+    }
+
+    /**
+     * @hide
+     */
+    public NetworkWatchlistManager(Context context) {
+        mContext = Preconditions.checkNotNull(context, "missing context");
+        mNetworkWatchlistManager = (INetworkWatchlistManager)
+                INetworkWatchlistManager.Stub.asInterface(
+                        ServiceManager.getService(Context.NETWORK_WATCHLIST_SERVICE));
+    }
+
+    /**
+     * Report network watchlist records if necessary.
+     *
+     * Watchlist report process will summarize records into a single report, then the
+     * report will be processed by differential privacy framework and stored on disk.
+     *
+     * @hide
+     */
+    public void reportWatchlistIfNecessary() {
+        try {
+            mNetworkWatchlistManager.reportWatchlistIfNecessary();
+        } catch (RemoteException e) {
+            Log.e(TAG, "Cannot report records", e);
+            e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Reload network watchlist.
+     *
+     * @hide
+     */
+    public void reloadWatchlist() {
+        try {
+            mNetworkWatchlistManager.reloadWatchlist();
+        } catch (RemoteException e) {
+            Log.e(TAG, "Unable to reload watchlist");
+            e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Get Network Watchlist config file hash.
+     */
+    public byte[] getWatchlistConfigHash() {
+        try {
+            return mNetworkWatchlistManager.getWatchlistConfigHash();
+        } catch (RemoteException e) {
+            Log.e(TAG, "Unable to get watchlist config hash");
+            throw e.rethrowFromSystemServer();
+        }
+    }
+}
diff --git a/android/net/PacProxySelector.java b/android/net/PacProxySelector.java
new file mode 100644
index 0000000..85bf79a
--- /dev/null
+++ b/android/net/PacProxySelector.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2013 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.os.ServiceManager;
+import android.util.Log;
+
+import com.android.net.IProxyService;
+import com.google.android.collect.Lists;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.MalformedURLException;
+import java.net.Proxy;
+import java.net.Proxy.Type;
+import java.net.ProxySelector;
+import java.net.SocketAddress;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.List;
+
+/**
+ * @hide
+ */
+public class PacProxySelector extends ProxySelector {
+    private static final String TAG = "PacProxySelector";
+    public static final String PROXY_SERVICE = "com.android.net.IProxyService";
+    private static final String SOCKS = "SOCKS ";
+    private static final String PROXY = "PROXY ";
+
+    private IProxyService mProxyService;
+    private final List<Proxy> mDefaultList;
+
+    public PacProxySelector() {
+        mProxyService = IProxyService.Stub.asInterface(
+                ServiceManager.getService(PROXY_SERVICE));
+        if (mProxyService == null) {
+            // Added because of b10267814 where mako is restarting.
+            Log.e(TAG, "PacManager: no proxy service");
+        }
+        mDefaultList = Lists.newArrayList(java.net.Proxy.NO_PROXY);
+    }
+
+    @Override
+    public List<Proxy> select(URI uri) {
+        if (mProxyService == null) {
+            mProxyService = IProxyService.Stub.asInterface(
+                    ServiceManager.getService(PROXY_SERVICE));
+        }
+        if (mProxyService == null) {
+            Log.e(TAG, "select: no proxy service return NO_PROXY");
+            return Lists.newArrayList(java.net.Proxy.NO_PROXY);
+        }
+        String response = null;
+        String urlString;
+        try {
+            // Strip path and username/password from URI so it's not visible to PAC script. The
+            // path often contains credentials the app does not want exposed to a potentially
+            // malicious PAC script.
+            if (!"http".equalsIgnoreCase(uri.getScheme())) {
+                uri = new URI(uri.getScheme(), null, uri.getHost(), uri.getPort(), "/", null, null);
+            }
+            urlString = uri.toURL().toString();
+        } catch (URISyntaxException e) {
+            urlString = uri.getHost();
+        } catch (MalformedURLException e) {
+            urlString = uri.getHost();
+        }
+        try {
+            response = mProxyService.resolvePacFile(uri.getHost(), urlString);
+        } catch (Exception e) {
+            Log.e(TAG, "Error resolving PAC File", e);
+        }
+        if (response == null) {
+            return mDefaultList;
+        }
+
+        return parseResponse(response);
+    }
+
+    private static List<Proxy> parseResponse(String response) {
+        String[] split = response.split(";");
+        List<Proxy> ret = Lists.newArrayList();
+        for (String s : split) {
+            String trimmed = s.trim();
+            if (trimmed.equals("DIRECT")) {
+                ret.add(java.net.Proxy.NO_PROXY);
+            } else if (trimmed.startsWith(PROXY)) {
+                Proxy proxy = proxyFromHostPort(Type.HTTP, trimmed.substring(PROXY.length()));
+                if (proxy != null) {
+                    ret.add(proxy);
+                }
+            } else if (trimmed.startsWith(SOCKS)) {
+                Proxy proxy = proxyFromHostPort(Type.SOCKS, trimmed.substring(SOCKS.length()));
+                if (proxy != null) {
+                    ret.add(proxy);
+                }
+            }
+        }
+        if (ret.size() == 0) {
+            ret.add(java.net.Proxy.NO_PROXY);
+        }
+        return ret;
+    }
+
+    private static Proxy proxyFromHostPort(Proxy.Type type, String hostPortString) {
+        try {
+            String[] hostPort = hostPortString.split(":");
+            String host = hostPort[0];
+            int port = Integer.parseInt(hostPort[1]);
+            return new Proxy(type, InetSocketAddress.createUnresolved(host, port));
+        } catch (NumberFormatException|ArrayIndexOutOfBoundsException e) {
+            Log.d(TAG, "Unable to parse proxy " + hostPortString + " " + e);
+            return null;
+        }
+    }
+
+    @Override
+    public void connectFailed(URI uri, SocketAddress address, IOException failure) {
+
+    }
+
+}
diff --git a/android/net/ParseException.java b/android/net/ParseException.java
new file mode 100644
index 0000000..bcfdd7e
--- /dev/null
+++ b/android/net/ParseException.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2006 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.NonNull;
+
+/**
+ * Thrown when parsing failed.
+ */
+// See non-public class {@link WebAddress}.
+public class ParseException extends RuntimeException {
+    public String response;
+
+    ParseException(@NonNull String response) {
+        super(response);
+        this.response = response;
+    }
+
+    ParseException(@NonNull String response, @NonNull Throwable cause) {
+        super(response, cause);
+        this.response = response;
+    }
+}
diff --git a/android/net/PrivateDnsConnectivityChecker.java b/android/net/PrivateDnsConnectivityChecker.java
new file mode 100644
index 0000000..cfd458c
--- /dev/null
+++ b/android/net/PrivateDnsConnectivityChecker.java
@@ -0,0 +1,64 @@
+/*
+ * Copyright (C) 2018 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.NonNull;
+import android.util.Log;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+
+import javax.net.SocketFactory;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+
+/**
+ * Class for testing connectivity to DNS-over-TLS servers.
+ * {@hide}
+ */
+public class PrivateDnsConnectivityChecker {
+    private static final String TAG = "NetworkUtils";
+
+    private static final int PRIVATE_DNS_PORT = 853;
+    private static final int CONNECTION_TIMEOUT_MS = 5000;
+
+    private PrivateDnsConnectivityChecker() { }
+
+    /**
+     * checks that a provided host can perform a TLS handshake on port 853.
+     * @param hostname host to connect to.
+     */
+    public static boolean canConnectToPrivateDnsServer(@NonNull String hostname) {
+        final SocketFactory factory = SSLSocketFactory.getDefault();
+        TrafficStats.setThreadStatsTag(TrafficStats.TAG_SYSTEM_APP);
+
+        try (SSLSocket socket = (SSLSocket) factory.createSocket()) {
+            socket.setSoTimeout(CONNECTION_TIMEOUT_MS);
+            socket.connect(new InetSocketAddress(hostname, PRIVATE_DNS_PORT));
+            if (!socket.isConnected()) {
+                Log.w(TAG, String.format("Connection to %s failed.", hostname));
+                return false;
+            }
+            socket.startHandshake();
+            Log.w(TAG, String.format("TLS handshake to %s succeeded.", hostname));
+            return true;
+        } catch (IOException e) {
+            Log.w(TAG, String.format("TLS handshake to %s failed.", hostname), e);
+            return false;
+        }
+    }
+}
diff --git a/android/net/Proxy.java b/android/net/Proxy.java
new file mode 100644
index 0000000..4600942
--- /dev/null
+++ b/android/net/Proxy.java
@@ -0,0 +1,291 @@
+/*
+ * Copyright (C) 2007 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.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.net.InetSocketAddress;
+import java.net.ProxySelector;
+import java.net.URI;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A convenience class for accessing the user and default proxy
+ * settings.
+ */
+public final class Proxy {
+
+    private static final String TAG = "Proxy";
+
+    private static final ProxySelector sDefaultProxySelector;
+
+    /**
+     * Used to notify an app that's caching the proxy that either the default
+     * connection has changed or any connection's proxy has changed. The new
+     * proxy should be queried using {@link ConnectivityManager#getDefaultProxy()}.
+     *
+     * <p class="note">This is a protected intent that can only be sent by the system
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String PROXY_CHANGE_ACTION = "android.intent.action.PROXY_CHANGE";
+    /**
+     * Intent extra included with {@link #PROXY_CHANGE_ACTION} intents.
+     * It describes the new proxy being used (as a {@link ProxyInfo} object).
+     * @deprecated Because {@code PROXY_CHANGE_ACTION} is sent whenever the proxy
+     * for any network on the system changes, applications should always use
+     * {@link ConnectivityManager#getDefaultProxy()} or
+     * {@link ConnectivityManager#getLinkProperties(Network)}.{@link LinkProperties#getHttpProxy()}
+     * to get the proxy for the Network(s) they are using.
+     */
+    @Deprecated
+    public static final String EXTRA_PROXY_INFO = "android.intent.extra.PROXY_INFO";
+
+    /** @hide */
+    public static final int PROXY_VALID             = 0;
+    /** @hide */
+    public static final int PROXY_HOSTNAME_EMPTY    = 1;
+    /** @hide */
+    public static final int PROXY_HOSTNAME_INVALID  = 2;
+    /** @hide */
+    public static final int PROXY_PORT_EMPTY        = 3;
+    /** @hide */
+    public static final int PROXY_PORT_INVALID      = 4;
+    /** @hide */
+    public static final int PROXY_EXCLLIST_INVALID  = 5;
+
+    private static ConnectivityManager sConnectivityManager = null;
+
+    // Hostname / IP REGEX validation
+    // Matches blank input, ips, and domain names
+    private static final String NAME_IP_REGEX =
+        "[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*(\\.[a-zA-Z0-9]+(\\-[a-zA-Z0-9]+)*)*";
+
+    private static final String HOSTNAME_REGEXP = "^$|^" + NAME_IP_REGEX + "$";
+
+    private static final Pattern HOSTNAME_PATTERN;
+
+    private static final String EXCL_REGEX =
+        "[a-zA-Z0-9*]+(\\-[a-zA-Z0-9*]+)*(\\.[a-zA-Z0-9*]+(\\-[a-zA-Z0-9*]+)*)*";
+
+    private static final String EXCLLIST_REGEXP = "^$|^" + EXCL_REGEX + "(," + EXCL_REGEX + ")*$";
+
+    private static final Pattern EXCLLIST_PATTERN;
+
+    static {
+        HOSTNAME_PATTERN = Pattern.compile(HOSTNAME_REGEXP);
+        EXCLLIST_PATTERN = Pattern.compile(EXCLLIST_REGEXP);
+        sDefaultProxySelector = ProxySelector.getDefault();
+    }
+
+    /**
+     * Return the proxy object to be used for the URL given as parameter.
+     * @param ctx A Context used to get the settings for the proxy host.
+     * @param url A URL to be accessed. Used to evaluate exclusion list.
+     * @return Proxy (java.net) object containing the host name. If the
+     *         user did not set a hostname it returns the default host.
+     *         A null value means that no host is to be used.
+     * {@hide}
+     */
+    @UnsupportedAppUsage
+    public static final java.net.Proxy getProxy(Context ctx, String url) {
+        String host = "";
+        if ((url != null) && !isLocalHost(host)) {
+            URI uri = URI.create(url);
+            ProxySelector proxySelector = ProxySelector.getDefault();
+
+            List<java.net.Proxy> proxyList = proxySelector.select(uri);
+
+            if (proxyList.size() > 0) {
+                return proxyList.get(0);
+            }
+        }
+        return java.net.Proxy.NO_PROXY;
+    }
+
+
+    /**
+     * Return the proxy host set by the user.
+     * @param ctx A Context used to get the settings for the proxy host.
+     * @return String containing the host name. If the user did not set a host
+     *         name it returns the default host. A null value means that no
+     *         host is to be used.
+     * @deprecated Use standard java vm proxy values to find the host, port
+     *         and exclusion list.  This call ignores the exclusion list.
+     */
+    @Deprecated
+    public static final String getHost(Context ctx) {
+        java.net.Proxy proxy = getProxy(ctx, null);
+        if (proxy == java.net.Proxy.NO_PROXY) return null;
+        try {
+            return ((InetSocketAddress)(proxy.address())).getHostName();
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
+    /**
+     * Return the proxy port set by the user.
+     * @param ctx A Context used to get the settings for the proxy port.
+     * @return The port number to use or -1 if no proxy is to be used.
+     * @deprecated Use standard java vm proxy values to find the host, port
+     *         and exclusion list.  This call ignores the exclusion list.
+     */
+    @Deprecated
+    public static final int getPort(Context ctx) {
+        java.net.Proxy proxy = getProxy(ctx, null);
+        if (proxy == java.net.Proxy.NO_PROXY) return -1;
+        try {
+            return ((InetSocketAddress)(proxy.address())).getPort();
+        } catch (Exception e) {
+            return -1;
+        }
+    }
+
+    /**
+     * Return the default proxy host specified by the carrier.
+     * @return String containing the host name or null if there is no proxy for
+     * this carrier.
+     * @deprecated Use standard java vm proxy values to find the host, port and
+     *         exclusion list.  This call ignores the exclusion list and no
+     *         longer reports only mobile-data apn-based proxy values.
+     */
+    @Deprecated
+    public static final String getDefaultHost() {
+        String host = System.getProperty("http.proxyHost");
+        if (TextUtils.isEmpty(host)) return null;
+        return host;
+    }
+
+    /**
+     * Return the default proxy port specified by the carrier.
+     * @return The port number to be used with the proxy host or -1 if there is
+     * no proxy for this carrier.
+     * @deprecated Use standard java vm proxy values to find the host, port and
+     *         exclusion list.  This call ignores the exclusion list and no
+     *         longer reports only mobile-data apn-based proxy values.
+     */
+    @Deprecated
+    public static final int getDefaultPort() {
+        if (getDefaultHost() == null) return -1;
+        try {
+            return Integer.parseInt(System.getProperty("http.proxyPort"));
+        } catch (NumberFormatException e) {
+            return -1;
+        }
+    }
+
+    private static final boolean isLocalHost(String host) {
+        if (host == null) {
+            return false;
+        }
+        try {
+            if (host != null) {
+                if (host.equalsIgnoreCase("localhost")) {
+                    return true;
+                }
+                if (NetworkUtils.numericToInetAddress(host).isLoopbackAddress()) {
+                    return true;
+                }
+            }
+        } catch (IllegalArgumentException iex) {
+        }
+        return false;
+    }
+
+    /**
+     * Validate syntax of hostname, port and exclusion list entries
+     * {@hide}
+     */
+    public static int validate(String hostname, String port, String exclList) {
+        Matcher match = HOSTNAME_PATTERN.matcher(hostname);
+        Matcher listMatch = EXCLLIST_PATTERN.matcher(exclList);
+
+        if (!match.matches()) return PROXY_HOSTNAME_INVALID;
+
+        if (!listMatch.matches()) return PROXY_EXCLLIST_INVALID;
+
+        if (hostname.length() > 0 && port.length() == 0) return PROXY_PORT_EMPTY;
+
+        if (port.length() > 0) {
+            if (hostname.length() == 0) return PROXY_HOSTNAME_EMPTY;
+            int portVal = -1;
+            try {
+                portVal = Integer.parseInt(port);
+            } catch (NumberFormatException ex) {
+                return PROXY_PORT_INVALID;
+            }
+            if (portVal <= 0 || portVal > 0xFFFF) return PROXY_PORT_INVALID;
+        }
+        return PROXY_VALID;
+    }
+
+    /** @hide */
+    @UnsupportedAppUsage
+    public static final void setHttpProxySystemProperty(ProxyInfo p) {
+        String host = null;
+        String port = null;
+        String exclList = null;
+        Uri pacFileUrl = Uri.EMPTY;
+        if (p != null) {
+            host = p.getHost();
+            port = Integer.toString(p.getPort());
+            exclList = p.getExclusionListAsString();
+            pacFileUrl = p.getPacFileUrl();
+        }
+        setHttpProxySystemProperty(host, port, exclList, pacFileUrl);
+    }
+
+    /** @hide */
+    public static final void setHttpProxySystemProperty(String host, String port, String exclList,
+            Uri pacFileUrl) {
+        if (exclList != null) exclList = exclList.replace(",", "|");
+        if (false) Log.d(TAG, "setHttpProxySystemProperty :"+host+":"+port+" - "+exclList);
+        if (host != null) {
+            System.setProperty("http.proxyHost", host);
+            System.setProperty("https.proxyHost", host);
+        } else {
+            System.clearProperty("http.proxyHost");
+            System.clearProperty("https.proxyHost");
+        }
+        if (port != null) {
+            System.setProperty("http.proxyPort", port);
+            System.setProperty("https.proxyPort", port);
+        } else {
+            System.clearProperty("http.proxyPort");
+            System.clearProperty("https.proxyPort");
+        }
+        if (exclList != null) {
+            System.setProperty("http.nonProxyHosts", exclList);
+            System.setProperty("https.nonProxyHosts", exclList);
+        } else {
+            System.clearProperty("http.nonProxyHosts");
+            System.clearProperty("https.nonProxyHosts");
+        }
+        if (!Uri.EMPTY.equals(pacFileUrl)) {
+            ProxySelector.setDefault(new PacProxySelector());
+        } else {
+            ProxySelector.setDefault(sDefaultProxySelector);
+        }
+    }
+}
diff --git a/android/net/ProxyInfo.java b/android/net/ProxyInfo.java
new file mode 100644
index 0000000..807c467
--- /dev/null
+++ b/android/net/ProxyInfo.java
@@ -0,0 +1,369 @@
+/*
+ * Copyright (C) 2010 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.UnsupportedAppUsage;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import java.net.InetSocketAddress;
+import java.net.URLConnection;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * Describes a proxy configuration.
+ *
+ * Proxy configurations are already integrated within the {@code java.net} and
+ * Apache HTTP stack. So {@link URLConnection} and Apache's {@code HttpClient} will use
+ * them automatically.
+ *
+ * Other HTTP stacks will need to obtain the proxy info from
+ * {@link Proxy#PROXY_CHANGE_ACTION} broadcast as the extra {@link Proxy#EXTRA_PROXY_INFO}.
+ */
+public class ProxyInfo implements Parcelable {
+
+    private final String mHost;
+    private final int mPort;
+    private final String mExclusionList;
+    private final String[] mParsedExclusionList;
+    private final Uri mPacFileUrl;
+
+    /**
+     *@hide
+     */
+    public static final String LOCAL_EXCL_LIST = "";
+    /**
+     *@hide
+     */
+    public static final int LOCAL_PORT = -1;
+    /**
+     *@hide
+     */
+    public static final String LOCAL_HOST = "localhost";
+
+    /**
+     * Constructs a {@link ProxyInfo} object that points at a Direct proxy
+     * on the specified host and port.
+     */
+    public static ProxyInfo buildDirectProxy(String host, int port) {
+        return new ProxyInfo(host, port, null);
+    }
+
+    /**
+     * Constructs a {@link ProxyInfo} object that points at a Direct proxy
+     * on the specified host and port.
+     *
+     * The proxy will not be used to access any host in exclusion list, exclList.
+     *
+     * @param exclList Hosts to exclude using the proxy on connections for.  These
+     *                 hosts can use wildcards such as *.example.com.
+     */
+    public static ProxyInfo buildDirectProxy(String host, int port, List<String> exclList) {
+        String[] array = exclList.toArray(new String[exclList.size()]);
+        return new ProxyInfo(host, port, TextUtils.join(",", array), array);
+    }
+
+    /**
+     * Construct a {@link ProxyInfo} that will download and run the PAC script
+     * at the specified URL.
+     */
+    public static ProxyInfo buildPacProxy(Uri pacUri) {
+        return new ProxyInfo(pacUri);
+    }
+
+    /**
+     * Create a ProxyProperties that points at a HTTP Proxy.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public ProxyInfo(String host, int port, String exclList) {
+        mHost = host;
+        mPort = port;
+        mExclusionList = exclList;
+        mParsedExclusionList = parseExclusionList(mExclusionList);
+        mPacFileUrl = Uri.EMPTY;
+    }
+
+    /**
+     * Create a ProxyProperties that points at a PAC URL.
+     * @hide
+     */
+    public ProxyInfo(Uri pacFileUrl) {
+        mHost = LOCAL_HOST;
+        mPort = LOCAL_PORT;
+        mExclusionList = LOCAL_EXCL_LIST;
+        mParsedExclusionList = parseExclusionList(mExclusionList);
+        if (pacFileUrl == null) {
+            throw new NullPointerException();
+        }
+        mPacFileUrl = pacFileUrl;
+    }
+
+    /**
+     * Create a ProxyProperties that points at a PAC URL.
+     * @hide
+     */
+    public ProxyInfo(String pacFileUrl) {
+        mHost = LOCAL_HOST;
+        mPort = LOCAL_PORT;
+        mExclusionList = LOCAL_EXCL_LIST;
+        mParsedExclusionList = parseExclusionList(mExclusionList);
+        mPacFileUrl = Uri.parse(pacFileUrl);
+    }
+
+    /**
+     * Only used in PacManager after Local Proxy is bound.
+     * @hide
+     */
+    public ProxyInfo(Uri pacFileUrl, int localProxyPort) {
+        mHost = LOCAL_HOST;
+        mPort = localProxyPort;
+        mExclusionList = LOCAL_EXCL_LIST;
+        mParsedExclusionList = parseExclusionList(mExclusionList);
+        if (pacFileUrl == null) {
+            throw new NullPointerException();
+        }
+        mPacFileUrl = pacFileUrl;
+    }
+
+    private static String[] parseExclusionList(String exclusionList) {
+        if (exclusionList == null) {
+            return new String[0];
+        } else {
+            return exclusionList.toLowerCase(Locale.ROOT).split(",");
+        }
+    }
+
+    private ProxyInfo(String host, int port, String exclList, String[] parsedExclList) {
+        mHost = host;
+        mPort = port;
+        mExclusionList = exclList;
+        mParsedExclusionList = parsedExclList;
+        mPacFileUrl = Uri.EMPTY;
+    }
+
+    // copy constructor instead of clone
+    /**
+     * @hide
+     */
+    public ProxyInfo(ProxyInfo source) {
+        if (source != null) {
+            mHost = source.getHost();
+            mPort = source.getPort();
+            mPacFileUrl = source.mPacFileUrl;
+            mExclusionList = source.getExclusionListAsString();
+            mParsedExclusionList = source.mParsedExclusionList;
+        } else {
+            mHost = null;
+            mPort = 0;
+            mExclusionList = null;
+            mParsedExclusionList = null;
+            mPacFileUrl = Uri.EMPTY;
+        }
+    }
+
+    /**
+     * @hide
+     */
+    public InetSocketAddress getSocketAddress() {
+        InetSocketAddress inetSocketAddress = null;
+        try {
+            inetSocketAddress = new InetSocketAddress(mHost, mPort);
+        } catch (IllegalArgumentException e) { }
+        return inetSocketAddress;
+    }
+
+    /**
+     * Returns the URL of the current PAC script or null if there is
+     * no PAC script.
+     */
+    public Uri getPacFileUrl() {
+        return mPacFileUrl;
+    }
+
+    /**
+     * When configured to use a Direct Proxy this returns the host
+     * of the proxy.
+     */
+    public String getHost() {
+        return mHost;
+    }
+
+    /**
+     * When configured to use a Direct Proxy this returns the port
+     * of the proxy
+     */
+    public int getPort() {
+        return mPort;
+    }
+
+    /**
+     * When configured to use a Direct Proxy this returns the list
+     * of hosts for which the proxy is ignored.
+     */
+    public String[] getExclusionList() {
+        return mParsedExclusionList;
+    }
+
+    /**
+     * comma separated
+     * @hide
+     */
+    public String getExclusionListAsString() {
+        return mExclusionList;
+    }
+
+    /**
+     * @hide
+     */
+    public boolean isValid() {
+        if (!Uri.EMPTY.equals(mPacFileUrl)) return true;
+        return Proxy.PROXY_VALID == Proxy.validate(mHost == null ? "" : mHost,
+                mPort == 0 ? "" : Integer.toString(mPort),
+                mExclusionList == null ? "" : mExclusionList);
+    }
+
+    /**
+     * @hide
+     */
+    public java.net.Proxy makeProxy() {
+        java.net.Proxy proxy = java.net.Proxy.NO_PROXY;
+        if (mHost != null) {
+            try {
+                InetSocketAddress inetSocketAddress = new InetSocketAddress(mHost, mPort);
+                proxy = new java.net.Proxy(java.net.Proxy.Type.HTTP, inetSocketAddress);
+            } catch (IllegalArgumentException e) {
+            }
+        }
+        return proxy;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        if (!Uri.EMPTY.equals(mPacFileUrl)) {
+            sb.append("PAC Script: ");
+            sb.append(mPacFileUrl);
+        }
+        if (mHost != null) {
+            sb.append("[");
+            sb.append(mHost);
+            sb.append("] ");
+            sb.append(Integer.toString(mPort));
+            if (mExclusionList != null) {
+                sb.append(" xl=").append(mExclusionList);
+            }
+        } else {
+            sb.append("[ProxyProperties.mHost == null]");
+        }
+        return sb.toString();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (!(o instanceof ProxyInfo)) return false;
+        ProxyInfo p = (ProxyInfo)o;
+        // If PAC URL is present in either then they must be equal.
+        // Other parameters will only be for fall back.
+        if (!Uri.EMPTY.equals(mPacFileUrl)) {
+            return mPacFileUrl.equals(p.getPacFileUrl()) && mPort == p.mPort;
+        }
+        if (!Uri.EMPTY.equals(p.mPacFileUrl)) {
+            return false;
+        }
+        if (mExclusionList != null && !mExclusionList.equals(p.getExclusionListAsString())) {
+            return false;
+        }
+        if (mHost != null && p.getHost() != null && mHost.equals(p.getHost()) == false) {
+            return false;
+        }
+        if (mHost != null && p.mHost == null) return false;
+        if (mHost == null && p.mHost != null) return false;
+        if (mPort != p.mPort) return false;
+        return true;
+    }
+
+    /**
+     * Implement the Parcelable interface
+     * @hide
+     */
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    /*
+     * generate hashcode based on significant fields
+     */
+    public int hashCode() {
+        return ((null == mHost) ? 0 : mHost.hashCode())
+                + ((null == mExclusionList) ? 0 : mExclusionList.hashCode())
+                + mPort;
+    }
+
+    /**
+     * Implement the Parcelable interface.
+     * @hide
+     */
+    public void writeToParcel(Parcel dest, int flags) {
+        if (!Uri.EMPTY.equals(mPacFileUrl)) {
+            dest.writeByte((byte)1);
+            mPacFileUrl.writeToParcel(dest, 0);
+            dest.writeInt(mPort);
+            return;
+        } else {
+            dest.writeByte((byte)0);
+        }
+        if (mHost != null) {
+            dest.writeByte((byte)1);
+            dest.writeString(mHost);
+            dest.writeInt(mPort);
+        } else {
+            dest.writeByte((byte)0);
+        }
+        dest.writeString(mExclusionList);
+        dest.writeStringArray(mParsedExclusionList);
+    }
+
+    public static final @android.annotation.NonNull Creator<ProxyInfo> CREATOR =
+        new Creator<ProxyInfo>() {
+            public ProxyInfo createFromParcel(Parcel in) {
+                String host = null;
+                int port = 0;
+                if (in.readByte() != 0) {
+                    Uri url = Uri.CREATOR.createFromParcel(in);
+                    int localPort = in.readInt();
+                    return new ProxyInfo(url, localPort);
+                }
+                if (in.readByte() != 0) {
+                    host = in.readString();
+                    port = in.readInt();
+                }
+                String exclList = in.readString();
+                String[] parsedExclList = in.readStringArray();
+                ProxyInfo proxyProperties = new ProxyInfo(host, port, exclList, parsedExclList);
+                return proxyProperties;
+            }
+
+            public ProxyInfo[] newArray(int size) {
+                return new ProxyInfo[size];
+            }
+        };
+}
diff --git a/android/net/RouteInfo.java b/android/net/RouteInfo.java
new file mode 100644
index 0000000..52d3fc4
--- /dev/null
+++ b/android/net/RouteInfo.java
@@ -0,0 +1,551 @@
+/*
+ * Copyright (C) 2011 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.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.annotation.UnsupportedAppUsage;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.Collection;
+import java.util.Objects;
+
+/**
+ * Represents a network route.
+ * <p>
+ * This is used both to describe static network configuration and live network
+ * configuration information.
+ *
+ * A route contains three pieces of information:
+ * <ul>
+ * <li>a destination {@link IpPrefix} specifying the network destinations covered by this route.
+ *     If this is {@code null} it indicates a default route of the address family (IPv4 or IPv6)
+ *     implied by the gateway IP address.
+ * <li>a gateway {@link InetAddress} indicating the next hop to use.  If this is {@code null} it
+ *     indicates a directly-connected route.
+ * <li>an interface (which may be unspecified).
+ * </ul>
+ * Either the destination or the gateway may be {@code null}, but not both.  If the
+ * destination and gateway are both specified, they must be of the same address family
+ * (IPv4 or IPv6).
+ */
+public final class RouteInfo implements Parcelable {
+    /** @hide */
+    @IntDef(value = {
+            RTN_UNICAST,
+            RTN_UNREACHABLE,
+            RTN_THROW,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface RouteType {}
+
+    /**
+     * The IP destination address for this route.
+     */
+    @NonNull
+    private final IpPrefix mDestination;
+
+    /**
+     * The gateway address for this route.
+     */
+    @UnsupportedAppUsage
+    @Nullable
+    private final InetAddress mGateway;
+
+    /**
+     * The interface for this route.
+     */
+    @Nullable
+    private final String mInterface;
+
+
+    /** Unicast route. @hide */
+    @SystemApi
+    @TestApi
+    public static final int RTN_UNICAST = 1;
+
+    /** Unreachable route. @hide */
+    @SystemApi
+    @TestApi
+    public static final int RTN_UNREACHABLE = 7;
+
+    /** Throw route. @hide */
+    @SystemApi
+    @TestApi
+    public static final int RTN_THROW = 9;
+
+    /**
+     * The type of this route; one of the RTN_xxx constants above.
+     */
+    private final int mType;
+
+    // Derived data members.
+    // TODO: remove these.
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    private final boolean mIsHost;
+    private final boolean mHasGateway;
+
+    /**
+     * Constructs a RouteInfo object.
+     *
+     * If destination is null, then gateway must be specified and the
+     * constructed route is either the IPv4 default route <code>0.0.0.0</code>
+     * if the gateway is an instance of {@link Inet4Address}, or the IPv6 default
+     * route <code>::/0</code> if gateway is an instance of
+     * {@link Inet6Address}.
+     * <p>
+     * destination and gateway may not both be null.
+     *
+     * @param destination the destination prefix
+     * @param gateway the IP address to route packets through
+     * @param iface the interface name to send packets on
+     * @param type the type of this route
+     *
+     * @hide
+     */
+    @SystemApi
+    @TestApi
+    public RouteInfo(@Nullable IpPrefix destination, @Nullable InetAddress gateway,
+            @Nullable String iface, @RouteType int type) {
+        switch (type) {
+            case RTN_UNICAST:
+            case RTN_UNREACHABLE:
+            case RTN_THROW:
+                // TODO: It would be nice to ensure that route types that don't have nexthops or
+                // interfaces, such as unreachable or throw, can't be created if an interface or
+                // a gateway is specified. This is a bit too complicated to do at the moment
+                // because:
+                //
+                // - LinkProperties sets the interface on routes added to it, and modifies the
+                //   interfaces of all the routes when its interface name changes.
+                // - Even when the gateway is null, we store a non-null gateway here.
+                //
+                // For now, we just rely on the code that sets routes to do things properly.
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown route type " + type);
+        }
+
+        if (destination == null) {
+            if (gateway != null) {
+                if (gateway instanceof Inet4Address) {
+                    destination = new IpPrefix(Inet4Address.ANY, 0);
+                } else {
+                    destination = new IpPrefix(Inet6Address.ANY, 0);
+                }
+            } else {
+                // no destination, no gateway. invalid.
+                throw new IllegalArgumentException("Invalid arguments passed in: " + gateway + "," +
+                                                   destination);
+            }
+        }
+        // TODO: set mGateway to null if there is no gateway. This is more correct, saves space, and
+        // matches the documented behaviour. Before we can do this we need to fix all callers (e.g.,
+        // ConnectivityService) to stop doing things like r.getGateway().equals(), ... .
+        if (gateway == null) {
+            if (destination.getAddress() instanceof Inet4Address) {
+                gateway = Inet4Address.ANY;
+            } else {
+                gateway = Inet6Address.ANY;
+            }
+        }
+        mHasGateway = (!gateway.isAnyLocalAddress());
+
+        if ((destination.getAddress() instanceof Inet4Address &&
+                 (gateway instanceof Inet4Address == false)) ||
+                (destination.getAddress() instanceof Inet6Address &&
+                 (gateway instanceof Inet6Address == false))) {
+            throw new IllegalArgumentException("address family mismatch in RouteInfo constructor");
+        }
+        mDestination = destination;  // IpPrefix objects are immutable.
+        mGateway = gateway;          // InetAddress objects are immutable.
+        mInterface = iface;          // Strings are immutable.
+        mType = type;
+        mIsHost = isHost();
+    }
+
+    /**
+     * Constructs a {@code RouteInfo} object.
+     *
+     * If destination is null, then gateway must be specified and the
+     * constructed route is either the IPv4 default route <code>0.0.0.0</code>
+     * if the gateway is an instance of {@link Inet4Address}, or the IPv6 default
+     * route <code>::/0</code> if gateway is an instance of {@link Inet6Address}.
+     * <p>
+     * Destination and gateway may not both be null.
+     *
+     * @param destination the destination address and prefix in an {@link IpPrefix}
+     * @param gateway the {@link InetAddress} to route packets through
+     * @param iface the interface name to send packets on
+     *
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public RouteInfo(@Nullable IpPrefix destination, @Nullable InetAddress gateway,
+            @Nullable String iface) {
+        this(destination, gateway, iface, RTN_UNICAST);
+    }
+
+    /**
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public RouteInfo(@Nullable LinkAddress destination, @Nullable InetAddress gateway,
+            @Nullable String iface) {
+        this(destination == null ? null :
+                new IpPrefix(destination.getAddress(), destination.getPrefixLength()),
+                gateway, iface);
+    }
+
+    /**
+     * Constructs a {@code RouteInfo} object.
+     *
+     * If destination is null, then gateway must be specified and the
+     * constructed route is either the IPv4 default route <code>0.0.0.0</code>
+     * if the gateway is an instance of {@link Inet4Address}, or the IPv6 default
+     * route <code>::/0</code> if gateway is an instance of {@link Inet6Address}.
+     * <p>
+     * Destination and gateway may not both be null.
+     *
+     * @param destination the destination address and prefix in an {@link IpPrefix}
+     * @param gateway the {@link InetAddress} to route packets through
+     *
+     * @hide
+     */
+    public RouteInfo(@Nullable IpPrefix destination, @Nullable InetAddress gateway) {
+        this(destination, gateway, null);
+    }
+
+    /**
+     * @hide
+     *
+     * TODO: Remove this.
+     */
+    @UnsupportedAppUsage
+    public RouteInfo(@Nullable LinkAddress destination, @Nullable InetAddress gateway) {
+        this(destination, gateway, null);
+    }
+
+    /**
+     * Constructs a default {@code RouteInfo} object.
+     *
+     * @param gateway the {@link InetAddress} to route packets through
+     *
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public RouteInfo(@NonNull InetAddress gateway) {
+        this((IpPrefix) null, gateway, null);
+    }
+
+    /**
+     * Constructs a {@code RouteInfo} object representing a direct connected subnet.
+     *
+     * @param destination the {@link IpPrefix} describing the address and prefix
+     *                    length of the subnet.
+     *
+     * @hide
+     */
+    public RouteInfo(@NonNull IpPrefix destination) {
+        this(destination, null, null);
+    }
+
+    /**
+     * @hide
+     */
+    public RouteInfo(@NonNull LinkAddress destination) {
+        this(destination, null, null);
+    }
+
+    /**
+     * @hide
+     */
+    public RouteInfo(@NonNull IpPrefix destination, @RouteType int type) {
+        this(destination, null, null, type);
+    }
+
+    /**
+     * @hide
+     */
+    public static RouteInfo makeHostRoute(@NonNull InetAddress host, @Nullable String iface) {
+        return makeHostRoute(host, null, iface);
+    }
+
+    /**
+     * @hide
+     */
+    public static RouteInfo makeHostRoute(@Nullable InetAddress host, @Nullable InetAddress gateway,
+            @Nullable String iface) {
+        if (host == null) return null;
+
+        if (host instanceof Inet4Address) {
+            return new RouteInfo(new IpPrefix(host, 32), gateway, iface);
+        } else {
+            return new RouteInfo(new IpPrefix(host, 128), gateway, iface);
+        }
+    }
+
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    private boolean isHost() {
+        return (mDestination.getAddress() instanceof Inet4Address &&
+                mDestination.getPrefixLength() == 32) ||
+               (mDestination.getAddress() instanceof Inet6Address &&
+                mDestination.getPrefixLength() == 128);
+    }
+
+    /**
+     * Retrieves the destination address and prefix length in the form of an {@link IpPrefix}.
+     *
+     * @return {@link IpPrefix} specifying the destination.  This is never {@code null}.
+     */
+    @NonNull
+    public IpPrefix getDestination() {
+        return mDestination;
+    }
+
+    /**
+     * TODO: Convert callers to use IpPrefix and then remove.
+     * @hide
+     */
+    @NonNull
+    public LinkAddress getDestinationLinkAddress() {
+        return new LinkAddress(mDestination.getAddress(), mDestination.getPrefixLength());
+    }
+
+    /**
+     * Retrieves the gateway or next hop {@link InetAddress} for this route.
+     *
+     * @return {@link InetAddress} specifying the gateway or next hop.  This may be
+     *                             {@code null} for a directly-connected route."
+     */
+    @Nullable
+    public InetAddress getGateway() {
+        return mGateway;
+    }
+
+    /**
+     * Retrieves the interface used for this route if specified, else {@code null}.
+     *
+     * @return The name of the interface used for this route.
+     */
+    @Nullable
+    public String getInterface() {
+        return mInterface;
+    }
+
+    /**
+     * Retrieves the type of this route.
+     *
+     * @return The type of this route; one of the {@code RTN_xxx} constants defined in this class.
+     *
+     * @hide
+     */
+    @TestApi
+    @SystemApi
+    @RouteType
+    public int getType() {
+        return mType;
+    }
+
+    /**
+     * Indicates if this route is a default route (ie, has no destination specified).
+     *
+     * @return {@code true} if the destination has a prefix length of 0.
+     */
+    public boolean isDefaultRoute() {
+        return mType == RTN_UNICAST && mDestination.getPrefixLength() == 0;
+    }
+
+    /**
+     * Indicates if this route is an IPv4 default route.
+     * @hide
+     */
+    public boolean isIPv4Default() {
+        return isDefaultRoute() && mDestination.getAddress() instanceof Inet4Address;
+    }
+
+    /**
+     * Indicates if this route is an IPv6 default route.
+     * @hide
+     */
+    public boolean isIPv6Default() {
+        return isDefaultRoute() && mDestination.getAddress() instanceof Inet6Address;
+    }
+
+    /**
+     * Indicates if this route is a host route (ie, matches only a single host address).
+     *
+     * @return {@code true} if the destination has a prefix length of 32 or 128 for IPv4 or IPv6,
+     * respectively.
+     * @hide
+     */
+    public boolean isHostRoute() {
+        return mIsHost;
+    }
+
+    /**
+     * Indicates if this route has a next hop ({@code true}) or is directly-connected
+     * ({@code false}).
+     *
+     * @return {@code true} if a gateway is specified
+     */
+    public boolean hasGateway() {
+        return mHasGateway;
+    }
+
+    /**
+     * Determines whether the destination and prefix of this route includes the specified
+     * address.
+     *
+     * @param destination A {@link InetAddress} to test to see if it would match this route.
+     * @return {@code true} if the destination and prefix length cover the given address.
+     */
+    public boolean matches(InetAddress destination) {
+        return mDestination.contains(destination);
+    }
+
+    /**
+     * Find the route from a Collection of routes that best matches a given address.
+     * May return null if no routes are applicable.
+     * @param routes a Collection of RouteInfos to chose from
+     * @param dest the InetAddress your trying to get to
+     * @return the RouteInfo from the Collection that best fits the given address
+     *
+     * @hide
+     */
+    @UnsupportedAppUsage
+    @Nullable
+    public static RouteInfo selectBestRoute(Collection<RouteInfo> routes, InetAddress dest) {
+        if ((routes == null) || (dest == null)) return null;
+
+        RouteInfo bestRoute = null;
+        // pick a longest prefix match under same address type
+        for (RouteInfo route : routes) {
+            if (NetworkUtils.addressTypeMatches(route.mDestination.getAddress(), dest)) {
+                if ((bestRoute != null) &&
+                        (bestRoute.mDestination.getPrefixLength() >=
+                        route.mDestination.getPrefixLength())) {
+                    continue;
+                }
+                if (route.matches(dest)) bestRoute = route;
+            }
+        }
+        return bestRoute;
+    }
+
+    /**
+     * Returns a human-readable description of this object.
+     */
+    public String toString() {
+        String val = "";
+        if (mDestination != null) val = mDestination.toString();
+        if (mType == RTN_UNREACHABLE) {
+            val += " unreachable";
+        } else if (mType == RTN_THROW) {
+            val += " throw";
+        } else {
+            val += " ->";
+            if (mGateway != null) val += " " + mGateway.getHostAddress();
+            if (mInterface != null) val += " " + mInterface;
+            if (mType != RTN_UNICAST) {
+                val += " unknown type " + mType;
+            }
+        }
+        return val;
+    }
+
+    /**
+     * Compares this RouteInfo object against the specified object and indicates if they are equal.
+     * @return {@code true} if the objects are equal, {@code false} otherwise.
+     */
+    public boolean equals(Object obj) {
+        if (this == obj) return true;
+
+        if (!(obj instanceof RouteInfo)) return false;
+
+        RouteInfo target = (RouteInfo) obj;
+
+        return Objects.equals(mDestination, target.getDestination()) &&
+                Objects.equals(mGateway, target.getGateway()) &&
+                Objects.equals(mInterface, target.getInterface()) &&
+                mType == target.getType();
+    }
+
+    /**
+     *  Returns a hashcode for this <code>RouteInfo</code> object.
+     */
+    public int hashCode() {
+        return (mDestination.hashCode() * 41)
+                + (mGateway == null ? 0 :mGateway.hashCode() * 47)
+                + (mInterface == null ? 0 :mInterface.hashCode() * 67)
+                + (mType * 71);
+    }
+
+    /**
+     * Implement the Parcelable interface
+     */
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Implement the Parcelable interface
+     */
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeParcelable(mDestination, flags);
+        byte[] gatewayBytes = (mGateway == null) ? null : mGateway.getAddress();
+        dest.writeByteArray(gatewayBytes);
+        dest.writeString(mInterface);
+        dest.writeInt(mType);
+    }
+
+    /**
+     * Implement the Parcelable interface.
+     */
+    public static final @android.annotation.NonNull Creator<RouteInfo> CREATOR =
+        new Creator<RouteInfo>() {
+        public RouteInfo createFromParcel(Parcel in) {
+            IpPrefix dest = in.readParcelable(null);
+
+            InetAddress gateway = null;
+            byte[] addr = in.createByteArray();
+            try {
+                gateway = InetAddress.getByAddress(addr);
+            } catch (UnknownHostException e) {}
+
+            String iface = in.readString();
+            int type = in.readInt();
+
+            return new RouteInfo(dest, gateway, iface, type);
+        }
+
+        public RouteInfo[] newArray(int size) {
+            return new RouteInfo[size];
+        }
+    };
+}
diff --git a/android/net/RssiCurve.java b/android/net/RssiCurve.java
new file mode 100644
index 0000000..a173b0c
--- /dev/null
+++ b/android/net/RssiCurve.java
@@ -0,0 +1,224 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.net;
+
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * A curve defining the network score over a range of RSSI values.
+ *
+ * <p>For each RSSI bucket, the score may be any byte. Scores have no absolute meaning and are only
+ * considered relative to other scores assigned by the same scorer. Networks with no score are
+ * treated equivalently to a network with score {@link Byte#MIN_VALUE}, and will not be used.
+ *
+ * <p>For example, consider a curve starting at -110 dBm with a bucket width of 10 and the
+ * following buckets: {@code [-20, -10, 0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100, 110, 120]}.
+ * This represents a linear curve between -110 dBm and 30 dBm. It scores progressively higher at
+ * stronger signal strengths.
+ *
+ * <p>A network can be assigned a fixed score independent of RSSI by setting
+ * {@link #rssiBuckets} to a one-byte array whose element is the fixed score. {@link #start}
+ * should be set to the lowest RSSI value at which this fixed score should apply, and
+ * {@link #bucketWidth} should be set such that {@code start + bucketWidth} is equal to the
+ * highest RSSI value at which this fixed score should apply.
+ *
+ * <p>Note that RSSI values below -110 dBm or above 30 dBm are unlikely to cause any difference
+ * in connectivity behavior from those endpoints. That is, the connectivity framework will treat
+ * a network with a -120 dBm signal exactly as it would treat one with a -110 dBm signal.
+ * Therefore, graphs which specify scores outside this range may be truncated to this range by
+ * the system.
+ *
+ * @see ScoredNetwork
+ * @hide
+ */
+@SystemApi
+public class RssiCurve implements Parcelable {
+    private static final int DEFAULT_ACTIVE_NETWORK_RSSI_BOOST = 25;
+
+    /** The starting dBm of the curve. */
+    public final int start;
+
+    /** The width of each RSSI bucket, in dBm. */
+    public final int bucketWidth;
+
+    /** The score for each RSSI bucket. */
+    public final byte[] rssiBuckets;
+
+    /**
+     * The RSSI boost to give this network when active, in dBm.
+     *
+     * <p>When the system is connected to this network, it will pretend that the network has this
+     * much higher of an RSSI. This is to avoid switching networks when another network has only a
+     * slightly higher score.
+     */
+    public final int activeNetworkRssiBoost;
+
+    /**
+     * Construct a new {@link RssiCurve}.
+     *
+     * @param start the starting dBm of the curve.
+     * @param bucketWidth the width of each RSSI bucket, in dBm.
+     * @param rssiBuckets the score for each RSSI bucket.
+     */
+    public RssiCurve(int start, int bucketWidth, byte[] rssiBuckets) {
+        this(start, bucketWidth, rssiBuckets, DEFAULT_ACTIVE_NETWORK_RSSI_BOOST);
+    }
+
+    /**
+     * Construct a new {@link RssiCurve}.
+     *
+     * @param start the starting dBm of the curve.
+     * @param bucketWidth the width of each RSSI bucket, in dBm.
+     * @param rssiBuckets the score for each RSSI bucket.
+     * @param activeNetworkRssiBoost the RSSI boost to apply when this network is active, in dBm.
+     */
+    public RssiCurve(int start, int bucketWidth, byte[] rssiBuckets, int activeNetworkRssiBoost) {
+        this.start = start;
+        this.bucketWidth = bucketWidth;
+        if (rssiBuckets == null || rssiBuckets.length == 0) {
+            throw new IllegalArgumentException("rssiBuckets must be at least one element large.");
+        }
+        this.rssiBuckets = rssiBuckets;
+        this.activeNetworkRssiBoost = activeNetworkRssiBoost;
+    }
+
+    private RssiCurve(Parcel in) {
+        start = in.readInt();
+        bucketWidth = in.readInt();
+        int bucketCount = in.readInt();
+        rssiBuckets = new byte[bucketCount];
+        in.readByteArray(rssiBuckets);
+        activeNetworkRssiBoost = in.readInt();
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeInt(start);
+        out.writeInt(bucketWidth);
+        out.writeInt(rssiBuckets.length);
+        out.writeByteArray(rssiBuckets);
+        out.writeInt(activeNetworkRssiBoost);
+    }
+
+    /**
+     * Lookup the score for a given RSSI value.
+     *
+     * @param rssi The RSSI to lookup. If the RSSI falls below the start of the curve, the score at
+     *         the start of the curve will be returned. If it falls after the end of the curve, the
+     *         score at the end of the curve will be returned.
+     * @return the score for the given RSSI.
+     */
+    public byte lookupScore(int rssi) {
+        return lookupScore(rssi, false /* isActiveNetwork */);
+    }
+
+    /**
+     * Lookup the score for a given RSSI value.
+     *
+     * @param rssi The RSSI to lookup. If the RSSI falls below the start of the curve, the score at
+     *         the start of the curve will be returned. If it falls after the end of the curve, the
+     *         score at the end of the curve will be returned.
+     * @param isActiveNetwork Whether this network is currently active.
+     * @return the score for the given RSSI.
+     */
+    public byte lookupScore(int rssi, boolean isActiveNetwork) {
+        if (isActiveNetwork) {
+            rssi += activeNetworkRssiBoost;
+        }
+
+        int index = (rssi - start) / bucketWidth;
+
+        // Snap the index to the closest bucket if it falls outside the curve.
+        if (index < 0) {
+            index = 0;
+        } else if (index > rssiBuckets.length - 1) {
+            index = rssiBuckets.length - 1;
+        }
+
+        return rssiBuckets[index];
+    }
+
+    /**
+     * Determine if two RSSI curves are defined in the same way.
+     *
+     * <p>Note that two curves can be equivalent but defined differently, e.g. if one bucket in one
+     * curve is split into two buckets in another. For the purpose of this method, these curves are
+     * not considered equal to each other.
+     */
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        RssiCurve rssiCurve = (RssiCurve) o;
+
+        return start == rssiCurve.start &&
+                bucketWidth == rssiCurve.bucketWidth &&
+                Arrays.equals(rssiBuckets, rssiCurve.rssiBuckets) &&
+                activeNetworkRssiBoost == rssiCurve.activeNetworkRssiBoost;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(start, bucketWidth, activeNetworkRssiBoost) ^ Arrays.hashCode(rssiBuckets);
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("RssiCurve[start=")
+                .append(start)
+                .append(",bucketWidth=")
+                .append(bucketWidth)
+                .append(",activeNetworkRssiBoost=")
+                .append(activeNetworkRssiBoost);
+
+        sb.append(",buckets=");
+        for (int i = 0; i < rssiBuckets.length; i++) {
+            sb.append(rssiBuckets[i]);
+            if (i < rssiBuckets.length - 1) {
+                sb.append(",");
+            }
+        }
+        sb.append("]");
+
+        return sb.toString();
+    }
+
+    public static final @android.annotation.NonNull Creator<RssiCurve> CREATOR =
+            new Creator<RssiCurve>() {
+                @Override
+                public RssiCurve createFromParcel(Parcel in) {
+                    return new RssiCurve(in);
+                }
+
+                @Override
+                public RssiCurve[] newArray(int size) {
+                    return new RssiCurve[size];
+                }
+            };
+}
diff --git a/android/net/SSLCertificateSocketFactory.java b/android/net/SSLCertificateSocketFactory.java
new file mode 100644
index 0000000..95d66bb
--- /dev/null
+++ b/android/net/SSLCertificateSocketFactory.java
@@ -0,0 +1,617 @@
+/*
+ * Copyright (C) 2008 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.UnsupportedAppUsage;
+import android.os.Build;
+import android.os.SystemProperties;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.os.RoSystemProperties;
+import com.android.org.conscrypt.ClientSessionContext;
+import com.android.org.conscrypt.OpenSSLSocketImpl;
+import com.android.org.conscrypt.SSLClientSessionCache;
+
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.net.SocketException;
+import java.security.KeyManagementException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+
+import javax.net.SocketFactory;
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLException;
+import javax.net.ssl.SSLPeerUnverifiedException;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.SSLSocket;
+import javax.net.ssl.SSLSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+
+/**
+ * SSLSocketFactory implementation with several extra features:
+ *
+ * <ul>
+ * <li>Timeout specification for SSL handshake operations
+ * <li>Hostname verification in most cases (see WARNINGs below)
+ * <li>Optional SSL session caching with {@link SSLSessionCache}
+ * <li>Optionally bypass all SSL certificate checks
+ * </ul>
+ *
+ * The handshake timeout does not apply to actual TCP socket connection.
+ * If you want a connection timeout as well, use {@link #createSocket()}
+ * and {@link Socket#connect(java.net.SocketAddress, int)}, after which you
+ * must verify the identity of the server you are connected to.
+ *
+ * <p class="caution"><b>Most {@link SSLSocketFactory} implementations do not
+ * verify the server's identity, allowing man-in-the-middle attacks.</b>
+ * This implementation does check the server's certificate hostname, but only
+ * for createSocket variants that specify a hostname.  When using methods that
+ * use {@link InetAddress} or which return an unconnected socket, you MUST
+ * verify the server's identity yourself to ensure a secure connection.
+ *
+ * Refer to
+ * <a href="https://developer.android.com/training/articles/security-gms-provider.html">
+ * Updating Your Security Provider to Protect Against SSL Exploits</a>
+ * for further information.</p>
+ *
+ * <p>The recommended way to verify the server's identity is to use
+ * {@link HttpsURLConnection#getDefaultHostnameVerifier()} to get a
+ * {@link HostnameVerifier} to verify the certificate hostname.
+ *
+ * <p><b>Warning</b>: Some methods on this class return connected sockets and some return
+ * unconnected sockets.  For the methods that return connected sockets, setting
+ * connection- or handshake-related properties on those sockets will have no effect.
+ *
+ * <p>On development devices, "setprop socket.relaxsslcheck yes" bypasses all
+ * SSL certificate and hostname checks for testing purposes.  This setting
+ * requires root access.
+ *
+ * @deprecated This class has less error-prone replacements using standard APIs.  To create an
+ * {@code SSLSocket}, obtain an {@link SSLSocketFactory} from {@link SSLSocketFactory#getDefault()}
+ * or {@link javax.net.ssl.SSLContext#getSocketFactory()}.  To verify hostnames, pass
+ * {@code "HTTPS"} to
+ * {@link javax.net.ssl.SSLParameters#setEndpointIdentificationAlgorithm(String)}.  To enable ALPN,
+ * use {@link javax.net.ssl.SSLParameters#setApplicationProtocols(String[])}.  To enable SNI,
+ * use {@link javax.net.ssl.SSLParameters#setServerNames(java.util.List)}.
+ */
+@Deprecated
+public class SSLCertificateSocketFactory extends SSLSocketFactory {
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    private static final String TAG = "SSLCertificateSocketFactory";
+
+    @UnsupportedAppUsage
+    private static final TrustManager[] INSECURE_TRUST_MANAGER = new TrustManager[] {
+        new X509TrustManager() {
+            public X509Certificate[] getAcceptedIssuers() { return null; }
+            public void checkClientTrusted(X509Certificate[] certs, String authType) { }
+            public void checkServerTrusted(X509Certificate[] certs, String authType) { }
+        }
+    };
+
+    @UnsupportedAppUsage
+    private SSLSocketFactory mInsecureFactory = null;
+    @UnsupportedAppUsage
+    private SSLSocketFactory mSecureFactory = null;
+    @UnsupportedAppUsage
+    private TrustManager[] mTrustManagers = null;
+    @UnsupportedAppUsage
+    private KeyManager[] mKeyManagers = null;
+    @UnsupportedAppUsage
+    private byte[] mNpnProtocols = null;
+    @UnsupportedAppUsage
+    private byte[] mAlpnProtocols = null;
+    @UnsupportedAppUsage
+    private PrivateKey mChannelIdPrivateKey = null;
+
+    @UnsupportedAppUsage
+    private final int mHandshakeTimeoutMillis;
+    @UnsupportedAppUsage
+    private final SSLClientSessionCache mSessionCache;
+    @UnsupportedAppUsage
+    private final boolean mSecure;
+
+    /** @deprecated Use {@link #getDefault(int)} instead. */
+    @Deprecated
+    public SSLCertificateSocketFactory(int handshakeTimeoutMillis) {
+        this(handshakeTimeoutMillis, null, true);
+    }
+
+    @UnsupportedAppUsage
+    private SSLCertificateSocketFactory(
+            int handshakeTimeoutMillis, SSLSessionCache cache, boolean secure) {
+        mHandshakeTimeoutMillis = handshakeTimeoutMillis;
+        mSessionCache = cache == null ? null : cache.mSessionCache;
+        mSecure = secure;
+    }
+
+    /**
+     * Returns a new socket factory instance with an optional handshake timeout.
+     *
+     * @param handshakeTimeoutMillis to use for SSL connection handshake, or 0
+     *         for none.  The socket timeout is reset to 0 after the handshake.
+     * @return a new SSLSocketFactory with the specified parameters
+     */
+    public static SocketFactory getDefault(int handshakeTimeoutMillis) {
+        return new SSLCertificateSocketFactory(handshakeTimeoutMillis, null, true);
+    }
+
+    /**
+     * Returns a new socket factory instance with an optional handshake timeout
+     * and SSL session cache.
+     *
+     * @param handshakeTimeoutMillis to use for SSL connection handshake, or 0
+     *         for none.  The socket timeout is reset to 0 after the handshake.
+     * @param cache The {@link SSLSessionCache} to use, or null for no cache.
+     * @return a new SSLSocketFactory with the specified parameters
+     */
+    public static SSLSocketFactory getDefault(int handshakeTimeoutMillis, SSLSessionCache cache) {
+        return new SSLCertificateSocketFactory(handshakeTimeoutMillis, cache, true);
+    }
+
+    /**
+     * Returns a new instance of a socket factory with all SSL security checks
+     * disabled, using an optional handshake timeout and SSL session cache.
+     *
+     * <p class="caution"><b>Warning:</b> Sockets created using this factory
+     * are vulnerable to man-in-the-middle attacks!</p>
+     *
+     * @param handshakeTimeoutMillis to use for SSL connection handshake, or 0
+     *         for none.  The socket timeout is reset to 0 after the handshake.
+     * @param cache The {@link SSLSessionCache} to use, or null for no cache.
+     * @return an insecure SSLSocketFactory with the specified parameters
+     */
+    public static SSLSocketFactory getInsecure(int handshakeTimeoutMillis, SSLSessionCache cache) {
+        return new SSLCertificateSocketFactory(handshakeTimeoutMillis, cache, false);
+    }
+
+    /**
+     * Returns a socket factory (also named SSLSocketFactory, but in a different
+     * namespace) for use with the Apache HTTP stack.
+     *
+     * @param handshakeTimeoutMillis to use for SSL connection handshake, or 0
+     *         for none.  The socket timeout is reset to 0 after the handshake.
+     * @param cache The {@link SSLSessionCache} to use, or null for no cache.
+     * @return a new SocketFactory with the specified parameters
+     *
+     * @deprecated Use {@link #getDefault()} along with a {@link javax.net.ssl.HttpsURLConnection}
+     *     instead. The Apache HTTP client is no longer maintained and may be removed in a future
+     *     release. Please visit <a href="http://android-developers.blogspot.com/2011/09/androids-http-clients.html">this webpage</a>
+     *     for further details.
+     *
+     * @removed
+     */
+    @Deprecated
+    public static org.apache.http.conn.ssl.SSLSocketFactory getHttpSocketFactory(
+            int handshakeTimeoutMillis, SSLSessionCache cache) {
+        return new org.apache.http.conn.ssl.SSLSocketFactory(
+                new SSLCertificateSocketFactory(handshakeTimeoutMillis, cache, true));
+    }
+
+    /**
+     * Verify the hostname of the certificate used by the other end of a connected socket using the
+     * {@link HostnameVerifier} obtained from {@code
+     * HttpsURLConnection.getDefaultHostnameVerifier()}. You MUST call this if you did not supply a
+     * hostname to {@link #createSocket()}.  It is harmless to call this method redundantly if the
+     * hostname has already been verified.
+     *
+     * <p>Wildcard certificates are allowed to verify any matching hostname, so
+     * "foo.bar.example.com" is verified if the peer has a certificate for "*.example.com".
+     *
+     * @param socket An SSL socket which has been connected to a server
+     * @param hostname The expected hostname of the remote server
+     * @throws IOException if something goes wrong handshaking with the server
+     * @throws SSLPeerUnverifiedException if the server cannot prove its identity
+     *
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public static void verifyHostname(Socket socket, String hostname) throws IOException {
+        if (!(socket instanceof SSLSocket)) {
+            throw new IllegalArgumentException("Attempt to verify non-SSL socket");
+        }
+
+        if (!isSslCheckRelaxed()) {
+            // The code at the start of OpenSSLSocketImpl.startHandshake()
+            // ensures that the call is idempotent, so we can safely call it.
+            SSLSocket ssl = (SSLSocket) socket;
+            ssl.startHandshake();
+
+            SSLSession session = ssl.getSession();
+            if (session == null) {
+                throw new SSLException("Cannot verify SSL socket without session");
+            }
+            if (!HttpsURLConnection.getDefaultHostnameVerifier().verify(hostname, session)) {
+                throw new SSLPeerUnverifiedException("Cannot verify hostname: " + hostname);
+            }
+        }
+    }
+
+    @UnsupportedAppUsage
+    private SSLSocketFactory makeSocketFactory(
+            KeyManager[] keyManagers, TrustManager[] trustManagers) {
+        try {
+            SSLContext sslContext = SSLContext.getInstance("TLS", "AndroidOpenSSL");
+            sslContext.init(keyManagers, trustManagers, null);
+            ((ClientSessionContext) sslContext.getClientSessionContext())
+                .setPersistentCache(mSessionCache);
+            return sslContext.getSocketFactory();
+        } catch (KeyManagementException | NoSuchAlgorithmException | NoSuchProviderException e) {
+            Log.wtf(TAG, e);
+            return (SSLSocketFactory) SSLSocketFactory.getDefault();  // Fallback
+        }
+    }
+
+    @UnsupportedAppUsage
+    private static boolean isSslCheckRelaxed() {
+        return RoSystemProperties.DEBUGGABLE &&
+            SystemProperties.getBoolean("socket.relaxsslcheck", false);
+    }
+
+    @UnsupportedAppUsage
+    private synchronized SSLSocketFactory getDelegate() {
+        // Relax the SSL check if instructed (for this factory, or systemwide)
+        if (!mSecure || isSslCheckRelaxed()) {
+            if (mInsecureFactory == null) {
+                if (mSecure) {
+                    Log.w(TAG, "*** BYPASSING SSL SECURITY CHECKS (socket.relaxsslcheck=yes) ***");
+                } else {
+                    Log.w(TAG, "Bypassing SSL security checks at caller's request");
+                }
+                mInsecureFactory = makeSocketFactory(mKeyManagers, INSECURE_TRUST_MANAGER);
+            }
+            return mInsecureFactory;
+        } else {
+            if (mSecureFactory == null) {
+                mSecureFactory = makeSocketFactory(mKeyManagers, mTrustManagers);
+            }
+            return mSecureFactory;
+        }
+    }
+
+    /**
+     * Sets the {@link TrustManager}s to be used for connections made by this factory.
+     */
+    public void setTrustManagers(TrustManager[] trustManager) {
+        mTrustManagers = trustManager;
+
+        // Clear out all cached secure factories since configurations have changed.
+        mSecureFactory = null;
+        // Note - insecure factories only ever use the INSECURE_TRUST_MANAGER so they need not
+        // be cleared out here.
+    }
+
+    /**
+     * Sets the <a href="http://technotes.googlecode.com/git/nextprotoneg.html">Next
+     * Protocol Negotiation (NPN)</a> protocols that this peer is interested in.
+     *
+     * <p>For servers this is the sequence of protocols to advertise as
+     * supported, in order of preference. This list is sent unencrypted to
+     * all clients that support NPN.
+     *
+     * <p>For clients this is a list of supported protocols to match against the
+     * server's list. If there is no protocol supported by both client and
+     * server then the first protocol in the client's list will be selected.
+     * The order of the client's protocols is otherwise insignificant.
+     *
+     * @param npnProtocols a non-empty list of protocol byte arrays. All arrays
+     *     must be non-empty and of length less than 256.
+     */
+    public void setNpnProtocols(byte[][] npnProtocols) {
+        this.mNpnProtocols = toLengthPrefixedList(npnProtocols);
+    }
+
+    /**
+     * Sets the
+     * <a href="http://tools.ietf.org/html/draft-ietf-tls-applayerprotoneg-01">
+     * Application Layer Protocol Negotiation (ALPN)</a> protocols that this peer
+     * is interested in.
+     *
+     * <p>For servers this is the sequence of protocols to advertise as
+     * supported, in order of preference. This list is sent unencrypted to
+     * all clients that support ALPN.
+     *
+     * <p>For clients this is a list of supported protocols to match against the
+     * server's list. If there is no protocol supported by both client and
+     * server then the first protocol in the client's list will be selected.
+     * The order of the client's protocols is otherwise insignificant.
+     *
+     * @param protocols a non-empty list of protocol byte arrays. All arrays
+     *     must be non-empty and of length less than 256.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public void setAlpnProtocols(byte[][] protocols) {
+        this.mAlpnProtocols = toLengthPrefixedList(protocols);
+    }
+
+    /**
+     * Returns an array containing the concatenation of length-prefixed byte
+     * strings.
+     * @hide
+     */
+    @VisibleForTesting
+    public static byte[] toLengthPrefixedList(byte[]... items) {
+        if (items.length == 0) {
+            throw new IllegalArgumentException("items.length == 0");
+        }
+        int totalLength = 0;
+        for (byte[] s : items) {
+            if (s.length == 0 || s.length > 255) {
+                throw new IllegalArgumentException("s.length == 0 || s.length > 255: " + s.length);
+            }
+            totalLength += 1 + s.length;
+        }
+        byte[] result = new byte[totalLength];
+        int pos = 0;
+        for (byte[] s : items) {
+            result[pos++] = (byte) s.length;
+            for (byte b : s) {
+                result[pos++] = b;
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Returns the <a href="http://technotes.googlecode.com/git/nextprotoneg.html">Next
+     * Protocol Negotiation (NPN)</a> protocol selected by client and server, or
+     * null if no protocol was negotiated.
+     *
+     * @param socket a socket created by this factory.
+     * @throws IllegalArgumentException if the socket was not created by this factory.
+     */
+    public byte[] getNpnSelectedProtocol(Socket socket) {
+        return castToOpenSSLSocket(socket).getNpnSelectedProtocol();
+    }
+
+    /**
+     * Returns the
+     * <a href="http://tools.ietf.org/html/draft-ietf-tls-applayerprotoneg-01">Application
+     * Layer Protocol Negotiation (ALPN)</a> protocol selected by client and server, or null
+     * if no protocol was negotiated.
+     *
+     * @param socket a socket created by this factory.
+     * @throws IllegalArgumentException if the socket was not created by this factory.
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    public byte[] getAlpnSelectedProtocol(Socket socket) {
+        return castToOpenSSLSocket(socket).getAlpnSelectedProtocol();
+    }
+
+    /**
+     * Sets the {@link KeyManager}s to be used for connections made by this factory.
+     */
+    public void setKeyManagers(KeyManager[] keyManagers) {
+        mKeyManagers = keyManagers;
+
+        // Clear out any existing cached factories since configurations have changed.
+        mSecureFactory = null;
+        mInsecureFactory = null;
+    }
+
+    /**
+     * Sets the private key to be used for TLS Channel ID by connections made by this
+     * factory.
+     *
+     * @param privateKey private key (enables TLS Channel ID) or {@code null} for no key (disables
+     *        TLS Channel ID). The private key has to be an Elliptic Curve (EC) key based on the
+     *        NIST P-256 curve (aka SECG secp256r1 or ANSI X9.62 prime256v1).
+     *
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    public void setChannelIdPrivateKey(PrivateKey privateKey) {
+        mChannelIdPrivateKey = privateKey;
+    }
+
+    /**
+     * Enables <a href="http://tools.ietf.org/html/rfc5077#section-3.2">session ticket</a>
+     * support on the given socket.
+     *
+     * @param socket a socket created by this factory
+     * @param useSessionTickets {@code true} to enable session ticket support on this socket.
+     * @throws IllegalArgumentException if the socket was not created by this factory.
+     */
+    public void setUseSessionTickets(Socket socket, boolean useSessionTickets) {
+        castToOpenSSLSocket(socket).setUseSessionTickets(useSessionTickets);
+    }
+
+    /**
+     * Turns on <a href="http://tools.ietf.org/html/rfc6066#section-3">Server
+     * Name Indication (SNI)</a> on a given socket.
+     *
+     * @param socket a socket created by this factory.
+     * @param hostName the desired SNI hostname, null to disable.
+     * @throws IllegalArgumentException if the socket was not created by this factory.
+     */
+    public void setHostname(Socket socket, String hostName) {
+        castToOpenSSLSocket(socket).setHostname(hostName);
+    }
+
+    /**
+     * Sets this socket's SO_SNDTIMEO write timeout in milliseconds.
+     * Use 0 for no timeout.
+     * To take effect, this option must be set before the blocking method was called.
+     *
+     * @param socket a socket created by this factory.
+     * @param timeout the desired write timeout in milliseconds.
+     * @throws IllegalArgumentException if the socket was not created by this factory.
+     *
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public void setSoWriteTimeout(Socket socket, int writeTimeoutMilliseconds)
+            throws SocketException {
+        castToOpenSSLSocket(socket).setSoWriteTimeout(writeTimeoutMilliseconds);
+    }
+
+    @UnsupportedAppUsage
+    private static OpenSSLSocketImpl castToOpenSSLSocket(Socket socket) {
+        if (!(socket instanceof OpenSSLSocketImpl)) {
+            throw new IllegalArgumentException("Socket not created by this factory: "
+                    + socket);
+        }
+
+        return (OpenSSLSocketImpl) socket;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>By default, this method returns a <i>connected</i> socket and verifies the peer's
+     * certificate hostname after connecting using the {@link HostnameVerifier} obtained from
+     * {@code HttpsURLConnection.getDefaultHostnameVerifier()}; if this instance was created with
+     * {@link #getInsecure(int, SSLSessionCache)}, it returns a socket that is <i>not connected</i>
+     * instead.
+     */
+    @Override
+    public Socket createSocket(Socket k, String host, int port, boolean close) throws IOException {
+        OpenSSLSocketImpl s = (OpenSSLSocketImpl) getDelegate().createSocket(k, host, port, close);
+        s.setNpnProtocols(mNpnProtocols);
+        s.setAlpnProtocols(mAlpnProtocols);
+        s.setHandshakeTimeout(mHandshakeTimeoutMillis);
+        s.setChannelIdPrivateKey(mChannelIdPrivateKey);
+        if (mSecure) {
+            verifyHostname(s, host);
+        }
+        return s;
+    }
+
+    /**
+     * Creates a new socket which is <i>not connected</i> to any remote host.
+     * You must use {@link Socket#connect} to connect the socket.
+     *
+     * <p class="caution"><b>Warning:</b> Hostname verification is not performed
+     * with this method.  You MUST verify the server's identity after connecting
+     * the socket to avoid man-in-the-middle attacks.</p>
+     */
+    @Override
+    public Socket createSocket() throws IOException {
+        OpenSSLSocketImpl s = (OpenSSLSocketImpl) getDelegate().createSocket();
+        s.setNpnProtocols(mNpnProtocols);
+        s.setAlpnProtocols(mAlpnProtocols);
+        s.setHandshakeTimeout(mHandshakeTimeoutMillis);
+        s.setChannelIdPrivateKey(mChannelIdPrivateKey);
+        return s;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>This method returns a socket that is <i>not connected</i>.
+     *
+     * <p class="caution"><b>Warning:</b> Hostname verification is not performed
+     * with this method.  You MUST verify the server's identity after connecting
+     * the socket to avoid man-in-the-middle attacks.</p>
+     */
+    @Override
+    public Socket createSocket(InetAddress addr, int port, InetAddress localAddr, int localPort)
+            throws IOException {
+        OpenSSLSocketImpl s = (OpenSSLSocketImpl) getDelegate().createSocket(
+                addr, port, localAddr, localPort);
+        s.setNpnProtocols(mNpnProtocols);
+        s.setAlpnProtocols(mAlpnProtocols);
+        s.setHandshakeTimeout(mHandshakeTimeoutMillis);
+        s.setChannelIdPrivateKey(mChannelIdPrivateKey);
+        return s;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>This method returns a socket that is <i>not connected</i>.
+     *
+     * <p class="caution"><b>Warning:</b> Hostname verification is not performed
+     * with this method.  You MUST verify the server's identity after connecting
+     * the socket to avoid man-in-the-middle attacks.</p>
+     */
+    @Override
+    public Socket createSocket(InetAddress addr, int port) throws IOException {
+        OpenSSLSocketImpl s = (OpenSSLSocketImpl) getDelegate().createSocket(addr, port);
+        s.setNpnProtocols(mNpnProtocols);
+        s.setAlpnProtocols(mAlpnProtocols);
+        s.setHandshakeTimeout(mHandshakeTimeoutMillis);
+        s.setChannelIdPrivateKey(mChannelIdPrivateKey);
+        return s;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>By default, this method returns a <i>connected</i> socket and verifies the peer's
+     * certificate hostname after connecting using the {@link HostnameVerifier} obtained from
+     * {@code HttpsURLConnection.getDefaultHostnameVerifier()}; if this instance was created with
+     * {@link #getInsecure(int, SSLSessionCache)}, it returns a socket that is <i>not connected</i>
+     * instead.
+     */
+    @Override
+    public Socket createSocket(String host, int port, InetAddress localAddr, int localPort)
+            throws IOException {
+        OpenSSLSocketImpl s = (OpenSSLSocketImpl) getDelegate().createSocket(
+                host, port, localAddr, localPort);
+        s.setNpnProtocols(mNpnProtocols);
+        s.setAlpnProtocols(mAlpnProtocols);
+        s.setHandshakeTimeout(mHandshakeTimeoutMillis);
+        s.setChannelIdPrivateKey(mChannelIdPrivateKey);
+        if (mSecure) {
+            verifyHostname(s, host);
+        }
+        return s;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>By default, this method returns a <i>connected</i> socket and verifies the peer's
+     * certificate hostname after connecting using the {@link HostnameVerifier} obtained from
+     * {@code HttpsURLConnection.getDefaultHostnameVerifier()}; if this instance was created with
+     * {@link #getInsecure(int, SSLSessionCache)}, it returns a socket that is <i>not connected</i>
+     * instead.
+     */
+    @Override
+    public Socket createSocket(String host, int port) throws IOException {
+        OpenSSLSocketImpl s = (OpenSSLSocketImpl) getDelegate().createSocket(host, port);
+        s.setNpnProtocols(mNpnProtocols);
+        s.setAlpnProtocols(mAlpnProtocols);
+        s.setHandshakeTimeout(mHandshakeTimeoutMillis);
+        s.setChannelIdPrivateKey(mChannelIdPrivateKey);
+        if (mSecure) {
+            verifyHostname(s, host);
+        }
+        return s;
+    }
+
+    @Override
+    public String[] getDefaultCipherSuites() {
+        return getDelegate().getDefaultCipherSuites();
+    }
+
+    @Override
+    public String[] getSupportedCipherSuites() {
+        return getDelegate().getSupportedCipherSuites();
+    }
+}
diff --git a/android/net/SSLSessionCache.java b/android/net/SSLSessionCache.java
new file mode 100644
index 0000000..9667e82
--- /dev/null
+++ b/android/net/SSLSessionCache.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2010 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.UnsupportedAppUsage;
+import android.content.Context;
+import android.util.Log;
+
+import com.android.org.conscrypt.ClientSessionContext;
+import com.android.org.conscrypt.FileClientSessionCache;
+import com.android.org.conscrypt.SSLClientSessionCache;
+
+import java.io.File;
+import java.io.IOException;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSessionContext;
+
+/**
+ * File-based cache of established SSL sessions.  When re-establishing a
+ * connection to the same server, using an SSL session cache can save some time,
+ * power, and bandwidth by skipping directly to an encrypted stream.
+ * This is a persistent cache which can span executions of the application.
+ *
+ * @see SSLCertificateSocketFactory
+ */
+public final class SSLSessionCache {
+    private static final String TAG = "SSLSessionCache";
+    @UnsupportedAppUsage
+    /* package */ final SSLClientSessionCache mSessionCache;
+
+    /**
+     * Installs a {@link SSLSessionCache} on a {@link SSLContext}. The cache will
+     * be used on all socket factories created by this context (including factories
+     * created before this call).
+     *
+     * @param cache the cache instance to install, or {@code null} to uninstall any
+     *         existing cache.
+     * @param context the context to install it on.
+     * @throws IllegalArgumentException if the context does not support a session
+     *         cache.
+     *
+     * @hide candidate for public API
+     */
+    public static void install(SSLSessionCache cache, SSLContext context) {
+        SSLSessionContext clientContext = context.getClientSessionContext();
+        if (clientContext instanceof ClientSessionContext) {
+            ((ClientSessionContext) clientContext).setPersistentCache(
+                    cache == null ? null : cache.mSessionCache);
+        } else {
+            throw new IllegalArgumentException("Incompatible SSLContext: " + context);
+        }
+    }
+
+    /**
+     * NOTE: This needs to be Object (and not SSLClientSessionCache) because apps
+     * that build directly against the framework (and not the SDK) might not declare
+     * a dependency on conscrypt. Javac will then has fail while resolving constructors.
+     *
+     * @hide For unit test use only
+     */
+    public SSLSessionCache(Object cache) {
+        mSessionCache = (SSLClientSessionCache) cache;
+    }
+
+    /**
+     * Create a session cache using the specified directory.
+     * Individual session entries will be files within the directory.
+     * Multiple instances for the same directory share data internally.
+     *
+     * @param dir to store session files in (created if necessary)
+     * @throws IOException if the cache can't be opened
+     */
+    public SSLSessionCache(File dir) throws IOException {
+        mSessionCache = FileClientSessionCache.usingDirectory(dir);
+    }
+
+    /**
+     * Create a session cache at the default location for this app.
+     * Multiple instances share data internally.
+     *
+     * @param context for the application
+     */
+    public SSLSessionCache(Context context) {
+        File dir = context.getDir("sslcache", Context.MODE_PRIVATE);
+        SSLClientSessionCache cache = null;
+        try {
+            cache = FileClientSessionCache.usingDirectory(dir);
+        } catch (IOException e) {
+            Log.w(TAG, "Unable to create SSL session cache in " + dir, e);
+        }
+        mSessionCache = cache;
+    }
+}
diff --git a/android/net/ScoredNetwork.java b/android/net/ScoredNetwork.java
new file mode 100644
index 0000000..effc1aa
--- /dev/null
+++ b/android/net/ScoredNetwork.java
@@ -0,0 +1,317 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.net;
+
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * A network identifier along with a score for the quality of that network.
+ *
+ * @hide
+ */
+@SystemApi
+public class ScoredNetwork implements Parcelable {
+
+  /**
+     * Key used with the {@link #attributes} bundle to define the badging curve.
+     *
+     * <p>The badging curve is a {@link RssiCurve} used to map different RSSI values to {@link
+     * NetworkBadging.Badging} enums.
+     */
+    public static final String ATTRIBUTES_KEY_BADGING_CURVE =
+            "android.net.attributes.key.BADGING_CURVE";
+    /**
+     * 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 ATTRIBUTES_KEY_HAS_CAPTIVE_PORTAL =
+            "android.net.attributes.key.HAS_CAPTIVE_PORTAL";
+
+    /**
+     * Key used with the {@link #attributes} bundle to define the rankingScoreOffset int value.
+     *
+     * <p>The rankingScoreOffset is used when calculating the ranking score used to rank networks
+     * against one another. See {@link #calculateRankingScore} for more information.
+     */
+    public static final String ATTRIBUTES_KEY_RANKING_SCORE_OFFSET =
+            "android.net.attributes.key.RANKING_SCORE_OFFSET";
+
+    /** A {@link NetworkKey} uniquely identifying this network. */
+    public final NetworkKey networkKey;
+
+    /**
+     * The {@link RssiCurve} representing the scores for this network based on the RSSI.
+     *
+     * <p>This field is optional and may be set to null to indicate that no score is available for
+     * this network at this time. Such networks, along with networks for which the scorer has not
+     * responded, are always prioritized below scored networks, regardless of the score.
+     */
+    public final RssiCurve rssiCurve;
+
+    /**
+     * A boolean value that indicates whether or not the network is believed to be metered.
+     *
+     * <p>A network can be classified as metered if the user would be
+     * sensitive to heavy data usage on that connection due to monetary costs,
+     * data limitations or battery/performance issues. A typical example would
+     * be a wifi connection where the user would be charged for usage.
+     */
+    public final boolean meteredHint;
+
+    /**
+     * An additional collection of optional attributes set by
+     * the Network Recommendation Provider.
+     *
+     * @see #ATTRIBUTES_KEY_HAS_CAPTIVE_PORTAL
+     * @see #ATTRIBUTES_KEY_RANKING_SCORE_OFFSET
+     */
+    @Nullable
+    public final Bundle 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.
+     */
+    public ScoredNetwork(NetworkKey networkKey, RssiCurve rssiCurve) {
+        this(networkKey, rssiCurve, false /* meteredHint */);
+    }
+
+    /**
+     * 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.
+     */
+    public ScoredNetwork(NetworkKey networkKey, RssiCurve rssiCurve, boolean meteredHint) {
+        this(networkKey, rssiCurve, 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,
+            @Nullable Bundle attributes) {
+        this.networkKey = networkKey;
+        this.rssiCurve = rssiCurve;
+        this.meteredHint = meteredHint;
+        this.attributes = attributes;
+    }
+
+    private ScoredNetwork(Parcel in) {
+        networkKey = NetworkKey.CREATOR.createFromParcel(in);
+        if (in.readByte() == 1) {
+            rssiCurve = RssiCurve.CREATOR.createFromParcel(in);
+        } else {
+            rssiCurve = null;
+        }
+        meteredHint = (in.readByte() == 1);
+        attributes = in.readBundle();
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        networkKey.writeToParcel(out, flags);
+        if (rssiCurve != null) {
+            out.writeByte((byte) 1);
+            rssiCurve.writeToParcel(out, flags);
+        } else {
+            out.writeByte((byte) 0);
+        }
+        out.writeByte((byte) (meteredHint ? 1 : 0));
+        out.writeBundle(attributes);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        ScoredNetwork that = (ScoredNetwork) o;
+
+        return Objects.equals(networkKey, that.networkKey)
+                && Objects.equals(rssiCurve, that.rssiCurve)
+                && Objects.equals(meteredHint, that.meteredHint)
+                && bundleEquals(attributes, that.attributes);
+    }
+
+    private boolean bundleEquals(Bundle bundle1, Bundle bundle2) {
+        if (bundle1 == bundle2) {
+            return true;
+        }
+        if (bundle1 == null || bundle2 == null) {
+            return false;
+        }
+        if (bundle1.size() != bundle2.size()) {
+            return false;
+        }
+        Set<String> keys = bundle1.keySet();
+        for (String key : keys) {
+            Object value1 = bundle1.get(key);
+            Object value2 = bundle2.get(key);
+            if (!Objects.equals(value1, value2)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(networkKey, rssiCurve, meteredHint, attributes);
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder out = new StringBuilder(
+                "ScoredNetwork{" +
+                "networkKey=" + networkKey +
+                ", rssiCurve=" + rssiCurve +
+                ", meteredHint=" + meteredHint);
+        // calling isEmpty will unparcel the bundle so its contents can be converted to a string
+        if (attributes != null && !attributes.isEmpty()) {
+            out.append(", attributes=" + attributes);
+        }
+        out.append('}');
+        return out.toString();
+    }
+
+    /**
+     * Returns true if a ranking score can be calculated for this network.
+     *
+     * @hide
+     */
+    public boolean hasRankingScore() {
+        return (rssiCurve != null)
+                || (attributes != null
+                        && attributes.containsKey(ATTRIBUTES_KEY_RANKING_SCORE_OFFSET));
+    }
+
+    /**
+     * Returns a ranking score for a given RSSI which can be used to comparatively
+     * rank networks.
+     *
+     * <p>The score obtained by the rssiCurve is bitshifted left by 8 bits to expand it to an
+     * integer and then the offset is added. If the addition operation overflows or underflows,
+     * Integer.MAX_VALUE and Integer.MIN_VALUE will be returned respectively.
+     *
+     * <p>{@link #hasRankingScore} should be called first to ensure this network is capable
+     * of returning a ranking score.
+     *
+     * @throws UnsupportedOperationException if there is no RssiCurve and no rankingScoreOffset
+     * for this network (hasRankingScore returns false).
+     *
+     * @hide
+     */
+    public int calculateRankingScore(int rssi) throws UnsupportedOperationException {
+        if (!hasRankingScore()) {
+            throw new UnsupportedOperationException(
+                    "Either rssiCurve or rankingScoreOffset is required to calculate the "
+                            + "ranking score");
+        }
+
+        int offset = 0;
+        if (attributes != null) {
+             offset += attributes.getInt(ATTRIBUTES_KEY_RANKING_SCORE_OFFSET, 0 /* default */);
+        }
+
+        int score = (rssiCurve == null) ? 0 : rssiCurve.lookupScore(rssi) << Byte.SIZE;
+
+        try {
+            return Math.addExact(score, offset);
+        } catch (ArithmeticException e) {
+            return (score < 0) ? Integer.MIN_VALUE : Integer.MAX_VALUE;
+        }
+    }
+
+    /**
+     * Return the {@link NetworkBadging.Badging} enum for this network for the given RSSI, derived from the
+     * badging curve.
+     *
+     * <p>If no badging curve is present, {@link #BADGE_NONE} will be returned.
+     *
+     * @param rssi The rssi level for which the badge should be calculated
+     */
+    @NetworkBadging.Badging
+    public int calculateBadge(int rssi) {
+        if (attributes != null && attributes.containsKey(ATTRIBUTES_KEY_BADGING_CURVE)) {
+            RssiCurve badgingCurve =
+                    attributes.getParcelable(ATTRIBUTES_KEY_BADGING_CURVE);
+            return badgingCurve.lookupScore(rssi);
+        }
+
+        return NetworkBadging.BADGING_NONE;
+    }
+
+    public static final @android.annotation.NonNull Parcelable.Creator<ScoredNetwork> CREATOR =
+            new Parcelable.Creator<ScoredNetwork>() {
+                @Override
+                public ScoredNetwork createFromParcel(Parcel in) {
+                    return new ScoredNetwork(in);
+                }
+
+                @Override
+                public ScoredNetwork[] newArray(int size) {
+                    return new ScoredNetwork[size];
+                }
+            };
+}
diff --git a/android/net/SntpClient.java b/android/net/SntpClient.java
new file mode 100644
index 0000000..a55d9d0
--- /dev/null
+++ b/android/net/SntpClient.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright (C) 2008 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.UnsupportedAppUsage;
+import android.os.SystemClock;
+import android.util.Log;
+
+import com.android.internal.util.TrafficStatsConstants;
+
+import java.net.DatagramPacket;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.util.Arrays;
+
+/**
+ * {@hide}
+ *
+ * Simple SNTP client class for retrieving network time.
+ *
+ * Sample usage:
+ * <pre>SntpClient client = new SntpClient();
+ * if (client.requestTime("time.foo.com")) {
+ *     long now = client.getNtpTime() + SystemClock.elapsedRealtime() - client.getNtpTimeReference();
+ * }
+ * </pre>
+ */
+public class SntpClient {
+    private static final String TAG = "SntpClient";
+    private static final boolean DBG = true;
+
+    private static final int REFERENCE_TIME_OFFSET = 16;
+    private static final int ORIGINATE_TIME_OFFSET = 24;
+    private static final int RECEIVE_TIME_OFFSET = 32;
+    private static final int TRANSMIT_TIME_OFFSET = 40;
+    private static final int NTP_PACKET_SIZE = 48;
+
+    private static final int NTP_PORT = 123;
+    private static final int NTP_MODE_CLIENT = 3;
+    private static final int NTP_MODE_SERVER = 4;
+    private static final int NTP_MODE_BROADCAST = 5;
+    private static final int NTP_VERSION = 3;
+
+    private static final int NTP_LEAP_NOSYNC = 3;
+    private static final int NTP_STRATUM_DEATH = 0;
+    private static final int NTP_STRATUM_MAX = 15;
+
+    // Number of seconds between Jan 1, 1900 and Jan 1, 1970
+    // 70 years plus 17 leap days
+    private static final long OFFSET_1900_TO_1970 = ((365L * 70L) + 17L) * 24L * 60L * 60L;
+
+    // system time computed from NTP server response
+    private long mNtpTime;
+
+    // value of SystemClock.elapsedRealtime() corresponding to mNtpTime
+    private long mNtpTimeReference;
+
+    // round trip time in milliseconds
+    private long mRoundTripTime;
+
+    private static class InvalidServerReplyException extends Exception {
+        public InvalidServerReplyException(String message) {
+            super(message);
+        }
+    }
+
+    /**
+     * Sends an SNTP request to the given host and processes the response.
+     *
+     * @param host host name of the server.
+     * @param timeout network timeout in milliseconds.
+     * @param network network over which to send the request.
+     * @return true if the transaction was successful.
+     */
+    public boolean requestTime(String host, int timeout, Network network) {
+        final Network networkForResolv = network.getPrivateDnsBypassingCopy();
+        InetAddress address = null;
+        try {
+            address = networkForResolv.getByName(host);
+        } catch (Exception e) {
+            EventLogTags.writeNtpFailure(host, e.toString());
+            if (DBG) Log.d(TAG, "request time failed: " + e);
+            return false;
+        }
+        return requestTime(address, NTP_PORT, timeout, networkForResolv);
+    }
+
+    public boolean requestTime(InetAddress address, int port, int timeout, Network network) {
+        DatagramSocket socket = null;
+        final int oldTag = TrafficStats.getAndSetThreadStatsTag(
+                TrafficStatsConstants.TAG_SYSTEM_NTP);
+        try {
+            socket = new DatagramSocket();
+            network.bindSocket(socket);
+            socket.setSoTimeout(timeout);
+            byte[] buffer = new byte[NTP_PACKET_SIZE];
+            DatagramPacket request = new DatagramPacket(buffer, buffer.length, address, port);
+
+            // set mode = 3 (client) and version = 3
+            // mode is in low 3 bits of first byte
+            // version is in bits 3-5 of first byte
+            buffer[0] = NTP_MODE_CLIENT | (NTP_VERSION << 3);
+
+            // get current time and write it to the request packet
+            final long requestTime = System.currentTimeMillis();
+            final long requestTicks = SystemClock.elapsedRealtime();
+            writeTimeStamp(buffer, TRANSMIT_TIME_OFFSET, requestTime);
+
+            socket.send(request);
+
+            // read the response
+            DatagramPacket response = new DatagramPacket(buffer, buffer.length);
+            socket.receive(response);
+            final long responseTicks = SystemClock.elapsedRealtime();
+            final long responseTime = requestTime + (responseTicks - requestTicks);
+
+            // extract the results
+            final byte leap = (byte) ((buffer[0] >> 6) & 0x3);
+            final byte mode = (byte) (buffer[0] & 0x7);
+            final int stratum = (int) (buffer[1] & 0xff);
+            final long originateTime = readTimeStamp(buffer, ORIGINATE_TIME_OFFSET);
+            final long receiveTime = readTimeStamp(buffer, RECEIVE_TIME_OFFSET);
+            final long transmitTime = readTimeStamp(buffer, TRANSMIT_TIME_OFFSET);
+
+            /* do sanity check according to RFC */
+            // TODO: validate originateTime == requestTime.
+            checkValidServerReply(leap, mode, stratum, transmitTime);
+
+            long roundTripTime = responseTicks - requestTicks - (transmitTime - receiveTime);
+            // receiveTime = originateTime + transit + skew
+            // responseTime = transmitTime + transit - skew
+            // clockOffset = ((receiveTime - originateTime) + (transmitTime - responseTime))/2
+            //             = ((originateTime + transit + skew - originateTime) +
+            //                (transmitTime - (transmitTime + transit - skew)))/2
+            //             = ((transit + skew) + (transmitTime - transmitTime - transit + skew))/2
+            //             = (transit + skew - transit + skew)/2
+            //             = (2 * skew)/2 = skew
+            long clockOffset = ((receiveTime - originateTime) + (transmitTime - responseTime))/2;
+            EventLogTags.writeNtpSuccess(address.toString(), roundTripTime, clockOffset);
+            if (DBG) {
+                Log.d(TAG, "round trip: " + roundTripTime + "ms, " +
+                        "clock offset: " + clockOffset + "ms");
+            }
+
+            // save our results - use the times on this side of the network latency
+            // (response rather than request time)
+            mNtpTime = responseTime + clockOffset;
+            mNtpTimeReference = responseTicks;
+            mRoundTripTime = roundTripTime;
+        } catch (Exception e) {
+            EventLogTags.writeNtpFailure(address.toString(), e.toString());
+            if (DBG) Log.d(TAG, "request time failed: " + e);
+            return false;
+        } finally {
+            if (socket != null) {
+                socket.close();
+            }
+            TrafficStats.setThreadStatsTag(oldTag);
+        }
+
+        return true;
+    }
+
+    @Deprecated
+    @UnsupportedAppUsage
+    public boolean requestTime(String host, int timeout) {
+        Log.w(TAG, "Shame on you for calling the hidden API requestTime()!");
+        return false;
+    }
+
+    /**
+     * Returns the time computed from the NTP transaction.
+     *
+     * @return time value computed from NTP server response.
+     */
+    @UnsupportedAppUsage
+    public long getNtpTime() {
+        return mNtpTime;
+    }
+
+    /**
+     * Returns the reference clock value (value of SystemClock.elapsedRealtime())
+     * corresponding to the NTP time.
+     *
+     * @return reference clock corresponding to the NTP time.
+     */
+    @UnsupportedAppUsage
+    public long getNtpTimeReference() {
+        return mNtpTimeReference;
+    }
+
+    /**
+     * Returns the round trip time of the NTP transaction
+     *
+     * @return round trip time in milliseconds.
+     */
+    @UnsupportedAppUsage
+    public long getRoundTripTime() {
+        return mRoundTripTime;
+    }
+
+    private static void checkValidServerReply(
+            byte leap, byte mode, int stratum, long transmitTime)
+            throws InvalidServerReplyException {
+        if (leap == NTP_LEAP_NOSYNC) {
+            throw new InvalidServerReplyException("unsynchronized server");
+        }
+        if ((mode != NTP_MODE_SERVER) && (mode != NTP_MODE_BROADCAST)) {
+            throw new InvalidServerReplyException("untrusted mode: " + mode);
+        }
+        if ((stratum == NTP_STRATUM_DEATH) || (stratum > NTP_STRATUM_MAX)) {
+            throw new InvalidServerReplyException("untrusted stratum: " + stratum);
+        }
+        if (transmitTime == 0) {
+            throw new InvalidServerReplyException("zero transmitTime");
+        }
+    }
+
+    /**
+     * Reads an unsigned 32 bit big endian number from the given offset in the buffer.
+     */
+    private long read32(byte[] buffer, int offset) {
+        byte b0 = buffer[offset];
+        byte b1 = buffer[offset+1];
+        byte b2 = buffer[offset+2];
+        byte b3 = buffer[offset+3];
+
+        // convert signed bytes to unsigned values
+        int i0 = ((b0 & 0x80) == 0x80 ? (b0 & 0x7F) + 0x80 : b0);
+        int i1 = ((b1 & 0x80) == 0x80 ? (b1 & 0x7F) + 0x80 : b1);
+        int i2 = ((b2 & 0x80) == 0x80 ? (b2 & 0x7F) + 0x80 : b2);
+        int i3 = ((b3 & 0x80) == 0x80 ? (b3 & 0x7F) + 0x80 : b3);
+
+        return ((long)i0 << 24) + ((long)i1 << 16) + ((long)i2 << 8) + (long)i3;
+    }
+
+    /**
+     * Reads the NTP time stamp at the given offset in the buffer and returns
+     * it as a system time (milliseconds since January 1, 1970).
+     */
+    private long readTimeStamp(byte[] buffer, int offset) {
+        long seconds = read32(buffer, offset);
+        long fraction = read32(buffer, offset + 4);
+        // Special case: zero means zero.
+        if (seconds == 0 && fraction == 0) {
+            return 0;
+        }
+        return ((seconds - OFFSET_1900_TO_1970) * 1000) + ((fraction * 1000L) / 0x100000000L);
+    }
+
+    /**
+     * Writes system time (milliseconds since January 1, 1970) as an NTP time stamp
+     * at the given offset in the buffer.
+     */
+    private void writeTimeStamp(byte[] buffer, int offset, long time) {
+        // Special case: zero means zero.
+        if (time == 0) {
+            Arrays.fill(buffer, offset, offset + 8, (byte) 0x00);
+            return;
+        }
+
+        long seconds = time / 1000L;
+        long milliseconds = time - seconds * 1000L;
+        seconds += OFFSET_1900_TO_1970;
+
+        // write seconds in big endian format
+        buffer[offset++] = (byte)(seconds >> 24);
+        buffer[offset++] = (byte)(seconds >> 16);
+        buffer[offset++] = (byte)(seconds >> 8);
+        buffer[offset++] = (byte)(seconds >> 0);
+
+        long fraction = milliseconds * 0x100000000L / 1000L;
+        // write fraction in big endian format
+        buffer[offset++] = (byte)(fraction >> 24);
+        buffer[offset++] = (byte)(fraction >> 16);
+        buffer[offset++] = (byte)(fraction >> 8);
+        // low order bits should be random data
+        buffer[offset++] = (byte)(Math.random() * 255.0);
+    }
+}
diff --git a/android/net/SocketKeepalive.java b/android/net/SocketKeepalive.java
new file mode 100644
index 0000000..ec73866
--- /dev/null
+++ b/android/net/SocketKeepalive.java
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2019 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.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Binder;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.concurrent.Executor;
+
+/**
+ * Allows applications to request that the system periodically send specific packets on their
+ * behalf, using hardware offload to save battery power.
+ *
+ * To request that the system send keepalives, call one of the methods that return a
+ * {@link SocketKeepalive} object, such as {@link ConnectivityManager#createSocketKeepalive},
+ * passing in a non-null callback. If the {@link SocketKeepalive} is successfully
+ * started, the callback's {@code onStarted} method will be called. If an error occurs,
+ * {@code onError} will be called, specifying one of the {@code ERROR_*} constants in this
+ * class.
+ *
+ * To stop an existing keepalive, call {@link SocketKeepalive#stop}. The system will call
+ * {@link SocketKeepalive.Callback#onStopped} if the operation was successful or
+ * {@link SocketKeepalive.Callback#onError} if an error occurred.
+ *
+ * For cellular, the device MUST support at least 1 keepalive slot.
+ *
+ * For WiFi, the device SHOULD support keepalive offload. If it does not, it MUST reply with
+ * {@link SocketKeepalive.Callback#onError} with {@code ERROR_UNSUPPORTED} to any keepalive offload
+ * request. If it does, it MUST support at least 3 concurrent keepalive slots.
+ */
+public abstract class SocketKeepalive implements AutoCloseable {
+    static final String TAG = "SocketKeepalive";
+
+    /** @hide */
+    public static final int SUCCESS = 0;
+
+    /** @hide */
+    public static final int NO_KEEPALIVE = -1;
+
+    /** @hide */
+    public static final int DATA_RECEIVED = -2;
+
+    /** @hide */
+    public static final int BINDER_DIED = -10;
+
+    /** The specified {@code Network} is not connected. */
+    public static final int ERROR_INVALID_NETWORK = -20;
+    /** The specified IP addresses are invalid. For example, the specified source IP address is
+     * not configured on the specified {@code Network}. */
+    public static final int ERROR_INVALID_IP_ADDRESS = -21;
+    /** The requested port is invalid. */
+    public static final int ERROR_INVALID_PORT = -22;
+    /** The packet length is invalid (e.g., too long). */
+    public static final int ERROR_INVALID_LENGTH = -23;
+    /** The packet transmission interval is invalid (e.g., too short). */
+    public static final int ERROR_INVALID_INTERVAL = -24;
+    /** The target socket is invalid. */
+    public static final int ERROR_INVALID_SOCKET = -25;
+    /** The target socket is not idle. */
+    public static final int ERROR_SOCKET_NOT_IDLE = -26;
+
+    /** The device does not support this request. */
+    public static final int ERROR_UNSUPPORTED = -30;
+    /** @hide TODO: delete when telephony code has been updated. */
+    public static final int ERROR_HARDWARE_UNSUPPORTED = ERROR_UNSUPPORTED;
+    /** The hardware returned an error. */
+    public static final int ERROR_HARDWARE_ERROR = -31;
+    /** The limitation of resource is reached. */
+    public static final int ERROR_INSUFFICIENT_RESOURCES = -32;
+
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = { "ERROR_" }, value = {
+            ERROR_INVALID_NETWORK,
+            ERROR_INVALID_IP_ADDRESS,
+            ERROR_INVALID_PORT,
+            ERROR_INVALID_LENGTH,
+            ERROR_INVALID_INTERVAL,
+            ERROR_INVALID_SOCKET,
+            ERROR_SOCKET_NOT_IDLE
+    })
+    public @interface ErrorCode {}
+
+    /**
+     * The minimum interval in seconds between keepalive packet transmissions.
+     *
+     * @hide
+     **/
+    public static final int MIN_INTERVAL_SEC = 10;
+
+    /**
+     * The maximum interval in seconds between keepalive packet transmissions.
+     *
+     * @hide
+     **/
+    public static final int MAX_INTERVAL_SEC = 3600;
+
+    /**
+     * An exception that embarks an error code.
+     * @hide
+     */
+    public static class ErrorCodeException extends Exception {
+        public final int error;
+        public ErrorCodeException(final int error, final Throwable e) {
+            super(e);
+            this.error = error;
+        }
+        public ErrorCodeException(final int error) {
+            this.error = error;
+        }
+    }
+
+    /**
+     * This socket is invalid.
+     * See the error code for details, and the optional cause.
+     * @hide
+     */
+    public static class InvalidSocketException extends ErrorCodeException {
+        public InvalidSocketException(final int error, final Throwable e) {
+            super(error, e);
+        }
+        public InvalidSocketException(final int error) {
+            super(error);
+        }
+    }
+
+    /**
+     * This packet is invalid.
+     * See the error code for details.
+     * @hide
+     */
+    public static class InvalidPacketException extends ErrorCodeException {
+        public InvalidPacketException(final int error) {
+            super(error);
+        }
+    }
+
+    @NonNull final IConnectivityManager mService;
+    @NonNull final Network mNetwork;
+    @NonNull final ParcelFileDescriptor mPfd;
+    @NonNull final Executor mExecutor;
+    @NonNull final ISocketKeepaliveCallback mCallback;
+    // TODO: remove slot since mCallback could be used to identify which keepalive to stop.
+    @Nullable Integer mSlot;
+
+    SocketKeepalive(@NonNull IConnectivityManager service, @NonNull Network network,
+            @NonNull ParcelFileDescriptor pfd,
+            @NonNull Executor executor, @NonNull Callback callback) {
+        mService = service;
+        mNetwork = network;
+        mPfd = pfd;
+        mExecutor = executor;
+        mCallback = new ISocketKeepaliveCallback.Stub() {
+            @Override
+            public void onStarted(int slot) {
+                Binder.withCleanCallingIdentity(() ->
+                        mExecutor.execute(() -> {
+                            mSlot = slot;
+                            callback.onStarted();
+                        }));
+            }
+
+            @Override
+            public void onStopped() {
+                Binder.withCleanCallingIdentity(() ->
+                        executor.execute(() -> {
+                            mSlot = null;
+                            callback.onStopped();
+                        }));
+            }
+
+            @Override
+            public void onError(int error) {
+                Binder.withCleanCallingIdentity(() ->
+                        executor.execute(() -> {
+                            mSlot = null;
+                            callback.onError(error);
+                        }));
+            }
+
+            @Override
+            public void onDataReceived() {
+                Binder.withCleanCallingIdentity(() ->
+                        executor.execute(() -> {
+                            mSlot = null;
+                            callback.onDataReceived();
+                        }));
+            }
+        };
+    }
+
+    /**
+     * Request that keepalive be started with the given {@code intervalSec}. See
+     * {@link SocketKeepalive}. If the remote binder dies, or the binder call throws an exception
+     * when invoking start or stop of the {@link SocketKeepalive}, a {@link RemoteException} will be
+     * thrown into the {@code executor}. This is typically not important to catch because the remote
+     * party is the system, so if it is not in shape to communicate through binder the system is
+     * probably going down anyway. If the caller cares regardless, it can use a custom
+     * {@link Executor} to catch the {@link RemoteException}.
+     *
+     * @param intervalSec The target interval in seconds between keepalive packet transmissions.
+     *                    The interval should be between 10 seconds and 3600 seconds, otherwise
+     *                    {@link #ERROR_INVALID_INTERVAL} will be returned.
+     */
+    public final void start(@IntRange(from = MIN_INTERVAL_SEC, to = MAX_INTERVAL_SEC)
+            int intervalSec) {
+        startImpl(intervalSec);
+    }
+
+    abstract void startImpl(int intervalSec);
+
+    /**
+     * Requests that keepalive be stopped. The application must wait for {@link Callback#onStopped}
+     * before using the object. See {@link SocketKeepalive}.
+     */
+    public final void stop() {
+        stopImpl();
+    }
+
+    abstract void stopImpl();
+
+    /**
+     * Deactivate this {@link SocketKeepalive} and free allocated resources. The instance won't be
+     * usable again if {@code close()} is called.
+     */
+    @Override
+    public final void close() {
+        stop();
+        try {
+            mPfd.close();
+        } catch (IOException e) {
+            // Nothing much can be done.
+        }
+    }
+
+    /**
+     * The callback which app can use to learn the status changes of {@link SocketKeepalive}. See
+     * {@link SocketKeepalive}.
+     */
+    public static class Callback {
+        /** The requested keepalive was successfully started. */
+        public void onStarted() {}
+        /** The keepalive was successfully stopped. */
+        public void onStopped() {}
+        /** An error occurred. */
+        public void onError(@ErrorCode int error) {}
+        /** The keepalive on a TCP socket was stopped because the socket received data. This is
+         * never called for UDP sockets. */
+        public void onDataReceived() {}
+    }
+}
diff --git a/android/net/StaticIpConfiguration.java b/android/net/StaticIpConfiguration.java
new file mode 100644
index 0000000..050fcfa
--- /dev/null
+++ b/android/net/StaticIpConfiguration.java
@@ -0,0 +1,327 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.annotation.UnsupportedAppUsage;
+import android.net.shared.InetAddressUtils;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Class that describes static IP configuration.
+ *
+ * <p>This class is different from {@link LinkProperties} because it represents
+ * configuration intent. The general contract is that if we can represent
+ * a configuration here, then we should be able to configure it on a network.
+ * The intent is that it closely match the UI we have for configuring networks.
+ *
+ * <p>In contrast, {@link LinkProperties} represents current state. It is much more
+ * expressive. For example, it supports multiple IP addresses, multiple routes,
+ * stacked interfaces, and so on. Because LinkProperties is so expressive,
+ * using it to represent configuration intent as well as current state causes
+ * problems. For example, we could unknowingly save a configuration that we are
+ * not in fact capable of applying, or we could save a configuration that the
+ * UI cannot display, which has the potential for malicious code to hide
+ * hostile or unexpected configuration from the user.
+ *
+ * @hide
+ */
+@SystemApi
+@TestApi
+public final class StaticIpConfiguration implements Parcelable {
+    /** @hide */
+    @UnsupportedAppUsage
+    @Nullable
+    public LinkAddress ipAddress;
+    /** @hide */
+    @UnsupportedAppUsage
+    @Nullable
+    public InetAddress gateway;
+    /** @hide */
+    @UnsupportedAppUsage
+    @NonNull
+    public final ArrayList<InetAddress> dnsServers;
+    /** @hide */
+    @UnsupportedAppUsage
+    @Nullable
+    public String domains;
+
+    public StaticIpConfiguration() {
+        dnsServers = new ArrayList<>();
+    }
+
+    public StaticIpConfiguration(@Nullable StaticIpConfiguration source) {
+        this();
+        if (source != null) {
+            // All of these except dnsServers are immutable, so no need to make copies.
+            ipAddress = source.ipAddress;
+            gateway = source.gateway;
+            dnsServers.addAll(source.dnsServers);
+            domains = source.domains;
+        }
+    }
+
+    public void clear() {
+        ipAddress = null;
+        gateway = null;
+        dnsServers.clear();
+        domains = null;
+    }
+
+    /**
+     * Get the static IP address included in the configuration.
+     */
+    public @Nullable LinkAddress getIpAddress() {
+        return ipAddress;
+    }
+
+    /**
+     * Get the gateway included in the configuration.
+     */
+    public @Nullable InetAddress getGateway() {
+        return gateway;
+    }
+
+    /**
+     * Get the DNS servers included in the configuration.
+     */
+    public @NonNull List<InetAddress> getDnsServers() {
+        return dnsServers;
+    }
+
+    /**
+     * Get a {@link String} listing in priority order of the comma separated domains to search when
+     * resolving host names on the link.
+     */
+    public @Nullable String getDomains() {
+        return domains;
+    }
+
+    /**
+     * Helper class to build a new instance of {@link StaticIpConfiguration}.
+     */
+    public static final class Builder {
+        private LinkAddress mIpAddress;
+        private InetAddress mGateway;
+        private Iterable<InetAddress> mDnsServers;
+        private String mDomains;
+
+        /**
+         * Set the IP address to be included in the configuration; null by default.
+         * @return The {@link Builder} for chaining.
+         */
+        public @NonNull Builder setIpAddress(@Nullable LinkAddress ipAddress) {
+            mIpAddress = ipAddress;
+            return this;
+        }
+
+        /**
+         * Set the address of the gateway to be included in the configuration; null by default.
+         * @return The {@link Builder} for chaining.
+         */
+        public @NonNull Builder setGateway(@Nullable InetAddress gateway) {
+            mGateway = gateway;
+            return this;
+        }
+
+        /**
+         * Set the addresses of the DNS servers included in the configuration; empty by default.
+         * @return The {@link Builder} for chaining.
+         */
+        public @NonNull Builder setDnsServers(@NonNull Iterable<InetAddress> dnsServers) {
+            mDnsServers = dnsServers;
+            return this;
+        }
+
+        /**
+         * Sets the DNS domain search path to be used on the link; null by default.
+         * @param newDomains A {@link String} containing the comma separated domains to search when
+         *                   resolving host names on this link, in priority order.
+         * @return The {@link Builder} for chaining.
+         */
+        public @NonNull Builder setDomains(@Nullable String newDomains) {
+            mDomains = newDomains;
+            return this;
+        }
+
+        /**
+         * Create a {@link StaticIpConfiguration} from the parameters in this {@link Builder}.
+         * @return The newly created StaticIpConfiguration.
+         */
+        public @NonNull StaticIpConfiguration build() {
+            final StaticIpConfiguration config = new StaticIpConfiguration();
+            config.ipAddress = mIpAddress;
+            config.gateway = mGateway;
+            for (InetAddress server : mDnsServers) {
+                config.dnsServers.add(server);
+            }
+            config.domains = mDomains;
+            return config;
+        }
+    }
+
+    /**
+     * Add a DNS server to this configuration.
+     */
+    public void addDnsServer(@NonNull InetAddress server) {
+        dnsServers.add(server);
+    }
+
+    /**
+     * Returns the network routes specified by this object. Will typically include a
+     * directly-connected route for the IP address's local subnet and a default route.
+     * @param iface Interface to include in the routes.
+     */
+    public @NonNull List<RouteInfo> getRoutes(@Nullable String iface) {
+        List<RouteInfo> routes = new ArrayList<RouteInfo>(3);
+        if (ipAddress != null) {
+            RouteInfo connectedRoute = new RouteInfo(ipAddress, null, iface);
+            routes.add(connectedRoute);
+            // If the default gateway is not covered by the directly-connected route, also add a
+            // host route to the gateway as well. This configuration is arguably invalid, but it
+            // used to work in K and earlier, and other OSes appear to accept it.
+            if (gateway != null && !connectedRoute.matches(gateway)) {
+                routes.add(RouteInfo.makeHostRoute(gateway, iface));
+            }
+        }
+        if (gateway != null) {
+            routes.add(new RouteInfo((IpPrefix) null, gateway, iface));
+        }
+        return routes;
+    }
+
+    /**
+     * Returns a LinkProperties object expressing the data in this object. Note that the information
+     * contained in the LinkProperties will not be a complete picture of the link's configuration,
+     * because any configuration information that is obtained dynamically by the network (e.g.,
+     * IPv6 configuration) will not be included.
+     * @hide
+     */
+    public @NonNull LinkProperties toLinkProperties(String iface) {
+        LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName(iface);
+        if (ipAddress != null) {
+            lp.addLinkAddress(ipAddress);
+        }
+        for (RouteInfo route : getRoutes(iface)) {
+            lp.addRoute(route);
+        }
+        for (InetAddress dns : dnsServers) {
+            lp.addDnsServer(dns);
+        }
+        lp.setDomains(domains);
+        return lp;
+    }
+
+    @Override
+    public String toString() {
+        StringBuffer str = new StringBuffer();
+
+        str.append("IP address ");
+        if (ipAddress != null ) str.append(ipAddress).append(" ");
+
+        str.append("Gateway ");
+        if (gateway != null) str.append(gateway.getHostAddress()).append(" ");
+
+        str.append(" DNS servers: [");
+        for (InetAddress dnsServer : dnsServers) {
+            str.append(" ").append(dnsServer.getHostAddress());
+        }
+
+        str.append(" ] Domains ");
+        if (domains != null) str.append(domains);
+        return str.toString();
+    }
+
+    @Override
+    public int hashCode() {
+        int result = 13;
+        result = 47 * result + (ipAddress == null ? 0 : ipAddress.hashCode());
+        result = 47 * result + (gateway == null ? 0 : gateway.hashCode());
+        result = 47 * result + (domains == null ? 0 : domains.hashCode());
+        result = 47 * result + dnsServers.hashCode();
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) return true;
+
+        if (!(obj instanceof StaticIpConfiguration)) return false;
+
+        StaticIpConfiguration other = (StaticIpConfiguration) obj;
+
+        return other != null &&
+                Objects.equals(ipAddress, other.ipAddress) &&
+                Objects.equals(gateway, other.gateway) &&
+                dnsServers.equals(other.dnsServers) &&
+                Objects.equals(domains, other.domains);
+    }
+
+    /** Implement the Parcelable interface */
+    public static final @android.annotation.NonNull Creator<StaticIpConfiguration> CREATOR =
+        new Creator<StaticIpConfiguration>() {
+            public StaticIpConfiguration createFromParcel(Parcel in) {
+                return readFromParcel(in);
+            }
+
+            public StaticIpConfiguration[] newArray(int size) {
+                return new StaticIpConfiguration[size];
+            }
+        };
+
+    /** Implement the Parcelable interface */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Implement the Parcelable interface */
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeParcelable(ipAddress, flags);
+        InetAddressUtils.parcelInetAddress(dest, gateway, flags);
+        dest.writeInt(dnsServers.size());
+        for (InetAddress dnsServer : dnsServers) {
+            InetAddressUtils.parcelInetAddress(dest, dnsServer, flags);
+        }
+        dest.writeString(domains);
+    }
+
+    /** @hide */
+    public static StaticIpConfiguration readFromParcel(Parcel in) {
+        final StaticIpConfiguration s = new StaticIpConfiguration();
+        s.ipAddress = in.readParcelable(null);
+        s.gateway = InetAddressUtils.unparcelInetAddress(in);
+        s.dnsServers.clear();
+        int size = in.readInt();
+        for (int i = 0; i < size; i++) {
+            s.dnsServers.add(InetAddressUtils.unparcelInetAddress(in));
+        }
+        s.domains = in.readString();
+        return s;
+    }
+}
diff --git a/android/net/StringNetworkSpecifier.java b/android/net/StringNetworkSpecifier.java
new file mode 100644
index 0000000..21dee55
--- /dev/null
+++ b/android/net/StringNetworkSpecifier.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.annotation.UnsupportedAppUsage;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.Objects;
+
+/** @hide */
+public final class StringNetworkSpecifier extends NetworkSpecifier implements Parcelable {
+    /**
+     * Arbitrary string used to pass (additional) information to the network factory.
+     */
+    @UnsupportedAppUsage
+    public final String specifier;
+
+    public StringNetworkSpecifier(String specifier) {
+        Preconditions.checkStringNotEmpty(specifier);
+        this.specifier = specifier;
+    }
+
+    @Override
+    public boolean satisfiedBy(NetworkSpecifier other) {
+        return equals(other);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (!(o instanceof StringNetworkSpecifier)) return false;
+        return TextUtils.equals(specifier, ((StringNetworkSpecifier) o).specifier);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(specifier);
+    }
+
+    @Override
+    public String toString() {
+        return specifier;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeString(specifier);
+    }
+
+    public static final @android.annotation.NonNull Parcelable.Creator<StringNetworkSpecifier> CREATOR =
+            new Parcelable.Creator<StringNetworkSpecifier>() {
+        public StringNetworkSpecifier createFromParcel(Parcel in) {
+            return new StringNetworkSpecifier(in.readString());
+        }
+        public StringNetworkSpecifier[] newArray(int size) {
+            return new StringNetworkSpecifier[size];
+        }
+    };
+}
diff --git a/android/net/TcpKeepalivePacketData.java b/android/net/TcpKeepalivePacketData.java
new file mode 100644
index 0000000..7f2f499
--- /dev/null
+++ b/android/net/TcpKeepalivePacketData.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2019 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 static android.net.SocketKeepalive.ERROR_INVALID_IP_ADDRESS;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.SocketKeepalive.InvalidPacketException;
+import android.net.util.IpUtils;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.system.OsConstants;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.Objects;
+
+/**
+ * Represents the actual tcp keep alive packets which will be used for hardware offload.
+ * @hide
+ */
+public class TcpKeepalivePacketData extends KeepalivePacketData implements Parcelable {
+    private static final String TAG = "TcpKeepalivePacketData";
+
+    /** TCP sequence number. */
+    public final int tcpSeq;
+
+    /** TCP ACK number. */
+    public final int tcpAck;
+
+    /** TCP RCV window. */
+    public final int tcpWnd;
+
+    /** TCP RCV window scale. */
+    public final int tcpWndScale;
+
+    /** IP TOS. */
+    public final int ipTos;
+
+    /** IP TTL. */
+    public final int ipTtl;
+
+    private static final int IPV4_HEADER_LENGTH = 20;
+    private static final int IPV6_HEADER_LENGTH = 40;
+    private static final int TCP_HEADER_LENGTH = 20;
+
+    // This should only be constructed via static factory methods, such as
+    // tcpKeepalivePacket.
+    private TcpKeepalivePacketData(final TcpKeepalivePacketDataParcelable tcpDetails,
+            final byte[] data) throws InvalidPacketException, UnknownHostException {
+        super(InetAddress.getByAddress(tcpDetails.srcAddress), tcpDetails.srcPort,
+                InetAddress.getByAddress(tcpDetails.dstAddress), tcpDetails.dstPort, data);
+        tcpSeq = tcpDetails.seq;
+        tcpAck = tcpDetails.ack;
+        // In the packet, the window is shifted right by the window scale.
+        tcpWnd = tcpDetails.rcvWnd;
+        tcpWndScale = tcpDetails.rcvWndScale;
+        ipTos = tcpDetails.tos;
+        ipTtl = tcpDetails.ttl;
+    }
+
+    /**
+     * Factory method to create tcp keepalive packet structure.
+     */
+    public static TcpKeepalivePacketData tcpKeepalivePacket(
+            TcpKeepalivePacketDataParcelable tcpDetails) throws InvalidPacketException {
+        final byte[] packet;
+        try {
+            if ((tcpDetails.srcAddress != null) && (tcpDetails.dstAddress != null)
+                    && (tcpDetails.srcAddress.length == 4 /* V4 IP length */)
+                    && (tcpDetails.dstAddress.length == 4 /* V4 IP length */)) {
+                packet = buildV4Packet(tcpDetails);
+            } else {
+                // TODO: support ipv6
+                throw new InvalidPacketException(ERROR_INVALID_IP_ADDRESS);
+            }
+            return new TcpKeepalivePacketData(tcpDetails, packet);
+        } catch (UnknownHostException e) {
+            throw new InvalidPacketException(ERROR_INVALID_IP_ADDRESS);
+        }
+
+    }
+
+    /**
+     * Build ipv4 tcp keepalive packet, not including the link-layer header.
+     */
+    // TODO : if this code is ever moved to the network stack, factorize constants with the ones
+    // over there.
+    private static byte[] buildV4Packet(TcpKeepalivePacketDataParcelable tcpDetails) {
+        final int length = IPV4_HEADER_LENGTH + TCP_HEADER_LENGTH;
+        ByteBuffer buf = ByteBuffer.allocate(length);
+        buf.order(ByteOrder.BIG_ENDIAN);
+        buf.put((byte) 0x45);                       // IP version and IHL
+        buf.put((byte) tcpDetails.tos);             // TOS
+        buf.putShort((short) length);
+        buf.putInt(0x00004000);                     // ID, flags=DF, offset
+        buf.put((byte) tcpDetails.ttl);             // TTL
+        buf.put((byte) OsConstants.IPPROTO_TCP);
+        final int ipChecksumOffset = buf.position();
+        buf.putShort((short) 0);                    // IP checksum
+        buf.put(tcpDetails.srcAddress);
+        buf.put(tcpDetails.dstAddress);
+        buf.putShort((short) tcpDetails.srcPort);
+        buf.putShort((short) tcpDetails.dstPort);
+        buf.putInt(tcpDetails.seq);                 // Sequence Number
+        buf.putInt(tcpDetails.ack);                 // ACK
+        buf.putShort((short) 0x5010);               // TCP length=5, flags=ACK
+        buf.putShort((short) (tcpDetails.rcvWnd >> tcpDetails.rcvWndScale));   // Window size
+        final int tcpChecksumOffset = buf.position();
+        buf.putShort((short) 0);                    // TCP checksum
+        // URG is not set therefore the urgent pointer is zero.
+        buf.putShort((short) 0);                    // Urgent pointer
+
+        buf.putShort(ipChecksumOffset, IpUtils.ipChecksum(buf, 0));
+        buf.putShort(tcpChecksumOffset, IpUtils.tcpChecksum(
+                buf, 0, IPV4_HEADER_LENGTH, TCP_HEADER_LENGTH));
+
+        return buf.array();
+    }
+
+    // TODO: add buildV6Packet.
+
+    @Override
+    public boolean equals(@Nullable final Object o) {
+        if (!(o instanceof TcpKeepalivePacketData)) return false;
+        final TcpKeepalivePacketData other = (TcpKeepalivePacketData) o;
+        return this.srcAddress.equals(other.srcAddress)
+                && this.dstAddress.equals(other.dstAddress)
+                && this.srcPort == other.srcPort
+                && this.dstPort == other.dstPort
+                && this.tcpAck == other.tcpAck
+                && this.tcpSeq == other.tcpSeq
+                && this.tcpWnd == other.tcpWnd
+                && this.tcpWndScale == other.tcpWndScale
+                && this.ipTos == other.ipTos
+                && this.ipTtl == other.ipTtl;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(srcAddress, dstAddress, srcPort, dstPort, tcpAck, tcpSeq, tcpWnd,
+                tcpWndScale, ipTos, ipTtl);
+    }
+
+    /**
+     * Parcelable Implementation.
+     * Note that this object implements parcelable (and needs to keep doing this as it inherits
+     * from a class that does), but should usually be parceled as a stable parcelable using
+     * the toStableParcelable() and fromStableParcelable() methods.
+     */
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Write to parcel. */
+    public void writeToParcel(Parcel out, int flags) {
+        super.writeToParcel(out, flags);
+        out.writeInt(tcpSeq);
+        out.writeInt(tcpAck);
+        out.writeInt(tcpWnd);
+        out.writeInt(tcpWndScale);
+        out.writeInt(ipTos);
+        out.writeInt(ipTtl);
+    }
+
+    private TcpKeepalivePacketData(Parcel in) {
+        super(in);
+        tcpSeq = in.readInt();
+        tcpAck = in.readInt();
+        tcpWnd = in.readInt();
+        tcpWndScale = in.readInt();
+        ipTos = in.readInt();
+        ipTtl = in.readInt();
+    }
+
+    /** Parcelable Creator. */
+    public static final @NonNull Parcelable.Creator<TcpKeepalivePacketData> CREATOR =
+            new Parcelable.Creator<TcpKeepalivePacketData>() {
+                public TcpKeepalivePacketData createFromParcel(Parcel in) {
+                    return new TcpKeepalivePacketData(in);
+                }
+
+                public TcpKeepalivePacketData[] newArray(int size) {
+                    return new TcpKeepalivePacketData[size];
+                }
+            };
+
+    /**
+     * Convert this TcpKeepalivePacketData to a TcpKeepalivePacketDataParcelable.
+     */
+    @NonNull
+    public TcpKeepalivePacketDataParcelable toStableParcelable() {
+        final TcpKeepalivePacketDataParcelable parcel = new TcpKeepalivePacketDataParcelable();
+        parcel.srcAddress = srcAddress.getAddress();
+        parcel.srcPort = srcPort;
+        parcel.dstAddress = dstAddress.getAddress();
+        parcel.dstPort = dstPort;
+        parcel.seq = tcpSeq;
+        parcel.ack = tcpAck;
+        parcel.rcvWnd = tcpWnd;
+        parcel.rcvWndScale = tcpWndScale;
+        parcel.tos = ipTos;
+        parcel.ttl = ipTtl;
+        return parcel;
+    }
+
+    @Override
+    public String toString() {
+        return "saddr: " + srcAddress
+                + " daddr: " + dstAddress
+                + " sport: " + srcPort
+                + " dport: " + dstPort
+                + " seq: " + tcpSeq
+                + " ack: " + tcpAck
+                + " wnd: " + tcpWnd
+                + " wndScale: " + tcpWndScale
+                + " tos: " + ipTos
+                + " ttl: " + ipTtl;
+    }
+}
diff --git a/android/net/TcpRepairWindow.java b/android/net/TcpRepairWindow.java
new file mode 100644
index 0000000..86034f0
--- /dev/null
+++ b/android/net/TcpRepairWindow.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2019 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;
+
+/**
+ * Corresponds to C's {@code struct tcp_repair_window} from
+ * include/uapi/linux/tcp.h
+ *
+ * @hide
+ */
+public final class TcpRepairWindow {
+    public final int sndWl1;
+    public final int sndWnd;
+    public final int maxWindow;
+    public final int rcvWnd;
+    public final int rcvWup;
+    public final int rcvWndScale;
+
+    /**
+     * Constructs an instance with the given field values.
+     */
+    public TcpRepairWindow(final int sndWl1, final int sndWnd, final int maxWindow,
+            final int rcvWnd, final int rcvWup, final int rcvWndScale) {
+        this.sndWl1 = sndWl1;
+        this.sndWnd = sndWnd;
+        this.maxWindow = maxWindow;
+        this.rcvWnd = rcvWnd;
+        this.rcvWup = rcvWup;
+        this.rcvWndScale = rcvWndScale;
+    }
+}
diff --git a/android/net/TcpSocketKeepalive.java b/android/net/TcpSocketKeepalive.java
new file mode 100644
index 0000000..436397e
--- /dev/null
+++ b/android/net/TcpSocketKeepalive.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2019 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.NonNull;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.io.FileDescriptor;
+import java.util.concurrent.Executor;
+
+/** @hide */
+final class TcpSocketKeepalive extends SocketKeepalive {
+
+    TcpSocketKeepalive(@NonNull IConnectivityManager service,
+            @NonNull Network network,
+            @NonNull ParcelFileDescriptor pfd,
+            @NonNull Executor executor,
+            @NonNull Callback callback) {
+        super(service, network, pfd, executor, callback);
+    }
+
+    /**
+     * Starts keepalives. {@code mSocket} must be a connected TCP socket.
+     *
+     * - The application must not write to or read from the socket after calling this method, until
+     *   onDataReceived, onStopped, or onError are called. If it does, the keepalive will fail
+     *   with {@link #ERROR_SOCKET_NOT_IDLE}, or {@code #ERROR_INVALID_SOCKET} if the socket
+     *   experienced an error (as in poll(2) returned POLLERR or POLLHUP); if this happens, the data
+     *   received from the socket may be invalid, and the socket can't be recovered.
+     * - If the socket has data in the send or receive buffer, then this call will fail with
+     *   {@link #ERROR_SOCKET_NOT_IDLE} and can be retried after the data has been processed.
+     *   An app could ensure this by using an application-layer protocol to receive acknowledgement
+     *   that indicates all data has been delivered to server, e.g. HTTP 200 OK.
+     *   Then the app could go into keepalive mode after reading all remaining data within the
+     *   acknowledgement.
+     */
+    @Override
+    void startImpl(int intervalSec) {
+        mExecutor.execute(() -> {
+            try {
+                final FileDescriptor fd = mPfd.getFileDescriptor();
+                mService.startTcpKeepalive(mNetwork, fd, intervalSec, mCallback);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error starting packet keepalive: ", e);
+                throw e.rethrowFromSystemServer();
+            }
+        });
+    }
+
+    @Override
+    void stopImpl() {
+        mExecutor.execute(() -> {
+            try {
+                if (mSlot != null) {
+                    mService.stopKeepalive(mNetwork, mSlot);
+                }
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error stopping packet keepalive: ", e);
+                throw e.rethrowFromSystemServer();
+            }
+        });
+    }
+}
diff --git a/android/net/TestNetworkInterface.java b/android/net/TestNetworkInterface.java
new file mode 100644
index 0000000..8455083
--- /dev/null
+++ b/android/net/TestNetworkInterface.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2019 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.TestApi;
+import android.os.Parcel;
+import android.os.ParcelFileDescriptor;
+import android.os.Parcelable;
+
+/**
+ * This class is used to return the interface name and fd of the test interface
+ *
+ * @hide
+ */
+@TestApi
+public final class TestNetworkInterface implements Parcelable {
+    private final ParcelFileDescriptor mFileDescriptor;
+    private final String mInterfaceName;
+
+    @Override
+    public int describeContents() {
+        return (mFileDescriptor != null) ? Parcelable.CONTENTS_FILE_DESCRIPTOR : 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeParcelable(mFileDescriptor, PARCELABLE_WRITE_RETURN_VALUE);
+        out.writeString(mInterfaceName);
+    }
+
+    public TestNetworkInterface(ParcelFileDescriptor pfd, String intf) {
+        mFileDescriptor = pfd;
+        mInterfaceName = intf;
+    }
+
+    private TestNetworkInterface(Parcel in) {
+        mFileDescriptor = in.readParcelable(ParcelFileDescriptor.class.getClassLoader());
+        mInterfaceName = in.readString();
+    }
+
+    public ParcelFileDescriptor getFileDescriptor() {
+        return mFileDescriptor;
+    }
+
+    public String getInterfaceName() {
+        return mInterfaceName;
+    }
+
+    public static final Parcelable.Creator<TestNetworkInterface> CREATOR =
+            new Parcelable.Creator<TestNetworkInterface>() {
+                public TestNetworkInterface createFromParcel(Parcel in) {
+                    return new TestNetworkInterface(in);
+                }
+
+                public TestNetworkInterface[] newArray(int size) {
+                    return new TestNetworkInterface[size];
+                }
+            };
+}
diff --git a/android/net/TestNetworkManager.java b/android/net/TestNetworkManager.java
new file mode 100644
index 0000000..e274005
--- /dev/null
+++ b/android/net/TestNetworkManager.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2018 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.NonNull;
+import android.annotation.TestApi;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+import com.android.internal.util.Preconditions;
+
+/**
+ * Class that allows creation and management of per-app, test-only networks
+ *
+ * @hide
+ */
+@TestApi
+public class TestNetworkManager {
+    @NonNull private static final String TAG = TestNetworkManager.class.getSimpleName();
+
+    @NonNull private final ITestNetworkManager mService;
+
+    /** @hide */
+    public TestNetworkManager(@NonNull ITestNetworkManager service) {
+        mService = Preconditions.checkNotNull(service, "missing ITestNetworkManager");
+    }
+
+    /**
+     * Teardown the capability-limited, testing-only network for a given interface
+     *
+     * @param network The test network that should be torn down
+     * @hide
+     */
+    @TestApi
+    public void teardownTestNetwork(@NonNull Network network) {
+        try {
+            mService.teardownTestNetwork(network.netId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Sets up a capability-limited, testing-only network for a given interface
+     *
+     * @param iface the name of the interface to be used for the Network LinkProperties.
+     * @param binder A binder object guarding the lifecycle of this test network.
+     * @hide
+     */
+    @TestApi
+    public void setupTestNetwork(@NonNull String iface, @NonNull IBinder binder) {
+        try {
+            mService.setupTestNetwork(iface, binder);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Create a tun interface for testing purposes
+     *
+     * @param linkAddrs an array of LinkAddresses to assign to the TUN interface
+     * @return A ParcelFileDescriptor of the underlying TUN interface. Close this to tear down the
+     *     TUN interface.
+     * @hide
+     */
+    @TestApi
+    public TestNetworkInterface createTunInterface(@NonNull LinkAddress[] linkAddrs) {
+        try {
+            return mService.createTunInterface(linkAddrs);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Create a tap interface for testing purposes
+     *
+     * @return A ParcelFileDescriptor of the underlying TAP interface. Close this to tear down the
+     *     TAP interface.
+     * @hide
+     */
+    @TestApi
+    public TestNetworkInterface createTapInterface() {
+        try {
+            return mService.createTapInterface();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+}
diff --git a/android/net/TrafficStats.java b/android/net/TrafficStats.java
new file mode 100644
index 0000000..1c6a484
--- /dev/null
+++ b/android/net/TrafficStats.java
@@ -0,0 +1,999 @@
+/*
+ * Copyright (C) 2007 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.SuppressLint;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.annotation.UnsupportedAppUsage;
+import android.app.DownloadManager;
+import android.app.backup.BackupManager;
+import android.app.usage.NetworkStatsManager;
+import android.content.Context;
+import android.media.MediaPlayer;
+import android.os.Build;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.DataUnit;
+
+import com.android.server.NetworkManagementSocketTagger;
+
+import dalvik.system.SocketTagger;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.DatagramSocket;
+import java.net.Socket;
+import java.net.SocketException;
+
+/**
+ * Class that provides network traffic statistics. These statistics include
+ * bytes transmitted and received and network packets transmitted and received,
+ * over all interfaces, over the mobile interface, and on a per-UID basis.
+ * <p>
+ * These statistics may not be available on all platforms. If the statistics are
+ * not supported by this device, {@link #UNSUPPORTED} will be returned.
+ * <p>
+ * Note that the statistics returned by this class reset and start from zero
+ * after every reboot. To access more robust historical network statistics data,
+ * use {@link NetworkStatsManager} instead.
+ */
+public class TrafficStats {
+    /**
+     * The return value to indicate that the device does not support the statistic.
+     */
+    public final static int UNSUPPORTED = -1;
+
+    /** @hide @deprecated use {@link DataUnit} instead to clarify SI-vs-IEC */
+    @Deprecated
+    public static final long KB_IN_BYTES = 1024;
+    /** @hide @deprecated use {@link DataUnit} instead to clarify SI-vs-IEC */
+    @Deprecated
+    public static final long MB_IN_BYTES = KB_IN_BYTES * 1024;
+    /** @hide @deprecated use {@link DataUnit} instead to clarify SI-vs-IEC */
+    @Deprecated
+    public static final long GB_IN_BYTES = MB_IN_BYTES * 1024;
+    /** @hide @deprecated use {@link DataUnit} instead to clarify SI-vs-IEC */
+    @Deprecated
+    public static final long TB_IN_BYTES = GB_IN_BYTES * 1024;
+    /** @hide @deprecated use {@link DataUnit} instead to clarify SI-vs-IEC */
+    @Deprecated
+    public static final long PB_IN_BYTES = TB_IN_BYTES * 1024;
+
+    /**
+     * Special UID value used when collecting {@link NetworkStatsHistory} for
+     * removed applications.
+     *
+     * @hide
+     */
+    public static final int UID_REMOVED = -4;
+
+    /**
+     * Special UID value used when collecting {@link NetworkStatsHistory} for
+     * tethering traffic.
+     *
+     * @hide
+     */
+    public static final int UID_TETHERING = -5;
+
+    /**
+     * Tag values in this range are reserved for the network stack. The network stack is
+     * running as UID {@link android.os.Process.NETWORK_STACK_UID} when in the mainline
+     * module separate process, and as the system UID otherwise.
+     */
+    /** @hide */
+    @SystemApi
+    public static final int TAG_NETWORK_STACK_RANGE_START = 0xFFFFFD00;
+    /** @hide */
+    @SystemApi
+    public static final int TAG_NETWORK_STACK_RANGE_END = 0xFFFFFEFF;
+
+    /**
+     * Tags between 0xFFFFFF00 and 0xFFFFFFFF are reserved and used internally by system services
+     * like DownloadManager when performing traffic on behalf of an application.
+     */
+    // Please note there is no enforcement of these constants, so do not rely on them to
+    // determine that the caller is a system caller.
+    /** @hide */
+    @SystemApi
+    public static final int TAG_SYSTEM_IMPERSONATION_RANGE_START = 0xFFFFFF00;
+    /** @hide */
+    @SystemApi
+    public static final int TAG_SYSTEM_IMPERSONATION_RANGE_END = 0xFFFFFF0F;
+
+    /**
+     * Tag values between these ranges are reserved for the network stack to do traffic
+     * on behalf of applications. It is a subrange of the range above.
+     */
+    /** @hide */
+    @SystemApi
+    public static final int TAG_NETWORK_STACK_IMPERSONATION_RANGE_START = 0xFFFFFF80;
+    /** @hide */
+    @SystemApi
+    public static final int TAG_NETWORK_STACK_IMPERSONATION_RANGE_END = 0xFFFFFF8F;
+
+    /**
+     * Default tag value for {@link DownloadManager} traffic.
+     *
+     * @hide
+     */
+    public static final int TAG_SYSTEM_DOWNLOAD = 0xFFFFFF01;
+
+    /**
+     * Default tag value for {@link MediaPlayer} traffic.
+     *
+     * @hide
+     */
+    public static final int TAG_SYSTEM_MEDIA = 0xFFFFFF02;
+
+    /**
+     * Default tag value for {@link BackupManager} backup traffic; that is,
+     * traffic from the device to the storage backend.
+     *
+     * @hide
+     */
+    public static final int TAG_SYSTEM_BACKUP = 0xFFFFFF03;
+
+    /**
+     * Default tag value for {@link BackupManager} restore traffic; that is,
+     * app data retrieved from the storage backend at install time.
+     *
+     * @hide
+     */
+    public static final int TAG_SYSTEM_RESTORE = 0xFFFFFF04;
+
+    /**
+     * Default tag value for code (typically APKs) downloaded by an app store on
+     * behalf of the app, such as updates.
+     *
+     * @hide
+     */
+    public static final int TAG_SYSTEM_APP = 0xFFFFFF05;
+
+    // TODO : remove this constant when Wifi code is updated
+    /** @hide */
+    public static final int TAG_SYSTEM_PROBE = 0xFFFFFF42;
+
+    private static INetworkStatsService sStatsService;
+
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562)
+    private synchronized static INetworkStatsService getStatsService() {
+        if (sStatsService == null) {
+            sStatsService = INetworkStatsService.Stub.asInterface(
+                    ServiceManager.getService(Context.NETWORK_STATS_SERVICE));
+        }
+        return sStatsService;
+    }
+
+    /**
+     * Snapshot of {@link NetworkStats} when the currently active profiling
+     * session started, or {@code null} if no session active.
+     *
+     * @see #startDataProfiling(Context)
+     * @see #stopDataProfiling(Context)
+     */
+    private static NetworkStats sActiveProfilingStart;
+
+    private static Object sProfilingLock = new Object();
+
+    private static final String LOOPBACK_IFACE = "lo";
+
+    /**
+     * Set active tag to use when accounting {@link Socket} traffic originating
+     * from the current thread. Only one active tag per thread is supported.
+     * <p>
+     * Changes only take effect during subsequent calls to
+     * {@link #tagSocket(Socket)}.
+     * <p>
+     * Tags between {@code 0xFFFFFF00} and {@code 0xFFFFFFFF} are reserved and
+     * used internally by system services like {@link DownloadManager} when
+     * performing traffic on behalf of an application.
+     *
+     * @see #clearThreadStatsTag()
+     */
+    public static void setThreadStatsTag(int tag) {
+        NetworkManagementSocketTagger.setThreadSocketStatsTag(tag);
+    }
+
+    /**
+     * Set active tag to use when accounting {@link Socket} traffic originating
+     * from the current thread. Only one active tag per thread is supported.
+     * <p>
+     * Changes only take effect during subsequent calls to
+     * {@link #tagSocket(Socket)}.
+     * <p>
+     * Tags between {@code 0xFFFFFF00} and {@code 0xFFFFFFFF} are reserved and
+     * used internally by system services like {@link DownloadManager} when
+     * performing traffic on behalf of an application.
+     *
+     * @return the current tag for the calling thread, which can be used to
+     *         restore any existing values after a nested operation is finished
+     */
+    public static int getAndSetThreadStatsTag(int tag) {
+        return NetworkManagementSocketTagger.setThreadSocketStatsTag(tag);
+    }
+
+    /**
+     * Set active tag to use when accounting {@link Socket} traffic originating
+     * from the current thread. The tag used internally is well-defined to
+     * distinguish all backup-related traffic.
+     *
+     * @hide
+     */
+    @SystemApi
+    public static void setThreadStatsTagBackup() {
+        setThreadStatsTag(TAG_SYSTEM_BACKUP);
+    }
+
+    /**
+     * Set active tag to use when accounting {@link Socket} traffic originating
+     * from the current thread. The tag used internally is well-defined to
+     * distinguish all restore-related traffic.
+     *
+     * @hide
+     */
+    @SystemApi
+    public static void setThreadStatsTagRestore() {
+        setThreadStatsTag(TAG_SYSTEM_RESTORE);
+    }
+
+    /**
+     * Set active tag to use when accounting {@link Socket} traffic originating
+     * from the current thread. The tag used internally is well-defined to
+     * distinguish all code (typically APKs) downloaded by an app store on
+     * behalf of the app, such as updates.
+     *
+     * @hide
+     */
+    @SystemApi
+    public static void setThreadStatsTagApp() {
+        setThreadStatsTag(TAG_SYSTEM_APP);
+    }
+
+    /**
+     * Get the active tag used when accounting {@link Socket} traffic originating
+     * from the current thread. Only one active tag per thread is supported.
+     * {@link #tagSocket(Socket)}.
+     *
+     * @see #setThreadStatsTag(int)
+     */
+    public static int getThreadStatsTag() {
+        return NetworkManagementSocketTagger.getThreadSocketStatsTag();
+    }
+
+    /**
+     * Clear any active tag set to account {@link Socket} traffic originating
+     * from the current thread.
+     *
+     * @see #setThreadStatsTag(int)
+     */
+    public static void clearThreadStatsTag() {
+        NetworkManagementSocketTagger.setThreadSocketStatsTag(-1);
+    }
+
+    /**
+     * Set specific UID to use when accounting {@link Socket} traffic
+     * originating from the current thread. Designed for use when performing an
+     * operation on behalf of another application, or when another application
+     * is performing operations on your behalf.
+     * <p>
+     * Any app can <em>accept</em> blame for traffic performed on a socket
+     * originally created by another app by calling this method with the
+     * {@link android.system.Os#getuid()} value. However, only apps holding the
+     * {@code android.Manifest.permission#UPDATE_DEVICE_STATS} permission may
+     * <em>assign</em> blame to another UIDs.
+     * <p>
+     * Changes only take effect during subsequent calls to
+     * {@link #tagSocket(Socket)}.
+     */
+    @SuppressLint("Doclava125")
+    public static void setThreadStatsUid(int uid) {
+        NetworkManagementSocketTagger.setThreadSocketStatsUid(uid);
+    }
+
+    /**
+     * Get the active UID used when accounting {@link Socket} traffic originating
+     * from the current thread. Only one active tag per thread is supported.
+     * {@link #tagSocket(Socket)}.
+     *
+     * @see #setThreadStatsUid(int)
+     */
+    public static int getThreadStatsUid() {
+        return NetworkManagementSocketTagger.getThreadSocketStatsUid();
+    }
+
+    /**
+     * Set specific UID to use when accounting {@link Socket} traffic
+     * originating from the current thread as the calling UID. Designed for use
+     * when another application is performing operations on your behalf.
+     * <p>
+     * Changes only take effect during subsequent calls to
+     * {@link #tagSocket(Socket)}.
+     *
+     * @removed
+     * @deprecated use {@link #setThreadStatsUid(int)} instead.
+     */
+    @Deprecated
+    public static void setThreadStatsUidSelf() {
+        setThreadStatsUid(android.os.Process.myUid());
+    }
+
+    /**
+     * Clear any active UID set to account {@link Socket} traffic originating
+     * from the current thread.
+     *
+     * @see #setThreadStatsUid(int)
+     */
+    @SuppressLint("Doclava125")
+    public static void clearThreadStatsUid() {
+        NetworkManagementSocketTagger.setThreadSocketStatsUid(-1);
+    }
+
+    /**
+     * Tag the given {@link Socket} with any statistics parameters active for
+     * the current thread. Subsequent calls always replace any existing
+     * parameters. When finished, call {@link #untagSocket(Socket)} to remove
+     * statistics parameters.
+     *
+     * @see #setThreadStatsTag(int)
+     */
+    public static void tagSocket(Socket socket) throws SocketException {
+        SocketTagger.get().tag(socket);
+    }
+
+    /**
+     * Remove any statistics parameters from the given {@link Socket}.
+     * <p>
+     * In Android 8.1 (API level 27) and lower, a socket is automatically
+     * untagged when it's sent to another process using binder IPC with a
+     * {@code ParcelFileDescriptor} container. In Android 9.0 (API level 28)
+     * and higher, the socket tag is kept when the socket is sent to another
+     * process using binder IPC. You can mimic the previous behavior by
+     * calling {@code untagSocket()} before sending the socket to another
+     * process.
+     */
+    public static void untagSocket(Socket socket) throws SocketException {
+        SocketTagger.get().untag(socket);
+    }
+
+    /**
+     * Tag the given {@link DatagramSocket} with any statistics parameters
+     * active for the current thread. Subsequent calls always replace any
+     * existing parameters. When finished, call
+     * {@link #untagDatagramSocket(DatagramSocket)} to remove statistics
+     * parameters.
+     *
+     * @see #setThreadStatsTag(int)
+     */
+    public static void tagDatagramSocket(DatagramSocket socket) throws SocketException {
+        SocketTagger.get().tag(socket);
+    }
+
+    /**
+     * Remove any statistics parameters from the given {@link DatagramSocket}.
+     */
+    public static void untagDatagramSocket(DatagramSocket socket) throws SocketException {
+        SocketTagger.get().untag(socket);
+    }
+
+    /**
+     * Tag the given {@link FileDescriptor} socket with any statistics
+     * parameters active for the current thread. Subsequent calls always replace
+     * any existing parameters. When finished, call
+     * {@link #untagFileDescriptor(FileDescriptor)} to remove statistics
+     * parameters.
+     *
+     * @see #setThreadStatsTag(int)
+     */
+    public static void tagFileDescriptor(FileDescriptor fd) throws IOException {
+        SocketTagger.get().tag(fd);
+    }
+
+    /**
+     * Remove any statistics parameters from the given {@link FileDescriptor}
+     * socket.
+     */
+    public static void untagFileDescriptor(FileDescriptor fd) throws IOException {
+        SocketTagger.get().untag(fd);
+    }
+
+    /**
+     * Start profiling data usage for current UID. Only one profiling session
+     * can be active at a time.
+     *
+     * @hide
+     */
+    public static void startDataProfiling(Context context) {
+        synchronized (sProfilingLock) {
+            if (sActiveProfilingStart != null) {
+                throw new IllegalStateException("already profiling data");
+            }
+
+            // take snapshot in time; we calculate delta later
+            sActiveProfilingStart = getDataLayerSnapshotForUid(context);
+        }
+    }
+
+    /**
+     * Stop profiling data usage for current UID.
+     *
+     * @return Detailed {@link NetworkStats} of data that occurred since last
+     *         {@link #startDataProfiling(Context)} call.
+     * @hide
+     */
+    public static NetworkStats stopDataProfiling(Context context) {
+        synchronized (sProfilingLock) {
+            if (sActiveProfilingStart == null) {
+                throw new IllegalStateException("not profiling data");
+            }
+
+            // subtract starting values and return delta
+            final NetworkStats profilingStop = getDataLayerSnapshotForUid(context);
+            final NetworkStats profilingDelta = NetworkStats.subtract(
+                    profilingStop, sActiveProfilingStart, null, null);
+            sActiveProfilingStart = null;
+            return profilingDelta;
+        }
+    }
+
+    /**
+     * Increment count of network operations performed under the accounting tag
+     * currently active on the calling thread. This can be used to derive
+     * bytes-per-operation.
+     *
+     * @param operationCount Number of operations to increment count by.
+     */
+    public static void incrementOperationCount(int operationCount) {
+        final int tag = getThreadStatsTag();
+        incrementOperationCount(tag, operationCount);
+    }
+
+    /**
+     * Increment count of network operations performed under the given
+     * accounting tag. This can be used to derive bytes-per-operation.
+     *
+     * @param tag Accounting tag used in {@link #setThreadStatsTag(int)}.
+     * @param operationCount Number of operations to increment count by.
+     */
+    public static void incrementOperationCount(int tag, int operationCount) {
+        final int uid = android.os.Process.myUid();
+        try {
+            getStatsService().incrementOperationCount(uid, tag, operationCount);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /** {@hide} */
+    public static void closeQuietly(INetworkStatsSession session) {
+        // TODO: move to NetworkStatsService once it exists
+        if (session != null) {
+            try {
+                session.close();
+            } catch (RuntimeException rethrown) {
+                throw rethrown;
+            } catch (Exception ignored) {
+            }
+        }
+    }
+
+    private static long addIfSupported(long stat) {
+        return (stat == UNSUPPORTED) ? 0 : stat;
+    }
+
+    /**
+     * Return number of packets transmitted across mobile networks since device
+     * boot. Counts packets across all mobile network interfaces, and always
+     * increases monotonically since device boot. Statistics are measured at the
+     * network layer, so they include both TCP and UDP usage.
+     * <p>
+     * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may
+     * return {@link #UNSUPPORTED} on devices where statistics aren't available.
+     */
+    public static long getMobileTxPackets() {
+        long total = 0;
+        for (String iface : getMobileIfaces()) {
+            total += addIfSupported(getTxPackets(iface));
+        }
+        return total;
+    }
+
+    /**
+     * Return number of packets received across mobile networks since device
+     * boot. Counts packets across all mobile network interfaces, and always
+     * increases monotonically since device boot. Statistics are measured at the
+     * network layer, so they include both TCP and UDP usage.
+     * <p>
+     * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may
+     * return {@link #UNSUPPORTED} on devices where statistics aren't available.
+     */
+    public static long getMobileRxPackets() {
+        long total = 0;
+        for (String iface : getMobileIfaces()) {
+            total += addIfSupported(getRxPackets(iface));
+        }
+        return total;
+    }
+
+    /**
+     * Return number of bytes transmitted across mobile networks since device
+     * boot. Counts packets across all mobile network interfaces, and always
+     * increases monotonically since device boot. Statistics are measured at the
+     * network layer, so they include both TCP and UDP usage.
+     * <p>
+     * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may
+     * return {@link #UNSUPPORTED} on devices where statistics aren't available.
+     */
+    public static long getMobileTxBytes() {
+        long total = 0;
+        for (String iface : getMobileIfaces()) {
+            total += addIfSupported(getTxBytes(iface));
+        }
+        return total;
+    }
+
+    /**
+     * Return number of bytes received across mobile networks since device boot.
+     * Counts packets across all mobile network interfaces, and always increases
+     * monotonically since device boot. Statistics are measured at the network
+     * layer, so they include both TCP and UDP usage.
+     * <p>
+     * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may
+     * return {@link #UNSUPPORTED} on devices where statistics aren't available.
+     */
+    public static long getMobileRxBytes() {
+        long total = 0;
+        for (String iface : getMobileIfaces()) {
+            total += addIfSupported(getRxBytes(iface));
+        }
+        return total;
+    }
+
+    /** {@hide} */
+    @UnsupportedAppUsage
+    public static long getMobileTcpRxPackets() {
+        long total = 0;
+        for (String iface : getMobileIfaces()) {
+            long stat = UNSUPPORTED;
+            try {
+                stat = getStatsService().getIfaceStats(iface, TYPE_TCP_RX_PACKETS);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+            total += addIfSupported(stat);
+        }
+        return total;
+    }
+
+    /** {@hide} */
+    @UnsupportedAppUsage
+    public static long getMobileTcpTxPackets() {
+        long total = 0;
+        for (String iface : getMobileIfaces()) {
+            long stat = UNSUPPORTED;
+            try {
+                stat = getStatsService().getIfaceStats(iface, TYPE_TCP_TX_PACKETS);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+            total += addIfSupported(stat);
+        }
+        return total;
+    }
+
+    /** {@hide} */
+    public static long getTxPackets(String iface) {
+        try {
+            return getStatsService().getIfaceStats(iface, TYPE_TX_PACKETS);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /** {@hide} */
+    public static long getRxPackets(String iface) {
+        try {
+            return getStatsService().getIfaceStats(iface, TYPE_RX_PACKETS);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /** {@hide} */
+    @UnsupportedAppUsage
+    public static long getTxBytes(String iface) {
+        try {
+            return getStatsService().getIfaceStats(iface, TYPE_TX_BYTES);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /** {@hide} */
+    @UnsupportedAppUsage
+    public static long getRxBytes(String iface) {
+        try {
+            return getStatsService().getIfaceStats(iface, TYPE_RX_BYTES);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /** {@hide} */
+    @TestApi
+    public static long getLoopbackTxPackets() {
+        try {
+            return getStatsService().getIfaceStats(LOOPBACK_IFACE, TYPE_TX_PACKETS);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /** {@hide} */
+    @TestApi
+    public static long getLoopbackRxPackets() {
+        try {
+            return getStatsService().getIfaceStats(LOOPBACK_IFACE, TYPE_RX_PACKETS);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /** {@hide} */
+    @TestApi
+    public static long getLoopbackTxBytes() {
+        try {
+            return getStatsService().getIfaceStats(LOOPBACK_IFACE, TYPE_TX_BYTES);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /** {@hide} */
+    @TestApi
+    public static long getLoopbackRxBytes() {
+        try {
+            return getStatsService().getIfaceStats(LOOPBACK_IFACE, TYPE_RX_BYTES);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Return number of packets transmitted since device boot. Counts packets
+     * across all network interfaces, and always increases monotonically since
+     * device boot. Statistics are measured at the network layer, so they
+     * include both TCP and UDP usage.
+     * <p>
+     * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may
+     * return {@link #UNSUPPORTED} on devices where statistics aren't available.
+     */
+    public static long getTotalTxPackets() {
+        try {
+            return getStatsService().getTotalStats(TYPE_TX_PACKETS);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Return number of packets received since device boot. Counts packets
+     * across all network interfaces, and always increases monotonically since
+     * device boot. Statistics are measured at the network layer, so they
+     * include both TCP and UDP usage.
+     * <p>
+     * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may
+     * return {@link #UNSUPPORTED} on devices where statistics aren't available.
+     */
+    public static long getTotalRxPackets() {
+        try {
+            return getStatsService().getTotalStats(TYPE_RX_PACKETS);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Return number of bytes transmitted since device boot. Counts packets
+     * across all network interfaces, and always increases monotonically since
+     * device boot. Statistics are measured at the network layer, so they
+     * include both TCP and UDP usage.
+     * <p>
+     * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may
+     * return {@link #UNSUPPORTED} on devices where statistics aren't available.
+     */
+    public static long getTotalTxBytes() {
+        try {
+            return getStatsService().getTotalStats(TYPE_TX_BYTES);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Return number of bytes received since device boot. Counts packets across
+     * all network interfaces, and always increases monotonically since device
+     * boot. Statistics are measured at the network layer, so they include both
+     * TCP and UDP usage.
+     * <p>
+     * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may
+     * return {@link #UNSUPPORTED} on devices where statistics aren't available.
+     */
+    public static long getTotalRxBytes() {
+        try {
+            return getStatsService().getTotalStats(TYPE_RX_BYTES);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Return number of bytes transmitted by the given UID since device boot.
+     * Counts packets across all network interfaces, and always increases
+     * monotonically since device boot. Statistics are measured at the network
+     * layer, so they include both TCP and UDP usage.
+     * <p>
+     * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may
+     * return {@link #UNSUPPORTED} on devices where statistics aren't available.
+     * <p>
+     * Starting in {@link android.os.Build.VERSION_CODES#N} this will only
+     * report traffic statistics for the calling UID. It will return
+     * {@link #UNSUPPORTED} for all other UIDs for privacy reasons. To access
+     * historical network statistics belonging to other UIDs, use
+     * {@link NetworkStatsManager}.
+     *
+     * @see android.os.Process#myUid()
+     * @see android.content.pm.ApplicationInfo#uid
+     */
+    public static long getUidTxBytes(int uid) {
+        // This isn't actually enforcing any security; it just returns the
+        // unsupported value. The real filtering is done at the kernel level.
+        final int callingUid = android.os.Process.myUid();
+        if (callingUid == android.os.Process.SYSTEM_UID || callingUid == uid) {
+            try {
+                return getStatsService().getUidStats(uid, TYPE_TX_BYTES);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        } else {
+            return UNSUPPORTED;
+        }
+    }
+
+    /**
+     * Return number of bytes received by the given UID since device boot.
+     * Counts packets across all network interfaces, and always increases
+     * monotonically since device boot. Statistics are measured at the network
+     * layer, so they include both TCP and UDP usage.
+     * <p>
+     * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may return
+     * {@link #UNSUPPORTED} on devices where statistics aren't available.
+     * <p>
+     * Starting in {@link android.os.Build.VERSION_CODES#N} this will only
+     * report traffic statistics for the calling UID. It will return
+     * {@link #UNSUPPORTED} for all other UIDs for privacy reasons. To access
+     * historical network statistics belonging to other UIDs, use
+     * {@link NetworkStatsManager}.
+     *
+     * @see android.os.Process#myUid()
+     * @see android.content.pm.ApplicationInfo#uid
+     */
+    public static long getUidRxBytes(int uid) {
+        // This isn't actually enforcing any security; it just returns the
+        // unsupported value. The real filtering is done at the kernel level.
+        final int callingUid = android.os.Process.myUid();
+        if (callingUid == android.os.Process.SYSTEM_UID || callingUid == uid) {
+            try {
+                return getStatsService().getUidStats(uid, TYPE_RX_BYTES);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        } else {
+            return UNSUPPORTED;
+        }
+    }
+
+    /**
+     * Return number of packets transmitted by the given UID since device boot.
+     * Counts packets across all network interfaces, and always increases
+     * monotonically since device boot. Statistics are measured at the network
+     * layer, so they include both TCP and UDP usage.
+     * <p>
+     * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may return
+     * {@link #UNSUPPORTED} on devices where statistics aren't available.
+     * <p>
+     * Starting in {@link android.os.Build.VERSION_CODES#N} this will only
+     * report traffic statistics for the calling UID. It will return
+     * {@link #UNSUPPORTED} for all other UIDs for privacy reasons. To access
+     * historical network statistics belonging to other UIDs, use
+     * {@link NetworkStatsManager}.
+     *
+     * @see android.os.Process#myUid()
+     * @see android.content.pm.ApplicationInfo#uid
+     */
+    public static long getUidTxPackets(int uid) {
+        // This isn't actually enforcing any security; it just returns the
+        // unsupported value. The real filtering is done at the kernel level.
+        final int callingUid = android.os.Process.myUid();
+        if (callingUid == android.os.Process.SYSTEM_UID || callingUid == uid) {
+            try {
+                return getStatsService().getUidStats(uid, TYPE_TX_PACKETS);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        } else {
+            return UNSUPPORTED;
+        }
+    }
+
+    /**
+     * Return number of packets received by the given UID since device boot.
+     * Counts packets across all network interfaces, and always increases
+     * monotonically since device boot. Statistics are measured at the network
+     * layer, so they include both TCP and UDP usage.
+     * <p>
+     * Before {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2}, this may return
+     * {@link #UNSUPPORTED} on devices where statistics aren't available.
+     * <p>
+     * Starting in {@link android.os.Build.VERSION_CODES#N} this will only
+     * report traffic statistics for the calling UID. It will return
+     * {@link #UNSUPPORTED} for all other UIDs for privacy reasons. To access
+     * historical network statistics belonging to other UIDs, use
+     * {@link NetworkStatsManager}.
+     *
+     * @see android.os.Process#myUid()
+     * @see android.content.pm.ApplicationInfo#uid
+     */
+    public static long getUidRxPackets(int uid) {
+        // This isn't actually enforcing any security; it just returns the
+        // unsupported value. The real filtering is done at the kernel level.
+        final int callingUid = android.os.Process.myUid();
+        if (callingUid == android.os.Process.SYSTEM_UID || callingUid == uid) {
+            try {
+                return getStatsService().getUidStats(uid, TYPE_RX_PACKETS);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        } else {
+            return UNSUPPORTED;
+        }
+    }
+
+    /**
+     * @deprecated Starting in {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2},
+     *             transport layer statistics are no longer available, and will
+     *             always return {@link #UNSUPPORTED}.
+     * @see #getUidTxBytes(int)
+     */
+    @Deprecated
+    public static long getUidTcpTxBytes(int uid) {
+        return UNSUPPORTED;
+    }
+
+    /**
+     * @deprecated Starting in {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2},
+     *             transport layer statistics are no longer available, and will
+     *             always return {@link #UNSUPPORTED}.
+     * @see #getUidRxBytes(int)
+     */
+    @Deprecated
+    public static long getUidTcpRxBytes(int uid) {
+        return UNSUPPORTED;
+    }
+
+    /**
+     * @deprecated Starting in {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2},
+     *             transport layer statistics are no longer available, and will
+     *             always return {@link #UNSUPPORTED}.
+     * @see #getUidTxBytes(int)
+     */
+    @Deprecated
+    public static long getUidUdpTxBytes(int uid) {
+        return UNSUPPORTED;
+    }
+
+    /**
+     * @deprecated Starting in {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2},
+     *             transport layer statistics are no longer available, and will
+     *             always return {@link #UNSUPPORTED}.
+     * @see #getUidRxBytes(int)
+     */
+    @Deprecated
+    public static long getUidUdpRxBytes(int uid) {
+        return UNSUPPORTED;
+    }
+
+    /**
+     * @deprecated Starting in {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2},
+     *             transport layer statistics are no longer available, and will
+     *             always return {@link #UNSUPPORTED}.
+     * @see #getUidTxPackets(int)
+     */
+    @Deprecated
+    public static long getUidTcpTxSegments(int uid) {
+        return UNSUPPORTED;
+    }
+
+    /**
+     * @deprecated Starting in {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2},
+     *             transport layer statistics are no longer available, and will
+     *             always return {@link #UNSUPPORTED}.
+     * @see #getUidRxPackets(int)
+     */
+    @Deprecated
+    public static long getUidTcpRxSegments(int uid) {
+        return UNSUPPORTED;
+    }
+
+    /**
+     * @deprecated Starting in {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2},
+     *             transport layer statistics are no longer available, and will
+     *             always return {@link #UNSUPPORTED}.
+     * @see #getUidTxPackets(int)
+     */
+    @Deprecated
+    public static long getUidUdpTxPackets(int uid) {
+        return UNSUPPORTED;
+    }
+
+    /**
+     * @deprecated Starting in {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR2},
+     *             transport layer statistics are no longer available, and will
+     *             always return {@link #UNSUPPORTED}.
+     * @see #getUidRxPackets(int)
+     */
+    @Deprecated
+    public static long getUidUdpRxPackets(int uid) {
+        return UNSUPPORTED;
+    }
+
+    /**
+     * Return detailed {@link NetworkStats} for the current UID. Requires no
+     * special permission.
+     */
+    private static NetworkStats getDataLayerSnapshotForUid(Context context) {
+        // TODO: take snapshot locally, since proc file is now visible
+        final int uid = android.os.Process.myUid();
+        try {
+            return getStatsService().getDataLayerSnapshotForUid(uid);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Return set of any ifaces associated with mobile networks since boot.
+     * Interfaces are never removed from this list, so counters should always be
+     * monotonic.
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 130143562)
+    private static String[] getMobileIfaces() {
+        try {
+            return getStatsService().getMobileIfaces();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    // NOTE: keep these in sync with android_net_TrafficStats.cpp
+    private static final int TYPE_RX_BYTES = 0;
+    private static final int TYPE_RX_PACKETS = 1;
+    private static final int TYPE_TX_BYTES = 2;
+    private static final int TYPE_TX_PACKETS = 3;
+    private static final int TYPE_TCP_RX_PACKETS = 4;
+    private static final int TYPE_TCP_TX_PACKETS = 5;
+}
diff --git a/android/net/TransportInfo.java b/android/net/TransportInfo.java
new file mode 100644
index 0000000..b78d3fe
--- /dev/null
+++ b/android/net/TransportInfo.java
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2018 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;
+
+/**
+ * A container for transport-specific capabilities which is returned by
+ * {@link NetworkCapabilities#getTransportInfo()}. Specific networks
+ * may provide concrete implementations of this interface.
+ */
+public interface TransportInfo {
+}
diff --git a/android/net/UidRange.java b/android/net/UidRange.java
new file mode 100644
index 0000000..d75c43d
--- /dev/null
+++ b/android/net/UidRange.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import static android.os.UserHandle.PER_USER_RANGE;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Collection;
+
+/**
+ * An inclusive range of UIDs.
+ *
+ * @hide
+ */
+public final class UidRange implements Parcelable {
+    public final int start;
+    public final int stop;
+
+    public UidRange(int startUid, int stopUid) {
+        if (startUid < 0) throw new IllegalArgumentException("Invalid start UID.");
+        if (stopUid < 0) throw new IllegalArgumentException("Invalid stop UID.");
+        if (startUid > stopUid) throw new IllegalArgumentException("Invalid UID range.");
+        start = startUid;
+        stop  = stopUid;
+    }
+
+    public static UidRange createForUser(int userId) {
+        return new UidRange(userId * PER_USER_RANGE, (userId + 1) * PER_USER_RANGE - 1);
+    }
+
+    /** Returns the smallest user Id which is contained in this UidRange */
+    public int getStartUser() {
+        return start / PER_USER_RANGE;
+    }
+
+    /** Returns the largest user Id which is contained in this UidRange */
+    public int getEndUser() {
+        return stop / PER_USER_RANGE;
+    }
+
+    public boolean contains(int uid) {
+        return start <= uid && uid <= stop;
+    }
+
+    /**
+     * Returns the count of UIDs in this range.
+     */
+    public int count() {
+        return 1 + stop - start;
+    }
+
+    /**
+     * @return {@code true} if this range contains every UID contained by the {@param other} range.
+     */
+    public boolean containsRange(UidRange other) {
+        return start <= other.start && other.stop <= stop;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = 17;
+        result = 31 * result + start;
+        result = 31 * result + stop;
+        return result;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (o instanceof UidRange) {
+            UidRange other = (UidRange) o;
+            return start == other.start && stop == other.stop;
+        }
+        return false;
+    }
+
+    @Override
+    public String toString() {
+        return start + "-" + stop;
+    }
+
+    // Implement the Parcelable interface
+    // TODO: Consider making this class no longer parcelable, since all users are likely in the
+    // system server.
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(start);
+        dest.writeInt(stop);
+    }
+
+    public static final @android.annotation.NonNull Creator<UidRange> CREATOR =
+        new Creator<UidRange>() {
+            @Override
+            public UidRange createFromParcel(Parcel in) {
+                int start = in.readInt();
+                int stop = in.readInt();
+
+                return new UidRange(start, stop);
+            }
+            @Override
+            public UidRange[] newArray(int size) {
+                return new UidRange[size];
+            }
+    };
+
+    /**
+     * Returns whether any of the UidRange in the collection contains the specified uid
+     *
+     * @param ranges The collection of UidRange to check
+     * @param uid the uid in question
+     * @return {@code true} if the uid is contained within the ranges, {@code false} otherwise
+     *
+     * @see UidRange#contains(int)
+     */
+    public static boolean containsUid(Collection<UidRange> ranges, int uid) {
+        if (ranges == null) return false;
+        for (UidRange range : ranges) {
+            if (range.contains(uid)) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
diff --git a/android/net/Uri.java b/android/net/Uri.java
new file mode 100644
index 0000000..c3166e9
--- /dev/null
+++ b/android/net/Uri.java
@@ -0,0 +1,2428 @@
+/*
+ * Copyright (C) 2007 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.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.UnsupportedAppUsage;
+import android.content.Intent;
+import android.os.Environment;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.StrictMode;
+import android.util.Log;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.AbstractList;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Objects;
+import java.util.RandomAccess;
+import java.util.Set;
+
+/**
+ * Immutable URI reference. A URI reference includes a URI and a fragment, the
+ * component of the URI following a '#'. Builds and parses URI references
+ * which conform to
+ * <a href="http://www.faqs.org/rfcs/rfc2396.html">RFC 2396</a>.
+ *
+ * <p>In the interest of performance, this class performs little to no
+ * validation. Behavior is undefined for invalid input. This class is very
+ * forgiving--in the face of invalid input, it will return garbage
+ * rather than throw an exception unless otherwise specified.
+ */
+public abstract class Uri implements Parcelable, Comparable<Uri> {
+
+    /*
+
+    This class aims to do as little up front work as possible. To accomplish
+    that, we vary the implementation depending on what the user passes in.
+    For example, we have one implementation if the user passes in a
+    URI string (StringUri) and another if the user passes in the
+    individual components (OpaqueUri).
+
+    *Concurrency notes*: Like any truly immutable object, this class is safe
+    for concurrent use. This class uses a caching pattern in some places where
+    it doesn't use volatile or synchronized. This is safe to do with ints
+    because getting or setting an int is atomic. It's safe to do with a String
+    because the internal fields are final and the memory model guarantees other
+    threads won't see a partially initialized instance. We are not guaranteed
+    that some threads will immediately see changes from other threads on
+    certain platforms, but we don't mind if those threads reconstruct the
+    cached result. As a result, we get thread safe caching with no concurrency
+    overhead, which means the most common case, access from a single thread,
+    is as fast as possible.
+
+    From the Java Language spec.:
+
+    "17.5 Final Field Semantics
+
+    ... when the object is seen by another thread, that thread will always
+    see the correctly constructed version of that object's final fields.
+    It will also see versions of any object or array referenced by
+    those final fields that are at least as up-to-date as the final fields
+    are."
+
+    In that same vein, all non-transient fields within Uri
+    implementations should be final and immutable so as to ensure true
+    immutability for clients even when they don't use proper concurrency
+    control.
+
+    For reference, from RFC 2396:
+
+    "4.3. Parsing a URI Reference
+
+       A URI reference is typically parsed according to the four main
+       components and fragment identifier in order to determine what
+       components are present and whether the reference is relative or
+       absolute.  The individual components are then parsed for their
+       subparts and, if not opaque, to verify their validity.
+
+       Although the BNF defines what is allowed in each component, it is
+       ambiguous in terms of differentiating between an authority component
+       and a path component that begins with two slash characters.  The
+       greedy algorithm is used for disambiguation: the left-most matching
+       rule soaks up as much of the URI reference string as it is capable of
+       matching.  In other words, the authority component wins."
+
+    The "four main components" of a hierarchical URI consist of
+    <scheme>://<authority><path>?<query>
+
+    */
+
+    /** Log tag. */
+    private static final String LOG = Uri.class.getSimpleName();
+
+    /**
+     * NOTE: EMPTY accesses this field during its own initialization, so this
+     * field *must* be initialized first, or else EMPTY will see a null value!
+     *
+     * Placeholder for strings which haven't been cached. This enables us
+     * to cache null. We intentionally create a new String instance so we can
+     * compare its identity and there is no chance we will confuse it with
+     * user data.
+     */
+    @SuppressWarnings("RedundantStringConstructorCall")
+    private static final String NOT_CACHED = new String("NOT CACHED");
+
+    /**
+     * The empty URI, equivalent to "".
+     */
+    public static final Uri EMPTY = new HierarchicalUri(null, Part.NULL,
+            PathPart.EMPTY, Part.NULL, Part.NULL);
+
+    /**
+     * Prevents external subclassing.
+     */
+    @UnsupportedAppUsage
+    private Uri() {}
+
+    /**
+     * Returns true if this URI is hierarchical like "http://google.com".
+     * Absolute URIs are hierarchical if the scheme-specific part starts with
+     * a '/'. Relative URIs are always hierarchical.
+     */
+    public abstract boolean isHierarchical();
+
+    /**
+     * Returns true if this URI is opaque like "mailto:[email protected]". The
+     * scheme-specific part of an opaque URI cannot start with a '/'.
+     */
+    public boolean isOpaque() {
+        return !isHierarchical();
+    }
+
+    /**
+     * Returns true if this URI is relative, i.e.&nbsp;if it doesn't contain an
+     * explicit scheme.
+     *
+     * @return true if this URI is relative, false if it's absolute
+     */
+    public abstract boolean isRelative();
+
+    /**
+     * Returns true if this URI is absolute, i.e.&nbsp;if it contains an
+     * explicit scheme.
+     *
+     * @return true if this URI is absolute, false if it's relative
+     */
+    public boolean isAbsolute() {
+        return !isRelative();
+    }
+
+    /**
+     * Gets the scheme of this URI. Example: "http"
+     *
+     * @return the scheme or null if this is a relative URI
+     */
+    @Nullable
+    public abstract String getScheme();
+
+    /**
+     * Gets the scheme-specific part of this URI, i.e.&nbsp;everything between
+     * the scheme separator ':' and the fragment separator '#'. If this is a
+     * relative URI, this method returns the entire URI. Decodes escaped octets.
+     *
+     * <p>Example: "//www.google.com/search?q=android"
+     *
+     * @return the decoded scheme-specific-part
+     */
+    public abstract String getSchemeSpecificPart();
+
+    /**
+     * Gets the scheme-specific part of this URI, i.e.&nbsp;everything between
+     * the scheme separator ':' and the fragment separator '#'. If this is a
+     * relative URI, this method returns the entire URI. Leaves escaped octets
+     * intact.
+     *
+     * <p>Example: "//www.google.com/search?q=android"
+     *
+     * @return the encoded scheme-specific-part
+     */
+    public abstract String getEncodedSchemeSpecificPart();
+
+    /**
+     * Gets the decoded authority part of this URI. For
+     * server addresses, the authority is structured as follows:
+     * {@code [ userinfo '@' ] host [ ':' port ]}
+     *
+     * <p>Examples: "google.com", "[email protected]:80"
+     *
+     * @return the authority for this URI or null if not present
+     */
+    @Nullable
+    public abstract String getAuthority();
+
+    /**
+     * Gets the encoded authority part of this URI. For
+     * server addresses, the authority is structured as follows:
+     * {@code [ userinfo '@' ] host [ ':' port ]}
+     *
+     * <p>Examples: "google.com", "[email protected]:80"
+     *
+     * @return the authority for this URI or null if not present
+     */
+    @Nullable
+    public abstract String getEncodedAuthority();
+
+    /**
+     * Gets the decoded user information from the authority.
+     * For example, if the authority is "[email protected]", this method will
+     * return "nobody".
+     *
+     * @return the user info for this URI or null if not present
+     */
+    @Nullable
+    public abstract String getUserInfo();
+
+    /**
+     * Gets the encoded user information from the authority.
+     * For example, if the authority is "[email protected]", this method will
+     * return "nobody".
+     *
+     * @return the user info for this URI or null if not present
+     */
+    @Nullable
+    public abstract String getEncodedUserInfo();
+
+    /**
+     * Gets the encoded host from the authority for this URI. For example,
+     * if the authority is "[email protected]", this method will return
+     * "google.com".
+     *
+     * @return the host for this URI or null if not present
+     */
+    @Nullable
+    public abstract String getHost();
+
+    /**
+     * Gets the port from the authority for this URI. For example,
+     * if the authority is "google.com:80", this method will return 80.
+     *
+     * @return the port for this URI or -1 if invalid or not present
+     */
+    public abstract int getPort();
+
+    /**
+     * Gets the decoded path.
+     *
+     * @return the decoded path, or null if this is not a hierarchical URI
+     * (like "mailto:[email protected]") or the URI is invalid
+     */
+    @Nullable
+    public abstract String getPath();
+
+    /**
+     * Gets the encoded path.
+     *
+     * @return the encoded path, or null if this is not a hierarchical URI
+     * (like "mailto:[email protected]") or the URI is invalid
+     */
+    @Nullable
+    public abstract String getEncodedPath();
+
+    /**
+     * Gets the decoded query component from this URI. The query comes after
+     * the query separator ('?') and before the fragment separator ('#'). This
+     * method would return "q=android" for
+     * "http://www.google.com/search?q=android".
+     *
+     * @return the decoded query or null if there isn't one
+     */
+    @Nullable
+    public abstract String getQuery();
+
+    /**
+     * Gets the encoded query component from this URI. The query comes after
+     * the query separator ('?') and before the fragment separator ('#'). This
+     * method would return "q=android" for
+     * "http://www.google.com/search?q=android".
+     *
+     * @return the encoded query or null if there isn't one
+     */
+    @Nullable
+    public abstract String getEncodedQuery();
+
+    /**
+     * Gets the decoded fragment part of this URI, everything after the '#'.
+     *
+     * @return the decoded fragment or null if there isn't one
+     */
+    @Nullable
+    public abstract String getFragment();
+
+    /**
+     * Gets the encoded fragment part of this URI, everything after the '#'.
+     *
+     * @return the encoded fragment or null if there isn't one
+     */
+    @Nullable
+    public abstract String getEncodedFragment();
+
+    /**
+     * Gets the decoded path segments.
+     *
+     * @return decoded path segments, each without a leading or trailing '/'
+     */
+    public abstract List<String> getPathSegments();
+
+    /**
+     * Gets the decoded last segment in the path.
+     *
+     * @return the decoded last segment or null if the path is empty
+     */
+    @Nullable
+    public abstract String getLastPathSegment();
+
+    /**
+     * Compares this Uri to another object for equality. Returns true if the
+     * encoded string representations of this Uri and the given Uri are
+     * equal. Case counts. Paths are not normalized. If one Uri specifies a
+     * default port explicitly and the other leaves it implicit, they will not
+     * be considered equal.
+     */
+    public boolean equals(Object o) {
+        if (!(o instanceof Uri)) {
+            return false;
+        }
+
+        Uri other = (Uri) o;
+
+        return toString().equals(other.toString());
+    }
+
+    /**
+     * Hashes the encoded string represention of this Uri consistently with
+     * {@link #equals(Object)}.
+     */
+    public int hashCode() {
+        return toString().hashCode();
+    }
+
+    /**
+     * Compares the string representation of this Uri with that of
+     * another.
+     */
+    public int compareTo(Uri other) {
+        return toString().compareTo(other.toString());
+    }
+
+    /**
+     * Returns the encoded string representation of this URI.
+     * Example: "http://google.com/"
+     */
+    public abstract String toString();
+
+    /**
+     * Return a string representation of this URI that has common forms of PII redacted,
+     * making it safer to use for logging purposes.  For example, {@code tel:800-466-4411} is
+     * returned as {@code tel:xxx-xxx-xxxx} and {@code http://example.com/path/to/item/} is
+     * returned as {@code http://example.com/...}.
+     * @return the common forms PII redacted string of this URI
+     * @hide
+     */
+    @SystemApi
+    public @NonNull String toSafeString() {
+        String scheme = getScheme();
+        String ssp = getSchemeSpecificPart();
+        if (scheme != null) {
+            if (scheme.equalsIgnoreCase("tel") || scheme.equalsIgnoreCase("sip")
+                    || scheme.equalsIgnoreCase("sms") || scheme.equalsIgnoreCase("smsto")
+                    || scheme.equalsIgnoreCase("mailto") || scheme.equalsIgnoreCase("nfc")) {
+                StringBuilder builder = new StringBuilder(64);
+                builder.append(scheme);
+                builder.append(':');
+                if (ssp != null) {
+                    for (int i=0; i<ssp.length(); i++) {
+                        char c = ssp.charAt(i);
+                        if (c == '-' || c == '@' || c == '.') {
+                            builder.append(c);
+                        } else {
+                            builder.append('x');
+                        }
+                    }
+                }
+                return builder.toString();
+            } else if (scheme.equalsIgnoreCase("http") || scheme.equalsIgnoreCase("https")
+                    || scheme.equalsIgnoreCase("ftp") || scheme.equalsIgnoreCase("rtsp")) {
+                ssp = "//" + ((getHost() != null) ? getHost() : "")
+                        + ((getPort() != -1) ? (":" + getPort()) : "")
+                        + "/...";
+            }
+        }
+        // Not a sensitive scheme, but let's still be conservative about
+        // the data we include -- only the ssp, not the query params or
+        // fragment, because those can often have sensitive info.
+        StringBuilder builder = new StringBuilder(64);
+        if (scheme != null) {
+            builder.append(scheme);
+            builder.append(':');
+        }
+        if (ssp != null) {
+            builder.append(ssp);
+        }
+        return builder.toString();
+    }
+
+    /**
+     * Constructs a new builder, copying the attributes from this Uri.
+     */
+    public abstract Builder buildUpon();
+
+    /** Index of a component which was not found. */
+    private final static int NOT_FOUND = -1;
+
+    /** Placeholder value for an index which hasn't been calculated yet. */
+    private final static int NOT_CALCULATED = -2;
+
+    /**
+     * Error message presented when a user tries to treat an opaque URI as
+     * hierarchical.
+     */
+    private static final String NOT_HIERARCHICAL
+            = "This isn't a hierarchical URI.";
+
+    /** Default encoding. */
+    private static final String DEFAULT_ENCODING = "UTF-8";
+
+    /**
+     * Creates a Uri which parses the given encoded URI string.
+     *
+     * @param uriString an RFC 2396-compliant, encoded URI
+     * @throws NullPointerException if uriString is null
+     * @return Uri for this given uri string
+     */
+    public static Uri parse(String uriString) {
+        return new StringUri(uriString);
+    }
+
+    /**
+     * Creates a Uri from a file. The URI has the form
+     * "file://<absolute path>". Encodes path characters with the exception of
+     * '/'.
+     *
+     * <p>Example: "file:///tmp/android.txt"
+     *
+     * @throws NullPointerException if file is null
+     * @return a Uri for the given file
+     */
+    public static Uri fromFile(File file) {
+        if (file == null) {
+            throw new NullPointerException("file");
+        }
+
+        PathPart path = PathPart.fromDecoded(file.getAbsolutePath());
+        return new HierarchicalUri(
+                "file", Part.EMPTY, path, Part.NULL, Part.NULL);
+    }
+
+    /**
+     * An implementation which wraps a String URI. This URI can be opaque or
+     * hierarchical, but we extend AbstractHierarchicalUri in case we need
+     * the hierarchical functionality.
+     */
+    private static class StringUri extends AbstractHierarchicalUri {
+
+        /** Used in parcelling. */
+        static final int TYPE_ID = 1;
+
+        /** URI string representation. */
+        private final String uriString;
+
+        private StringUri(String uriString) {
+            if (uriString == null) {
+                throw new NullPointerException("uriString");
+            }
+
+            this.uriString = uriString;
+        }
+
+        static Uri readFrom(Parcel parcel) {
+            return new StringUri(parcel.readString());
+        }
+
+        public int describeContents() {
+            return 0;
+        }
+
+        public void writeToParcel(Parcel parcel, int flags) {
+            parcel.writeInt(TYPE_ID);
+            parcel.writeString(uriString);
+        }
+
+        /** Cached scheme separator index. */
+        private volatile int cachedSsi = NOT_CALCULATED;
+
+        /** Finds the first ':'. Returns -1 if none found. */
+        private int findSchemeSeparator() {
+            return cachedSsi == NOT_CALCULATED
+                    ? cachedSsi = uriString.indexOf(':')
+                    : cachedSsi;
+        }
+
+        /** Cached fragment separator index. */
+        private volatile int cachedFsi = NOT_CALCULATED;
+
+        /** Finds the first '#'. Returns -1 if none found. */
+        private int findFragmentSeparator() {
+            return cachedFsi == NOT_CALCULATED
+                    ? cachedFsi = uriString.indexOf('#', findSchemeSeparator())
+                    : cachedFsi;
+        }
+
+        public boolean isHierarchical() {
+            int ssi = findSchemeSeparator();
+
+            if (ssi == NOT_FOUND) {
+                // All relative URIs are hierarchical.
+                return true;
+            }
+
+            if (uriString.length() == ssi + 1) {
+                // No ssp.
+                return false;
+            }
+
+            // If the ssp starts with a '/', this is hierarchical.
+            return uriString.charAt(ssi + 1) == '/';
+        }
+
+        public boolean isRelative() {
+            // Note: We return true if the index is 0
+            return findSchemeSeparator() == NOT_FOUND;
+        }
+
+        private volatile String scheme = NOT_CACHED;
+
+        public String getScheme() {
+            @SuppressWarnings("StringEquality")
+            boolean cached = (scheme != NOT_CACHED);
+            return cached ? scheme : (scheme = parseScheme());
+        }
+
+        private String parseScheme() {
+            int ssi = findSchemeSeparator();
+            return ssi == NOT_FOUND ? null : uriString.substring(0, ssi);
+        }
+
+        private Part ssp;
+
+        private Part getSsp() {
+            return ssp == null ? ssp = Part.fromEncoded(parseSsp()) : ssp;
+        }
+
+        public String getEncodedSchemeSpecificPart() {
+            return getSsp().getEncoded();
+        }
+
+        public String getSchemeSpecificPart() {
+            return getSsp().getDecoded();
+        }
+
+        private String parseSsp() {
+            int ssi = findSchemeSeparator();
+            int fsi = findFragmentSeparator();
+
+            // Return everything between ssi and fsi.
+            return fsi == NOT_FOUND
+                    ? uriString.substring(ssi + 1)
+                    : uriString.substring(ssi + 1, fsi);
+        }
+
+        private Part authority;
+
+        private Part getAuthorityPart() {
+            if (authority == null) {
+                String encodedAuthority
+                        = parseAuthority(this.uriString, findSchemeSeparator());
+                return authority = Part.fromEncoded(encodedAuthority);
+            }
+
+            return authority;
+        }
+
+        public String getEncodedAuthority() {
+            return getAuthorityPart().getEncoded();
+        }
+
+        public String getAuthority() {
+            return getAuthorityPart().getDecoded();
+        }
+
+        private PathPart path;
+
+        private PathPart getPathPart() {
+            return path == null
+                    ? path = PathPart.fromEncoded(parsePath())
+                    : path;
+        }
+
+        public String getPath() {
+            return getPathPart().getDecoded();
+        }
+
+        public String getEncodedPath() {
+            return getPathPart().getEncoded();
+        }
+
+        public List<String> getPathSegments() {
+            return getPathPart().getPathSegments();
+        }
+
+        private String parsePath() {
+            String uriString = this.uriString;
+            int ssi = findSchemeSeparator();
+
+            // If the URI is absolute.
+            if (ssi > -1) {
+                // Is there anything after the ':'?
+                boolean schemeOnly = ssi + 1 == uriString.length();
+                if (schemeOnly) {
+                    // Opaque URI.
+                    return null;
+                }
+
+                // A '/' after the ':' means this is hierarchical.
+                if (uriString.charAt(ssi + 1) != '/') {
+                    // Opaque URI.
+                    return null;
+                }
+            } else {
+                // All relative URIs are hierarchical.
+            }
+
+            return parsePath(uriString, ssi);
+        }
+
+        private Part query;
+
+        private Part getQueryPart() {
+            return query == null
+                    ? query = Part.fromEncoded(parseQuery()) : query;
+        }
+
+        public String getEncodedQuery() {
+            return getQueryPart().getEncoded();
+        }
+
+        private String parseQuery() {
+            // It doesn't make sense to cache this index. We only ever
+            // calculate it once.
+            int qsi = uriString.indexOf('?', findSchemeSeparator());
+            if (qsi == NOT_FOUND) {
+                return null;
+            }
+
+            int fsi = findFragmentSeparator();
+
+            if (fsi == NOT_FOUND) {
+                return uriString.substring(qsi + 1);
+            }
+
+            if (fsi < qsi) {
+                // Invalid.
+                return null;
+            }
+
+            return uriString.substring(qsi + 1, fsi);
+        }
+
+        public String getQuery() {
+            return getQueryPart().getDecoded();
+        }
+
+        private Part fragment;
+
+        private Part getFragmentPart() {
+            return fragment == null
+                    ? fragment = Part.fromEncoded(parseFragment()) : fragment;
+        }
+
+        public String getEncodedFragment() {
+            return getFragmentPart().getEncoded();
+        }
+
+        private String parseFragment() {
+            int fsi = findFragmentSeparator();
+            return fsi == NOT_FOUND ? null : uriString.substring(fsi + 1);
+        }
+
+        public String getFragment() {
+            return getFragmentPart().getDecoded();
+        }
+
+        public String toString() {
+            return uriString;
+        }
+
+        /**
+         * Parses an authority out of the given URI string.
+         *
+         * @param uriString URI string
+         * @param ssi scheme separator index, -1 for a relative URI
+         *
+         * @return the authority or null if none is found
+         */
+        static String parseAuthority(String uriString, int ssi) {
+            int length = uriString.length();
+
+            // If "//" follows the scheme separator, we have an authority.
+            if (length > ssi + 2
+                    && uriString.charAt(ssi + 1) == '/'
+                    && uriString.charAt(ssi + 2) == '/') {
+                // We have an authority.
+
+                // Look for the start of the path, query, or fragment, or the
+                // end of the string.
+                int end = ssi + 3;
+                LOOP: while (end < length) {
+                    switch (uriString.charAt(end)) {
+                        case '/': // Start of path
+                        case '\\':// Start of path
+                          // Per http://url.spec.whatwg.org/#host-state, the \ character
+                          // is treated as if it were a / character when encountered in a
+                          // host
+                        case '?': // Start of query
+                        case '#': // Start of fragment
+                            break LOOP;
+                    }
+                    end++;
+                }
+
+                return uriString.substring(ssi + 3, end);
+            } else {
+                return null;
+            }
+
+        }
+
+        /**
+         * Parses a path out of this given URI string.
+         *
+         * @param uriString URI string
+         * @param ssi scheme separator index, -1 for a relative URI
+         *
+         * @return the path
+         */
+        static String parsePath(String uriString, int ssi) {
+            int length = uriString.length();
+
+            // Find start of path.
+            int pathStart;
+            if (length > ssi + 2
+                    && uriString.charAt(ssi + 1) == '/'
+                    && uriString.charAt(ssi + 2) == '/') {
+                // Skip over authority to path.
+                pathStart = ssi + 3;
+                LOOP: while (pathStart < length) {
+                    switch (uriString.charAt(pathStart)) {
+                        case '?': // Start of query
+                        case '#': // Start of fragment
+                            return ""; // Empty path.
+                        case '/': // Start of path!
+                        case '\\':// Start of path!
+                          // Per http://url.spec.whatwg.org/#host-state, the \ character
+                          // is treated as if it were a / character when encountered in a
+                          // host
+                            break LOOP;
+                    }
+                    pathStart++;
+                }
+            } else {
+                // Path starts immediately after scheme separator.
+                pathStart = ssi + 1;
+            }
+
+            // Find end of path.
+            int pathEnd = pathStart;
+            LOOP: while (pathEnd < length) {
+                switch (uriString.charAt(pathEnd)) {
+                    case '?': // Start of query
+                    case '#': // Start of fragment
+                        break LOOP;
+                }
+                pathEnd++;
+            }
+
+            return uriString.substring(pathStart, pathEnd);
+        }
+
+        public Builder buildUpon() {
+            if (isHierarchical()) {
+                return new Builder()
+                        .scheme(getScheme())
+                        .authority(getAuthorityPart())
+                        .path(getPathPart())
+                        .query(getQueryPart())
+                        .fragment(getFragmentPart());
+            } else {
+                return new Builder()
+                        .scheme(getScheme())
+                        .opaquePart(getSsp())
+                        .fragment(getFragmentPart());
+            }
+        }
+    }
+
+    /**
+     * Creates an opaque Uri from the given components. Encodes the ssp
+     * which means this method cannot be used to create hierarchical URIs.
+     *
+     * @param scheme of the URI
+     * @param ssp scheme-specific-part, everything between the
+     *  scheme separator (':') and the fragment separator ('#'), which will
+     *  get encoded
+     * @param fragment fragment, everything after the '#', null if undefined,
+     *  will get encoded
+     *
+     * @throws NullPointerException if scheme or ssp is null
+     * @return Uri composed of the given scheme, ssp, and fragment
+     *
+     * @see Builder if you don't want the ssp and fragment to be encoded
+     */
+    public static Uri fromParts(String scheme, String ssp,
+            String fragment) {
+        if (scheme == null) {
+            throw new NullPointerException("scheme");
+        }
+        if (ssp == null) {
+            throw new NullPointerException("ssp");
+        }
+
+        return new OpaqueUri(scheme, Part.fromDecoded(ssp),
+                Part.fromDecoded(fragment));
+    }
+
+    /**
+     * Opaque URI.
+     */
+    private static class OpaqueUri extends Uri {
+
+        /** Used in parcelling. */
+        static final int TYPE_ID = 2;
+
+        private final String scheme;
+        private final Part ssp;
+        private final Part fragment;
+
+        private OpaqueUri(String scheme, Part ssp, Part fragment) {
+            this.scheme = scheme;
+            this.ssp = ssp;
+            this.fragment = fragment == null ? Part.NULL : fragment;
+        }
+
+        static Uri readFrom(Parcel parcel) {
+            return new OpaqueUri(
+                parcel.readString(),
+                Part.readFrom(parcel),
+                Part.readFrom(parcel)
+            );
+        }
+
+        public int describeContents() {
+            return 0;
+        }
+
+        public void writeToParcel(Parcel parcel, int flags) {
+            parcel.writeInt(TYPE_ID);
+            parcel.writeString(scheme);
+            ssp.writeTo(parcel);
+            fragment.writeTo(parcel);
+        }
+
+        public boolean isHierarchical() {
+            return false;
+        }
+
+        public boolean isRelative() {
+            return scheme == null;
+        }
+
+        public String getScheme() {
+            return this.scheme;
+        }
+
+        public String getEncodedSchemeSpecificPart() {
+            return ssp.getEncoded();
+        }
+
+        public String getSchemeSpecificPart() {
+            return ssp.getDecoded();
+        }
+
+        public String getAuthority() {
+            return null;
+        }
+
+        public String getEncodedAuthority() {
+            return null;
+        }
+
+        public String getPath() {
+            return null;
+        }
+
+        public String getEncodedPath() {
+            return null;
+        }
+
+        public String getQuery() {
+            return null;
+        }
+
+        public String getEncodedQuery() {
+            return null;
+        }
+
+        public String getFragment() {
+            return fragment.getDecoded();
+        }
+
+        public String getEncodedFragment() {
+            return fragment.getEncoded();
+        }
+
+        public List<String> getPathSegments() {
+            return Collections.emptyList();
+        }
+
+        public String getLastPathSegment() {
+            return null;
+        }
+
+        public String getUserInfo() {
+            return null;
+        }
+
+        public String getEncodedUserInfo() {
+            return null;
+        }
+
+        public String getHost() {
+            return null;
+        }
+
+        public int getPort() {
+            return -1;
+        }
+
+        private volatile String cachedString = NOT_CACHED;
+
+        public String toString() {
+            @SuppressWarnings("StringEquality")
+            boolean cached = cachedString != NOT_CACHED;
+            if (cached) {
+                return cachedString;
+            }
+
+            StringBuilder sb = new StringBuilder();
+
+            sb.append(scheme).append(':');
+            sb.append(getEncodedSchemeSpecificPart());
+
+            if (!fragment.isEmpty()) {
+                sb.append('#').append(fragment.getEncoded());
+            }
+
+            return cachedString = sb.toString();
+        }
+
+        public Builder buildUpon() {
+            return new Builder()
+                    .scheme(this.scheme)
+                    .opaquePart(this.ssp)
+                    .fragment(this.fragment);
+        }
+    }
+
+    /**
+     * Wrapper for path segment array.
+     */
+    static class PathSegments extends AbstractList<String>
+            implements RandomAccess {
+
+        static final PathSegments EMPTY = new PathSegments(null, 0);
+
+        final String[] segments;
+        final int size;
+
+        PathSegments(String[] segments, int size) {
+            this.segments = segments;
+            this.size = size;
+        }
+
+        public String get(int index) {
+            if (index >= size) {
+                throw new IndexOutOfBoundsException();
+            }
+
+            return segments[index];
+        }
+
+        public int size() {
+            return this.size;
+        }
+    }
+
+    /**
+     * Builds PathSegments.
+     */
+    static class PathSegmentsBuilder {
+
+        String[] segments;
+        int size = 0;
+
+        void add(String segment) {
+            if (segments == null) {
+                segments = new String[4];
+            } else if (size + 1 == segments.length) {
+                String[] expanded = new String[segments.length * 2];
+                System.arraycopy(segments, 0, expanded, 0, segments.length);
+                segments = expanded;
+            }
+
+            segments[size++] = segment;
+        }
+
+        PathSegments build() {
+            if (segments == null) {
+                return PathSegments.EMPTY;
+            }
+
+            try {
+                return new PathSegments(segments, size);
+            } finally {
+                // Makes sure this doesn't get reused.
+                segments = null;
+            }
+        }
+    }
+
+    /**
+     * Support for hierarchical URIs.
+     */
+    private abstract static class AbstractHierarchicalUri extends Uri {
+
+        public String getLastPathSegment() {
+            // TODO: If we haven't parsed all of the segments already, just
+            // grab the last one directly so we only allocate one string.
+
+            List<String> segments = getPathSegments();
+            int size = segments.size();
+            if (size == 0) {
+                return null;
+            }
+            return segments.get(size - 1);
+        }
+
+        private Part userInfo;
+
+        private Part getUserInfoPart() {
+            return userInfo == null
+                    ? userInfo = Part.fromEncoded(parseUserInfo()) : userInfo;
+        }
+
+        public final String getEncodedUserInfo() {
+            return getUserInfoPart().getEncoded();
+        }
+
+        private String parseUserInfo() {
+            String authority = getEncodedAuthority();
+            if (authority == null) {
+                return null;
+            }
+
+            int end = authority.lastIndexOf('@');
+            return end == NOT_FOUND ? null : authority.substring(0, end);
+        }
+
+        public String getUserInfo() {
+            return getUserInfoPart().getDecoded();
+        }
+
+        private volatile String host = NOT_CACHED;
+
+        public String getHost() {
+            @SuppressWarnings("StringEquality")
+            boolean cached = (host != NOT_CACHED);
+            return cached ? host : (host = parseHost());
+        }
+
+        private String parseHost() {
+            final String authority = getEncodedAuthority();
+            if (authority == null) {
+                return null;
+            }
+
+            // Parse out user info and then port.
+            int userInfoSeparator = authority.lastIndexOf('@');
+            int portSeparator = findPortSeparator(authority);
+
+            String encodedHost = portSeparator == NOT_FOUND
+                    ? authority.substring(userInfoSeparator + 1)
+                    : authority.substring(userInfoSeparator + 1, portSeparator);
+
+            return decode(encodedHost);
+        }
+
+        private volatile int port = NOT_CALCULATED;
+
+        public int getPort() {
+            return port == NOT_CALCULATED
+                    ? port = parsePort()
+                    : port;
+        }
+
+        private int parsePort() {
+            final String authority = getEncodedAuthority();
+            int portSeparator = findPortSeparator(authority);
+            if (portSeparator == NOT_FOUND) {
+                return -1;
+            }
+
+            String portString = decode(authority.substring(portSeparator + 1));
+            try {
+                return Integer.parseInt(portString);
+            } catch (NumberFormatException e) {
+                Log.w(LOG, "Error parsing port string.", e);
+                return -1;
+            }
+        }
+
+        private int findPortSeparator(String authority) {
+            if (authority == null) {
+                return NOT_FOUND;
+            }
+
+            // Reverse search for the ':' character that breaks as soon as a char that is neither
+            // a colon nor an ascii digit is encountered. Thanks to the goodness of UTF-16 encoding,
+            // it's not possible that a surrogate matches one of these, so this loop can just
+            // look for characters rather than care about code points.
+            for (int i = authority.length() - 1; i >= 0; --i) {
+                final int character = authority.charAt(i);
+                if (':' == character) return i;
+                // Character.isDigit would include non-ascii digits
+                if (character < '0' || character > '9') return NOT_FOUND;
+            }
+            return NOT_FOUND;
+        }
+    }
+
+    /**
+     * Hierarchical Uri.
+     */
+    private static class HierarchicalUri extends AbstractHierarchicalUri {
+
+        /** Used in parcelling. */
+        static final int TYPE_ID = 3;
+
+        private final String scheme; // can be null
+        private final Part authority;
+        private final PathPart path;
+        private final Part query;
+        private final Part fragment;
+
+        private HierarchicalUri(String scheme, Part authority, PathPart path,
+                Part query, Part fragment) {
+            this.scheme = scheme;
+            this.authority = Part.nonNull(authority);
+            this.path = path == null ? PathPart.NULL : path;
+            this.query = Part.nonNull(query);
+            this.fragment = Part.nonNull(fragment);
+        }
+
+        static Uri readFrom(Parcel parcel) {
+            return new HierarchicalUri(
+                parcel.readString(),
+                Part.readFrom(parcel),
+                PathPart.readFrom(parcel),
+                Part.readFrom(parcel),
+                Part.readFrom(parcel)
+            );
+        }
+
+        public int describeContents() {
+            return 0;
+        }
+
+        public void writeToParcel(Parcel parcel, int flags) {
+            parcel.writeInt(TYPE_ID);
+            parcel.writeString(scheme);
+            authority.writeTo(parcel);
+            path.writeTo(parcel);
+            query.writeTo(parcel);
+            fragment.writeTo(parcel);
+        }
+
+        public boolean isHierarchical() {
+            return true;
+        }
+
+        public boolean isRelative() {
+            return scheme == null;
+        }
+
+        public String getScheme() {
+            return scheme;
+        }
+
+        private Part ssp;
+
+        private Part getSsp() {
+            return ssp == null
+                    ? ssp = Part.fromEncoded(makeSchemeSpecificPart()) : ssp;
+        }
+
+        public String getEncodedSchemeSpecificPart() {
+            return getSsp().getEncoded();
+        }
+
+        public String getSchemeSpecificPart() {
+            return getSsp().getDecoded();
+        }
+
+        /**
+         * Creates the encoded scheme-specific part from its sub parts.
+         */
+        private String makeSchemeSpecificPart() {
+            StringBuilder builder = new StringBuilder();
+            appendSspTo(builder);
+            return builder.toString();
+        }
+
+        private void appendSspTo(StringBuilder builder) {
+            String encodedAuthority = authority.getEncoded();
+            if (encodedAuthority != null) {
+                // Even if the authority is "", we still want to append "//".
+                builder.append("//").append(encodedAuthority);
+            }
+
+            String encodedPath = path.getEncoded();
+            if (encodedPath != null) {
+                builder.append(encodedPath);
+            }
+
+            if (!query.isEmpty()) {
+                builder.append('?').append(query.getEncoded());
+            }
+        }
+
+        public String getAuthority() {
+            return this.authority.getDecoded();
+        }
+
+        public String getEncodedAuthority() {
+            return this.authority.getEncoded();
+        }
+
+        public String getEncodedPath() {
+            return this.path.getEncoded();
+        }
+
+        public String getPath() {
+            return this.path.getDecoded();
+        }
+
+        public String getQuery() {
+            return this.query.getDecoded();
+        }
+
+        public String getEncodedQuery() {
+            return this.query.getEncoded();
+        }
+
+        public String getFragment() {
+            return this.fragment.getDecoded();
+        }
+
+        public String getEncodedFragment() {
+            return this.fragment.getEncoded();
+        }
+
+        public List<String> getPathSegments() {
+            return this.path.getPathSegments();
+        }
+
+        private volatile String uriString = NOT_CACHED;
+
+        @Override
+        public String toString() {
+            @SuppressWarnings("StringEquality")
+            boolean cached = (uriString != NOT_CACHED);
+            return cached ? uriString
+                    : (uriString = makeUriString());
+        }
+
+        private String makeUriString() {
+            StringBuilder builder = new StringBuilder();
+
+            if (scheme != null) {
+                builder.append(scheme).append(':');
+            }
+
+            appendSspTo(builder);
+
+            if (!fragment.isEmpty()) {
+                builder.append('#').append(fragment.getEncoded());
+            }
+
+            return builder.toString();
+        }
+
+        public Builder buildUpon() {
+            return new Builder()
+                    .scheme(scheme)
+                    .authority(authority)
+                    .path(path)
+                    .query(query)
+                    .fragment(fragment);
+        }
+    }
+
+    /**
+     * Helper class for building or manipulating URI references. Not safe for
+     * concurrent use.
+     *
+     * <p>An absolute hierarchical URI reference follows the pattern:
+     * {@code <scheme>://<authority><absolute path>?<query>#<fragment>}
+     *
+     * <p>Relative URI references (which are always hierarchical) follow one
+     * of two patterns: {@code <relative or absolute path>?<query>#<fragment>}
+     * or {@code //<authority><absolute path>?<query>#<fragment>}
+     *
+     * <p>An opaque URI follows this pattern:
+     * {@code <scheme>:<opaque part>#<fragment>}
+     *
+     * <p>Use {@link Uri#buildUpon()} to obtain a builder representing an existing URI.
+     */
+    public static final class Builder {
+
+        private String scheme;
+        private Part opaquePart;
+        private Part authority;
+        private PathPart path;
+        private Part query;
+        private Part fragment;
+
+        /**
+         * Constructs a new Builder.
+         */
+        public Builder() {}
+
+        /**
+         * Sets the scheme.
+         *
+         * @param scheme name or {@code null} if this is a relative Uri
+         */
+        public Builder scheme(String scheme) {
+            this.scheme = scheme;
+            return this;
+        }
+
+        Builder opaquePart(Part opaquePart) {
+            this.opaquePart = opaquePart;
+            return this;
+        }
+
+        /**
+         * Encodes and sets the given opaque scheme-specific-part.
+         *
+         * @param opaquePart decoded opaque part
+         */
+        public Builder opaquePart(String opaquePart) {
+            return opaquePart(Part.fromDecoded(opaquePart));
+        }
+
+        /**
+         * Sets the previously encoded opaque scheme-specific-part.
+         *
+         * @param opaquePart encoded opaque part
+         */
+        public Builder encodedOpaquePart(String opaquePart) {
+            return opaquePart(Part.fromEncoded(opaquePart));
+        }
+
+        Builder authority(Part authority) {
+            // This URI will be hierarchical.
+            this.opaquePart = null;
+
+            this.authority = authority;
+            return this;
+        }
+
+        /**
+         * Encodes and sets the authority.
+         */
+        public Builder authority(String authority) {
+            return authority(Part.fromDecoded(authority));
+        }
+
+        /**
+         * Sets the previously encoded authority.
+         */
+        public Builder encodedAuthority(String authority) {
+            return authority(Part.fromEncoded(authority));
+        }
+
+        Builder path(PathPart path) {
+            // This URI will be hierarchical.
+            this.opaquePart = null;
+
+            this.path = path;
+            return this;
+        }
+
+        /**
+         * Sets the path. Leaves '/' characters intact but encodes others as
+         * necessary.
+         *
+         * <p>If the path is not null and doesn't start with a '/', and if
+         * you specify a scheme and/or authority, the builder will prepend the
+         * given path with a '/'.
+         */
+        public Builder path(String path) {
+            return path(PathPart.fromDecoded(path));
+        }
+
+        /**
+         * Sets the previously encoded path.
+         *
+         * <p>If the path is not null and doesn't start with a '/', and if
+         * you specify a scheme and/or authority, the builder will prepend the
+         * given path with a '/'.
+         */
+        public Builder encodedPath(String path) {
+            return path(PathPart.fromEncoded(path));
+        }
+
+        /**
+         * Encodes the given segment and appends it to the path.
+         */
+        public Builder appendPath(String newSegment) {
+            return path(PathPart.appendDecodedSegment(path, newSegment));
+        }
+
+        /**
+         * Appends the given segment to the path.
+         */
+        public Builder appendEncodedPath(String newSegment) {
+            return path(PathPart.appendEncodedSegment(path, newSegment));
+        }
+
+        Builder query(Part query) {
+            // This URI will be hierarchical.
+            this.opaquePart = null;
+
+            this.query = query;
+            return this;
+        }
+
+        /**
+         * Encodes and sets the query.
+         */
+        public Builder query(String query) {
+            return query(Part.fromDecoded(query));
+        }
+
+        /**
+         * Sets the previously encoded query.
+         */
+        public Builder encodedQuery(String query) {
+            return query(Part.fromEncoded(query));
+        }
+
+        Builder fragment(Part fragment) {
+            this.fragment = fragment;
+            return this;
+        }
+
+        /**
+         * Encodes and sets the fragment.
+         */
+        public Builder fragment(String fragment) {
+            return fragment(Part.fromDecoded(fragment));
+        }
+
+        /**
+         * Sets the previously encoded fragment.
+         */
+        public Builder encodedFragment(String fragment) {
+            return fragment(Part.fromEncoded(fragment));
+        }
+
+        /**
+         * Encodes the key and value and then appends the parameter to the
+         * query string.
+         *
+         * @param key which will be encoded
+         * @param value which will be encoded
+         */
+        public Builder appendQueryParameter(String key, String value) {
+            // This URI will be hierarchical.
+            this.opaquePart = null;
+
+            String encodedParameter = encode(key, null) + "="
+                    + encode(value, null);
+
+            if (query == null) {
+                query = Part.fromEncoded(encodedParameter);
+                return this;
+            }
+
+            String oldQuery = query.getEncoded();
+            if (oldQuery == null || oldQuery.length() == 0) {
+                query = Part.fromEncoded(encodedParameter);
+            } else {
+                query = Part.fromEncoded(oldQuery + "&" + encodedParameter);
+            }
+
+            return this;
+        }
+
+        /**
+         * Clears the the previously set query.
+         */
+        public Builder clearQuery() {
+          return query((Part) null);
+        }
+
+        /**
+         * Constructs a Uri with the current attributes.
+         *
+         * @throws UnsupportedOperationException if the URI is opaque and the
+         *  scheme is null
+         */
+        public Uri build() {
+            if (opaquePart != null) {
+                if (this.scheme == null) {
+                    throw new UnsupportedOperationException(
+                            "An opaque URI must have a scheme.");
+                }
+
+                return new OpaqueUri(scheme, opaquePart, fragment);
+            } else {
+                // Hierarchical URIs should not return null for getPath().
+                PathPart path = this.path;
+                if (path == null || path == PathPart.NULL) {
+                    path = PathPart.EMPTY;
+                } else {
+                    // If we have a scheme and/or authority, the path must
+                    // be absolute. Prepend it with a '/' if necessary.
+                    if (hasSchemeOrAuthority()) {
+                        path = PathPart.makeAbsolute(path);
+                    }
+                }
+
+                return new HierarchicalUri(
+                        scheme, authority, path, query, fragment);
+            }
+        }
+
+        private boolean hasSchemeOrAuthority() {
+            return scheme != null
+                    || (authority != null && authority != Part.NULL);
+
+        }
+
+        @Override
+        public String toString() {
+            return build().toString();
+        }
+    }
+
+    /**
+     * Returns a set of the unique names of all query parameters. Iterating
+     * over the set will return the names in order of their first occurrence.
+     *
+     * @throws UnsupportedOperationException if this isn't a hierarchical URI
+     *
+     * @return a set of decoded names
+     */
+    public Set<String> getQueryParameterNames() {
+        if (isOpaque()) {
+            throw new UnsupportedOperationException(NOT_HIERARCHICAL);
+        }
+
+        String query = getEncodedQuery();
+        if (query == null) {
+            return Collections.emptySet();
+        }
+
+        Set<String> names = new LinkedHashSet<String>();
+        int start = 0;
+        do {
+            int next = query.indexOf('&', start);
+            int end = (next == -1) ? query.length() : next;
+
+            int separator = query.indexOf('=', start);
+            if (separator > end || separator == -1) {
+                separator = end;
+            }
+
+            String name = query.substring(start, separator);
+            names.add(decode(name));
+
+            // Move start to end of name.
+            start = end + 1;
+        } while (start < query.length());
+
+        return Collections.unmodifiableSet(names);
+    }
+
+    /**
+     * Searches the query string for parameter values with the given key.
+     *
+     * @param key which will be encoded
+     *
+     * @throws UnsupportedOperationException if this isn't a hierarchical URI
+     * @throws NullPointerException if key is null
+     * @return a list of decoded values
+     */
+    public List<String> getQueryParameters(String key) {
+        if (isOpaque()) {
+            throw new UnsupportedOperationException(NOT_HIERARCHICAL);
+        }
+        if (key == null) {
+          throw new NullPointerException("key");
+        }
+
+        String query = getEncodedQuery();
+        if (query == null) {
+            return Collections.emptyList();
+        }
+
+        String encodedKey;
+        try {
+            encodedKey = URLEncoder.encode(key, DEFAULT_ENCODING);
+        } catch (UnsupportedEncodingException e) {
+            throw new AssertionError(e);
+        }
+
+        ArrayList<String> values = new ArrayList<String>();
+
+        int start = 0;
+        do {
+            int nextAmpersand = query.indexOf('&', start);
+            int end = nextAmpersand != -1 ? nextAmpersand : query.length();
+
+            int separator = query.indexOf('=', start);
+            if (separator > end || separator == -1) {
+                separator = end;
+            }
+
+            if (separator - start == encodedKey.length()
+                    && query.regionMatches(start, encodedKey, 0, encodedKey.length())) {
+                if (separator == end) {
+                  values.add("");
+                } else {
+                  values.add(decode(query.substring(separator + 1, end)));
+                }
+            }
+
+            // Move start to end of name.
+            if (nextAmpersand != -1) {
+                start = nextAmpersand + 1;
+            } else {
+                break;
+            }
+        } while (true);
+
+        return Collections.unmodifiableList(values);
+    }
+
+    /**
+     * Searches the query string for the first value with the given key.
+     *
+     * <p><strong>Warning:</strong> Prior to Jelly Bean, this decoded
+     * the '+' character as '+' rather than ' '.
+     *
+     * @param key which will be encoded
+     * @throws UnsupportedOperationException if this isn't a hierarchical URI
+     * @throws NullPointerException if key is null
+     * @return the decoded value or null if no parameter is found
+     */
+    @Nullable
+    public String getQueryParameter(String key) {
+        if (isOpaque()) {
+            throw new UnsupportedOperationException(NOT_HIERARCHICAL);
+        }
+        if (key == null) {
+            throw new NullPointerException("key");
+        }
+
+        final String query = getEncodedQuery();
+        if (query == null) {
+            return null;
+        }
+
+        final String encodedKey = encode(key, null);
+        final int length = query.length();
+        int start = 0;
+        do {
+            int nextAmpersand = query.indexOf('&', start);
+            int end = nextAmpersand != -1 ? nextAmpersand : length;
+
+            int separator = query.indexOf('=', start);
+            if (separator > end || separator == -1) {
+                separator = end;
+            }
+
+            if (separator - start == encodedKey.length()
+                    && query.regionMatches(start, encodedKey, 0, encodedKey.length())) {
+                if (separator == end) {
+                    return "";
+                } else {
+                    String encodedValue = query.substring(separator + 1, end);
+                    return UriCodec.decode(encodedValue, true, StandardCharsets.UTF_8, false);
+                }
+            }
+
+            // Move start to end of name.
+            if (nextAmpersand != -1) {
+                start = nextAmpersand + 1;
+            } else {
+                break;
+            }
+        } while (true);
+        return null;
+    }
+
+    /**
+     * Searches the query string for the first value with the given key and interprets it
+     * as a boolean value. "false" and "0" are interpreted as <code>false</code>, everything
+     * else is interpreted as <code>true</code>.
+     *
+     * @param key which will be decoded
+     * @param defaultValue the default value to return if there is no query parameter for key
+     * @return the boolean interpretation of the query parameter key
+     */
+    public boolean getBooleanQueryParameter(String key, boolean defaultValue) {
+        String flag = getQueryParameter(key);
+        if (flag == null) {
+            return defaultValue;
+        }
+        flag = flag.toLowerCase(Locale.ROOT);
+        return (!"false".equals(flag) && !"0".equals(flag));
+    }
+
+    /**
+     * Return an equivalent URI with a lowercase scheme component.
+     * This aligns the Uri with Android best practices for
+     * intent filtering.
+     *
+     * <p>For example, "HTTP://www.android.com" becomes
+     * "http://www.android.com"
+     *
+     * <p>All URIs received from outside Android (such as user input,
+     * or external sources like Bluetooth, NFC, or the Internet) should
+     * be normalized before they are used to create an Intent.
+     *
+     * <p class="note">This method does <em>not</em> validate bad URI's,
+     * or 'fix' poorly formatted URI's - so do not use it for input validation.
+     * A Uri will always be returned, even if the Uri is badly formatted to
+     * begin with and a scheme component cannot be found.
+     *
+     * @return normalized Uri (never null)
+     * @see android.content.Intent#setData
+     * @see android.content.Intent#setDataAndNormalize
+     */
+    public Uri normalizeScheme() {
+        String scheme = getScheme();
+        if (scheme == null) return this;  // give up
+        String lowerScheme = scheme.toLowerCase(Locale.ROOT);
+        if (scheme.equals(lowerScheme)) return this;  // no change
+
+        return buildUpon().scheme(lowerScheme).build();
+    }
+
+    /** Identifies a null parcelled Uri. */
+    private static final int NULL_TYPE_ID = 0;
+
+    /**
+     * Reads Uris from Parcels.
+     */
+    public static final @android.annotation.NonNull Parcelable.Creator<Uri> CREATOR
+            = new Parcelable.Creator<Uri>() {
+        public Uri createFromParcel(Parcel in) {
+            int type = in.readInt();
+            switch (type) {
+                case NULL_TYPE_ID: return null;
+                case StringUri.TYPE_ID: return StringUri.readFrom(in);
+                case OpaqueUri.TYPE_ID: return OpaqueUri.readFrom(in);
+                case HierarchicalUri.TYPE_ID:
+                    return HierarchicalUri.readFrom(in);
+            }
+
+            throw new IllegalArgumentException("Unknown URI type: " + type);
+        }
+
+        public Uri[] newArray(int size) {
+            return new Uri[size];
+        }
+    };
+
+    /**
+     * Writes a Uri to a Parcel.
+     *
+     * @param out parcel to write to
+     * @param uri to write, can be null
+     */
+    public static void writeToParcel(Parcel out, Uri uri) {
+        if (uri == null) {
+            out.writeInt(NULL_TYPE_ID);
+        } else {
+            uri.writeToParcel(out, 0);
+        }
+    }
+
+    private static final char[] HEX_DIGITS = "0123456789ABCDEF".toCharArray();
+
+    /**
+     * Encodes characters in the given string as '%'-escaped octets
+     * using the UTF-8 scheme. Leaves letters ("A-Z", "a-z"), numbers
+     * ("0-9"), and unreserved characters ("_-!.~'()*") intact. Encodes
+     * all other characters.
+     *
+     * @param s string to encode
+     * @return an encoded version of s suitable for use as a URI component,
+     *  or null if s is null
+     */
+    public static String encode(String s) {
+        return encode(s, null);
+    }
+
+    /**
+     * Encodes characters in the given string as '%'-escaped octets
+     * using the UTF-8 scheme. Leaves letters ("A-Z", "a-z"), numbers
+     * ("0-9"), and unreserved characters ("_-!.~'()*") intact. Encodes
+     * all other characters with the exception of those specified in the
+     * allow argument.
+     *
+     * @param s string to encode
+     * @param allow set of additional characters to allow in the encoded form,
+     *  null if no characters should be skipped
+     * @return an encoded version of s suitable for use as a URI component,
+     *  or null if s is null
+     */
+    public static String encode(String s, String allow) {
+        if (s == null) {
+            return null;
+        }
+
+        // Lazily-initialized buffers.
+        StringBuilder encoded = null;
+
+        int oldLength = s.length();
+
+        // This loop alternates between copying over allowed characters and
+        // encoding in chunks. This results in fewer method calls and
+        // allocations than encoding one character at a time.
+        int current = 0;
+        while (current < oldLength) {
+            // Start in "copying" mode where we copy over allowed chars.
+
+            // Find the next character which needs to be encoded.
+            int nextToEncode = current;
+            while (nextToEncode < oldLength
+                    && isAllowed(s.charAt(nextToEncode), allow)) {
+                nextToEncode++;
+            }
+
+            // If there's nothing more to encode...
+            if (nextToEncode == oldLength) {
+                if (current == 0) {
+                    // We didn't need to encode anything!
+                    return s;
+                } else {
+                    // Presumably, we've already done some encoding.
+                    encoded.append(s, current, oldLength);
+                    return encoded.toString();
+                }
+            }
+
+            if (encoded == null) {
+                encoded = new StringBuilder();
+            }
+
+            if (nextToEncode > current) {
+                // Append allowed characters leading up to this point.
+                encoded.append(s, current, nextToEncode);
+            } else {
+                // assert nextToEncode == current
+            }
+
+            // Switch to "encoding" mode.
+
+            // Find the next allowed character.
+            current = nextToEncode;
+            int nextAllowed = current + 1;
+            while (nextAllowed < oldLength
+                    && !isAllowed(s.charAt(nextAllowed), allow)) {
+                nextAllowed++;
+            }
+
+            // Convert the substring to bytes and encode the bytes as
+            // '%'-escaped octets.
+            String toEncode = s.substring(current, nextAllowed);
+            try {
+                byte[] bytes = toEncode.getBytes(DEFAULT_ENCODING);
+                int bytesLength = bytes.length;
+                for (int i = 0; i < bytesLength; i++) {
+                    encoded.append('%');
+                    encoded.append(HEX_DIGITS[(bytes[i] & 0xf0) >> 4]);
+                    encoded.append(HEX_DIGITS[bytes[i] & 0xf]);
+                }
+            } catch (UnsupportedEncodingException e) {
+                throw new AssertionError(e);
+            }
+
+            current = nextAllowed;
+        }
+
+        // Encoded could still be null at this point if s is empty.
+        return encoded == null ? s : encoded.toString();
+    }
+
+    /**
+     * Returns true if the given character is allowed.
+     *
+     * @param c character to check
+     * @param allow characters to allow
+     * @return true if the character is allowed or false if it should be
+     *  encoded
+     */
+    private static boolean isAllowed(char c, String allow) {
+        return (c >= 'A' && c <= 'Z')
+                || (c >= 'a' && c <= 'z')
+                || (c >= '0' && c <= '9')
+                || "_-!.~'()*".indexOf(c) != NOT_FOUND
+                || (allow != null && allow.indexOf(c) != NOT_FOUND);
+    }
+
+    /**
+     * Decodes '%'-escaped octets in the given string using the UTF-8 scheme.
+     * Replaces invalid octets with the unicode replacement character
+     * ("\\uFFFD").
+     *
+     * @param s encoded string to decode
+     * @return the given string with escaped octets decoded, or null if
+     *  s is null
+     */
+    public static String decode(String s) {
+        if (s == null) {
+            return null;
+        }
+        return UriCodec.decode(
+                s, false /* convertPlus */, StandardCharsets.UTF_8, false /* throwOnFailure */);
+    }
+
+    /**
+     * Support for part implementations.
+     */
+    static abstract class AbstractPart {
+
+        /**
+         * Enum which indicates which representation of a given part we have.
+         */
+        static class Representation {
+            static final int BOTH = 0;
+            static final int ENCODED = 1;
+            static final int DECODED = 2;
+        }
+
+        volatile String encoded;
+        volatile String decoded;
+
+        AbstractPart(String encoded, String decoded) {
+            this.encoded = encoded;
+            this.decoded = decoded;
+        }
+
+        abstract String getEncoded();
+
+        final String getDecoded() {
+            @SuppressWarnings("StringEquality")
+            boolean hasDecoded = decoded != NOT_CACHED;
+            return hasDecoded ? decoded : (decoded = decode(encoded));
+        }
+
+        final void writeTo(Parcel parcel) {
+            @SuppressWarnings("StringEquality")
+            boolean hasEncoded = encoded != NOT_CACHED;
+
+            @SuppressWarnings("StringEquality")
+            boolean hasDecoded = decoded != NOT_CACHED;
+
+            if (hasEncoded && hasDecoded) {
+                parcel.writeInt(Representation.BOTH);
+                parcel.writeString(encoded);
+                parcel.writeString(decoded);
+            } else if (hasEncoded) {
+                parcel.writeInt(Representation.ENCODED);
+                parcel.writeString(encoded);
+            } else if (hasDecoded) {
+                parcel.writeInt(Representation.DECODED);
+                parcel.writeString(decoded);
+            } else {
+                throw new IllegalArgumentException("Neither encoded nor decoded");
+            }
+        }
+    }
+
+    /**
+     * Immutable wrapper of encoded and decoded versions of a URI part. Lazily
+     * creates the encoded or decoded version from the other.
+     */
+    static class Part extends AbstractPart {
+
+        /** A part with null values. */
+        static final Part NULL = new EmptyPart(null);
+
+        /** A part with empty strings for values. */
+        static final Part EMPTY = new EmptyPart("");
+
+        private Part(String encoded, String decoded) {
+            super(encoded, decoded);
+        }
+
+        boolean isEmpty() {
+            return false;
+        }
+
+        String getEncoded() {
+            @SuppressWarnings("StringEquality")
+            boolean hasEncoded = encoded != NOT_CACHED;
+            return hasEncoded ? encoded : (encoded = encode(decoded));
+        }
+
+        static Part readFrom(Parcel parcel) {
+            int representation = parcel.readInt();
+            switch (representation) {
+                case Representation.BOTH:
+                    return from(parcel.readString(), parcel.readString());
+                case Representation.ENCODED:
+                    return fromEncoded(parcel.readString());
+                case Representation.DECODED:
+                    return fromDecoded(parcel.readString());
+                default:
+                    throw new IllegalArgumentException("Unknown representation: "
+                            + representation);
+            }
+        }
+
+        /**
+         * Returns given part or {@link #NULL} if the given part is null.
+         */
+        static Part nonNull(Part part) {
+            return part == null ? NULL : part;
+        }
+
+        /**
+         * Creates a part from the encoded string.
+         *
+         * @param encoded part string
+         */
+        static Part fromEncoded(String encoded) {
+            return from(encoded, NOT_CACHED);
+        }
+
+        /**
+         * Creates a part from the decoded string.
+         *
+         * @param decoded part string
+         */
+        static Part fromDecoded(String decoded) {
+            return from(NOT_CACHED, decoded);
+        }
+
+        /**
+         * Creates a part from the encoded and decoded strings.
+         *
+         * @param encoded part string
+         * @param decoded part string
+         */
+        static Part from(String encoded, String decoded) {
+            // We have to check both encoded and decoded in case one is
+            // NOT_CACHED.
+
+            if (encoded == null) {
+                return NULL;
+            }
+            if (encoded.length() == 0) {
+                return EMPTY;
+            }
+
+            if (decoded == null) {
+                return NULL;
+            }
+            if (decoded .length() == 0) {
+                return EMPTY;
+            }
+
+            return new Part(encoded, decoded);
+        }
+
+        private static class EmptyPart extends Part {
+            public EmptyPart(String value) {
+                super(value, value);
+            }
+
+            @Override
+            boolean isEmpty() {
+                return true;
+            }
+        }
+    }
+
+    /**
+     * Immutable wrapper of encoded and decoded versions of a path part. Lazily
+     * creates the encoded or decoded version from the other.
+     */
+    static class PathPart extends AbstractPart {
+
+        /** A part with null values. */
+        static final PathPart NULL = new PathPart(null, null);
+
+        /** A part with empty strings for values. */
+        static final PathPart EMPTY = new PathPart("", "");
+
+        private PathPart(String encoded, String decoded) {
+            super(encoded, decoded);
+        }
+
+        String getEncoded() {
+            @SuppressWarnings("StringEquality")
+            boolean hasEncoded = encoded != NOT_CACHED;
+
+            // Don't encode '/'.
+            return hasEncoded ? encoded : (encoded = encode(decoded, "/"));
+        }
+
+        /**
+         * Cached path segments. This doesn't need to be volatile--we don't
+         * care if other threads see the result.
+         */
+        private PathSegments pathSegments;
+
+        /**
+         * Gets the individual path segments. Parses them if necessary.
+         *
+         * @return parsed path segments or null if this isn't a hierarchical
+         *  URI
+         */
+        PathSegments getPathSegments() {
+            if (pathSegments != null) {
+                return pathSegments;
+            }
+
+            String path = getEncoded();
+            if (path == null) {
+                return pathSegments = PathSegments.EMPTY;
+            }
+
+            PathSegmentsBuilder segmentBuilder = new PathSegmentsBuilder();
+
+            int previous = 0;
+            int current;
+            while ((current = path.indexOf('/', previous)) > -1) {
+                // This check keeps us from adding a segment if the path starts
+                // '/' and an empty segment for "//".
+                if (previous < current) {
+                    String decodedSegment
+                            = decode(path.substring(previous, current));
+                    segmentBuilder.add(decodedSegment);
+                }
+                previous = current + 1;
+            }
+
+            // Add in the final path segment.
+            if (previous < path.length()) {
+                segmentBuilder.add(decode(path.substring(previous)));
+            }
+
+            return pathSegments = segmentBuilder.build();
+        }
+
+        static PathPart appendEncodedSegment(PathPart oldPart,
+                String newSegment) {
+            // If there is no old path, should we make the new path relative
+            // or absolute? I pick absolute.
+
+            if (oldPart == null) {
+                // No old path.
+                return fromEncoded("/" + newSegment);
+            }
+
+            String oldPath = oldPart.getEncoded();
+
+            if (oldPath == null) {
+                oldPath = "";
+            }
+
+            int oldPathLength = oldPath.length();
+            String newPath;
+            if (oldPathLength == 0) {
+                // No old path.
+                newPath = "/" + newSegment;
+            } else if (oldPath.charAt(oldPathLength - 1) == '/') {
+                newPath = oldPath + newSegment;
+            } else {
+                newPath = oldPath + "/" + newSegment;
+            }
+
+            return fromEncoded(newPath);
+        }
+
+        static PathPart appendDecodedSegment(PathPart oldPart, String decoded) {
+            String encoded = encode(decoded);
+
+            // TODO: Should we reuse old PathSegments? Probably not.
+            return appendEncodedSegment(oldPart, encoded);
+        }
+
+        static PathPart readFrom(Parcel parcel) {
+            int representation = parcel.readInt();
+            switch (representation) {
+                case Representation.BOTH:
+                    return from(parcel.readString(), parcel.readString());
+                case Representation.ENCODED:
+                    return fromEncoded(parcel.readString());
+                case Representation.DECODED:
+                    return fromDecoded(parcel.readString());
+                default:
+                    throw new IllegalArgumentException("Bad representation: " + representation);
+            }
+        }
+
+        /**
+         * Creates a path from the encoded string.
+         *
+         * @param encoded part string
+         */
+        static PathPart fromEncoded(String encoded) {
+            return from(encoded, NOT_CACHED);
+        }
+
+        /**
+         * Creates a path from the decoded string.
+         *
+         * @param decoded part string
+         */
+        static PathPart fromDecoded(String decoded) {
+            return from(NOT_CACHED, decoded);
+        }
+
+        /**
+         * Creates a path from the encoded and decoded strings.
+         *
+         * @param encoded part string
+         * @param decoded part string
+         */
+        static PathPart from(String encoded, String decoded) {
+            if (encoded == null) {
+                return NULL;
+            }
+
+            if (encoded.length() == 0) {
+                return EMPTY;
+            }
+
+            return new PathPart(encoded, decoded);
+        }
+
+        /**
+         * Prepends path values with "/" if they're present, not empty, and
+         * they don't already start with "/".
+         */
+        static PathPart makeAbsolute(PathPart oldPart) {
+            @SuppressWarnings("StringEquality")
+            boolean encodedCached = oldPart.encoded != NOT_CACHED;
+
+            // We don't care which version we use, and we don't want to force
+            // unneccessary encoding/decoding.
+            String oldPath = encodedCached ? oldPart.encoded : oldPart.decoded;
+
+            if (oldPath == null || oldPath.length() == 0
+                    || oldPath.startsWith("/")) {
+                return oldPart;
+            }
+
+            // Prepend encoded string if present.
+            String newEncoded = encodedCached
+                    ? "/" + oldPart.encoded : NOT_CACHED;
+
+            // Prepend decoded string if present.
+            @SuppressWarnings("StringEquality")
+            boolean decodedCached = oldPart.decoded != NOT_CACHED;
+            String newDecoded = decodedCached
+                    ? "/" + oldPart.decoded
+                    : NOT_CACHED;
+
+            return new PathPart(newEncoded, newDecoded);
+        }
+    }
+
+    /**
+     * Creates a new Uri by appending an already-encoded path segment to a
+     * base Uri.
+     *
+     * @param baseUri Uri to append path segment to
+     * @param pathSegment encoded path segment to append
+     * @return a new Uri based on baseUri with the given segment appended to
+     *  the path
+     * @throws NullPointerException if baseUri is null
+     */
+    public static Uri withAppendedPath(Uri baseUri, String pathSegment) {
+        Builder builder = baseUri.buildUpon();
+        builder = builder.appendEncodedPath(pathSegment);
+        return builder.build();
+    }
+
+    /**
+     * If this {@link Uri} is {@code file://}, then resolve and return its
+     * canonical path. Also fixes legacy emulated storage paths so they are
+     * usable across user boundaries. Should always be called from the app
+     * process before sending elsewhere.
+     *
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public Uri getCanonicalUri() {
+        if ("file".equals(getScheme())) {
+            final String canonicalPath;
+            try {
+                canonicalPath = new File(getPath()).getCanonicalPath();
+            } catch (IOException e) {
+                return this;
+            }
+
+            if (Environment.isExternalStorageEmulated()) {
+                final String legacyPath = Environment.getLegacyExternalStorageDirectory()
+                        .toString();
+
+                // Splice in user-specific path when legacy path is found
+                if (canonicalPath.startsWith(legacyPath)) {
+                    return Uri.fromFile(new File(
+                            Environment.getExternalStorageDirectory().toString(),
+                            canonicalPath.substring(legacyPath.length() + 1)));
+                }
+            }
+
+            return Uri.fromFile(new File(canonicalPath));
+        } else {
+            return this;
+        }
+    }
+
+    /**
+     * If this is a {@code file://} Uri, it will be reported to
+     * {@link StrictMode}.
+     *
+     * @hide
+     */
+    public void checkFileUriExposed(String location) {
+        if ("file".equals(getScheme())
+                && (getPath() != null) && !getPath().startsWith("/system/")) {
+            StrictMode.onFileUriExposed(this, location);
+        }
+    }
+
+    /**
+     * If this is a {@code content://} Uri without access flags, it will be
+     * reported to {@link StrictMode}.
+     *
+     * @hide
+     */
+    public void checkContentUriWithoutPermission(String location, int flags) {
+        if ("content".equals(getScheme()) && !Intent.isAccessUriMode(flags)) {
+            StrictMode.onContentUriWithoutPermission(this, location);
+        }
+    }
+
+    /**
+     * Test if this is a path prefix match against the given Uri. Verifies that
+     * scheme, authority, and atomic path segments match.
+     *
+     * @hide
+     */
+    public boolean isPathPrefixMatch(Uri prefix) {
+        if (!Objects.equals(getScheme(), prefix.getScheme())) return false;
+        if (!Objects.equals(getAuthority(), prefix.getAuthority())) return false;
+
+        List<String> seg = getPathSegments();
+        List<String> prefixSeg = prefix.getPathSegments();
+
+        final int prefixSize = prefixSeg.size();
+        if (seg.size() < prefixSize) return false;
+
+        for (int i = 0; i < prefixSize; i++) {
+            if (!Objects.equals(seg.get(i), prefixSeg.get(i))) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+}
diff --git a/android/net/UriCodec.java b/android/net/UriCodec.java
new file mode 100644
index 0000000..e1470e0
--- /dev/null
+++ b/android/net/UriCodec.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2015 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 java.net.URISyntaxException;
+import java.nio.ByteBuffer;
+import java.nio.charset.CharacterCodingException;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CodingErrorAction;
+
+/**
+ * Decodes “application/x-www-form-urlencoded” content.
+ *
+ * @hide
+ */
+public final class UriCodec {
+
+    private UriCodec() {}
+
+    /**
+     * Interprets a char as hex digits, returning a number from -1 (invalid char) to 15 ('f').
+     */
+    private static int hexCharToValue(char c) {
+        if ('0' <= c && c <= '9') {
+            return c - '0';
+        }
+        if ('a' <= c && c <= 'f') {
+            return 10 + c - 'a';
+        }
+        if ('A' <= c && c <= 'F') {
+            return 10 + c - 'A';
+        }
+        return -1;
+    }
+
+    private static URISyntaxException unexpectedCharacterException(
+            String uri, String name, char unexpected, int index) {
+        String nameString = (name == null) ? "" :  " in [" + name + "]";
+        return new URISyntaxException(
+                uri, "Unexpected character" + nameString + ": " + unexpected, index);
+    }
+
+    private static char getNextCharacter(String uri, int index, int end, String name)
+             throws URISyntaxException {
+        if (index >= end) {
+            String nameString = (name == null) ? "" :  " in [" + name + "]";
+            throw new URISyntaxException(
+                    uri, "Unexpected end of string" + nameString, index);
+        }
+        return uri.charAt(index);
+    }
+
+    /**
+     * Decode a string according to the rules of this decoder.
+     *
+     * - if {@code convertPlus == true} all ‘+’ chars in the decoded output are converted to ‘ ‘
+     *   (white space)
+     * - if {@code throwOnFailure == true}, an {@link IllegalArgumentException} is thrown for
+     *   invalid inputs. Else, U+FFFd is emitted to the output in place of invalid input octets.
+     */
+    public static String decode(
+            String s, boolean convertPlus, Charset charset, boolean throwOnFailure) {
+        StringBuilder builder = new StringBuilder(s.length());
+        appendDecoded(builder, s, convertPlus, charset, throwOnFailure);
+        return builder.toString();
+    }
+
+    /**
+     * Character to be output when there's an error decoding an input.
+     */
+    private static final char INVALID_INPUT_CHARACTER = '\ufffd';
+
+    private static void appendDecoded(
+            StringBuilder builder,
+            String s,
+            boolean convertPlus,
+            Charset charset,
+            boolean throwOnFailure) {
+        CharsetDecoder decoder = charset.newDecoder()
+                .onMalformedInput(CodingErrorAction.REPLACE)
+                .replaceWith("\ufffd")
+                .onUnmappableCharacter(CodingErrorAction.REPORT);
+        // Holds the bytes corresponding to the escaped chars being read (empty if the last char
+        // wasn't a escaped char).
+        ByteBuffer byteBuffer = ByteBuffer.allocate(s.length());
+        int i = 0;
+        while (i < s.length()) {
+            char c = s.charAt(i);
+            i++;
+            switch (c) {
+                case '+':
+                    flushDecodingByteAccumulator(
+                            builder, decoder, byteBuffer, throwOnFailure);
+                    builder.append(convertPlus ? ' ' : '+');
+                    break;
+                case '%':
+                    // Expect two characters representing a number in hex.
+                    byte hexValue = 0;
+                    for (int j = 0; j < 2; j++) {
+                        try {
+                            c = getNextCharacter(s, i, s.length(), null /* name */);
+                        } catch (URISyntaxException e) {
+                            // Unexpected end of input.
+                            if (throwOnFailure) {
+                                throw new IllegalArgumentException(e);
+                            } else {
+                                flushDecodingByteAccumulator(
+                                        builder, decoder, byteBuffer, throwOnFailure);
+                                builder.append(INVALID_INPUT_CHARACTER);
+                                return;
+                            }
+                        }
+                        i++;
+                        int newDigit = hexCharToValue(c);
+                        if (newDigit < 0) {
+                            if (throwOnFailure) {
+                                throw new IllegalArgumentException(
+                                        unexpectedCharacterException(s, null /* name */, c, i - 1));
+                            } else {
+                                flushDecodingByteAccumulator(
+                                        builder, decoder, byteBuffer, throwOnFailure);
+                                builder.append(INVALID_INPUT_CHARACTER);
+                                break;
+                            }
+                        }
+                        hexValue = (byte) (hexValue * 0x10 + newDigit);
+                    }
+                    byteBuffer.put(hexValue);
+                    break;
+                default:
+                    flushDecodingByteAccumulator(builder, decoder, byteBuffer, throwOnFailure);
+                    builder.append(c);
+            }
+        }
+        flushDecodingByteAccumulator(builder, decoder, byteBuffer, throwOnFailure);
+    }
+
+    private static void flushDecodingByteAccumulator(
+            StringBuilder builder,
+            CharsetDecoder decoder,
+            ByteBuffer byteBuffer,
+            boolean throwOnFailure) {
+        if (byteBuffer.position() == 0) {
+            return;
+        }
+        byteBuffer.flip();
+        try {
+            builder.append(decoder.decode(byteBuffer));
+        } catch (CharacterCodingException e) {
+            if (throwOnFailure) {
+                throw new IllegalArgumentException(e);
+            } else {
+                builder.append(INVALID_INPUT_CHARACTER);
+            }
+        } finally {
+            // Use the byte buffer to write again.
+            byteBuffer.flip();
+            byteBuffer.limit(byteBuffer.capacity());
+        }
+    }
+}
diff --git a/android/net/UrlQuerySanitizer.java b/android/net/UrlQuerySanitizer.java
new file mode 100644
index 0000000..cf08b65
--- /dev/null
+++ b/android/net/UrlQuerySanitizer.java
@@ -0,0 +1,911 @@
+/*
+ * Copyright (C) 2007 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 java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import java.util.StringTokenizer;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ *
+ * Sanitizes the Query portion of a URL. Simple example:
+ * <code>
+ * UrlQuerySanitizer sanitizer = new UrlQuerySanitizer();
+ * sanitizer.setAllowUnregisteredParamaters(true);
+ * sanitizer.parseUrl("http://example.com/?name=Joe+User");
+ * String name = sanitizer.getValue("name"));
+ * // name now contains "Joe_User"
+ * </code>
+ *
+ * Register ValueSanitizers to customize the way individual
+ * parameters are sanitized:
+ * <code>
+ * UrlQuerySanitizer sanitizer = new UrlQuerySanitizer();
+ * sanitizer.registerParamater("name", UrlQuerySanitizer.createSpaceLegal());
+ * sanitizer.parseUrl("http://example.com/?name=Joe+User");
+ * String name = sanitizer.getValue("name"));
+ * // name now contains "Joe User". (The string is first decoded, which
+ * // converts the '+' to a ' '. Then the string is sanitized, which
+ * // converts the ' ' to an '_'. (The ' ' is converted because the default
+ * unregistered parameter sanitizer does not allow any special characters,
+ * and ' ' is a special character.)
+ * </code>
+ *
+ * There are several ways to create ValueSanitizers. In order of increasing
+ * sophistication:
+ * <ol>
+ * <li>Call one of the UrlQuerySanitizer.createXXX() methods.
+ * <li>Construct your own instance of
+ * UrlQuerySanitizer.IllegalCharacterValueSanitizer.
+ * <li>Subclass UrlQuerySanitizer.ValueSanitizer to define your own value
+ * sanitizer.
+ * </ol>
+ *
+ */
+public class UrlQuerySanitizer {
+
+    /**
+     * A simple tuple that holds parameter-value pairs.
+     *
+     */
+    public class ParameterValuePair {
+        /**
+         * Construct a parameter-value tuple.
+         * @param parameter an unencoded parameter
+         * @param value an unencoded value
+         */
+        public ParameterValuePair(String parameter,
+                String value) {
+            mParameter = parameter;
+            mValue = value;
+        }
+        /**
+         * The unencoded parameter
+         */
+        public String mParameter;
+        /**
+         * The unencoded value
+         */
+        public String mValue;
+    }
+
+    final private HashMap<String, ValueSanitizer> mSanitizers =
+        new HashMap<String, ValueSanitizer>();
+    final private HashMap<String, String> mEntries =
+        new HashMap<String, String>();
+    final private ArrayList<ParameterValuePair> mEntriesList =
+        new ArrayList<ParameterValuePair>();
+    private boolean mAllowUnregisteredParamaters;
+    private boolean mPreferFirstRepeatedParameter;
+    private ValueSanitizer mUnregisteredParameterValueSanitizer =
+        getAllIllegal();
+
+    /**
+     * A functor used to sanitize a single query value.
+     *
+     */
+    public static interface ValueSanitizer {
+        /**
+         * Sanitize an unencoded value.
+         * @param value
+         * @return the sanitized unencoded value
+         */
+        public String sanitize(String value);
+    }
+
+    /**
+     * Sanitize values based on which characters they contain. Illegal
+     * characters are replaced with either space or '_', depending upon
+     * whether space is a legal character or not.
+     */
+    public static class IllegalCharacterValueSanitizer implements
+        ValueSanitizer {
+        private int mFlags;
+
+        /**
+         * Allow space (' ') characters.
+         */
+        public final static int SPACE_OK =              1 << 0;
+        /**
+         * Allow whitespace characters other than space. The
+         * other whitespace characters are
+         * '\t' '\f' '\n' '\r' and '\0x000b' (vertical tab)
+         */
+        public final static int OTHER_WHITESPACE_OK =  1 << 1;
+        /**
+         * Allow characters with character codes 128 to 255.
+         */
+        public final static int NON_7_BIT_ASCII_OK =    1 << 2;
+        /**
+         * Allow double quote characters. ('"')
+         */
+        public final static int DQUOTE_OK =             1 << 3;
+        /**
+         * Allow single quote characters. ('\'')
+         */
+        public final static int SQUOTE_OK =             1 << 4;
+        /**
+         * Allow less-than characters. ('<')
+         */
+        public final static int LT_OK =                 1 << 5;
+        /**
+         * Allow greater-than characters. ('>')
+         */
+        public final static int GT_OK =                 1 << 6;
+        /**
+         * Allow ampersand characters ('&')
+         */
+        public final static int AMP_OK =                1 << 7;
+        /**
+         * Allow percent-sign characters ('%')
+         */
+        public final static int PCT_OK =                1 << 8;
+        /**
+         * Allow nul characters ('\0')
+         */
+        public final static int NUL_OK =                1 << 9;
+        /**
+         * Allow text to start with a script URL
+         * such as "javascript:" or "vbscript:"
+         */
+        public final static int SCRIPT_URL_OK =         1 << 10;
+
+        /**
+         * Mask with all fields set to OK
+         */
+        public final static int ALL_OK =                0x7ff;
+
+        /**
+         * Mask with both regular space and other whitespace OK
+         */
+        public final static int ALL_WHITESPACE_OK =
+            SPACE_OK | OTHER_WHITESPACE_OK;
+
+
+        // Common flag combinations:
+
+        /**
+         * <ul>
+         * <li>Deny all special characters.
+         * <li>Deny script URLs.
+         * </ul>
+         */
+        public final static int ALL_ILLEGAL =
+            0;
+        /**
+         * <ul>
+         * <li>Allow all special characters except Nul. ('\0').
+         * <li>Allow script URLs.
+         * </ul>
+         */
+        public final static int ALL_BUT_NUL_LEGAL =
+            ALL_OK & ~NUL_OK;
+        /**
+         * <ul>
+         * <li>Allow all special characters except for:
+         * <ul>
+         *  <li>whitespace characters
+         *  <li>Nul ('\0')
+         * </ul>
+         * <li>Allow script URLs.
+         * </ul>
+         */
+        public final static int ALL_BUT_WHITESPACE_LEGAL =
+            ALL_OK & ~(ALL_WHITESPACE_OK | NUL_OK);
+        /**
+         * <ul>
+         * <li>Allow characters used by encoded URLs.
+         * <li>Deny script URLs.
+         * </ul>
+         */
+        public final static int URL_LEGAL =
+            NON_7_BIT_ASCII_OK | SQUOTE_OK | AMP_OK | PCT_OK;
+        /**
+         * <ul>
+         * <li>Allow characters used by encoded URLs.
+         * <li>Allow spaces.
+         * <li>Deny script URLs.
+         * </ul>
+         */
+        public final static int URL_AND_SPACE_LEGAL =
+            URL_LEGAL | SPACE_OK;
+        /**
+         * <ul>
+         * <li>Allow ampersand.
+         * <li>Deny script URLs.
+         * </ul>
+         */
+        public final static int AMP_LEGAL =
+            AMP_OK;
+        /**
+         * <ul>
+         * <li>Allow ampersand.
+         * <li>Allow space.
+         * <li>Deny script URLs.
+         * </ul>
+         */
+        public final static int AMP_AND_SPACE_LEGAL =
+            AMP_OK | SPACE_OK;
+        /**
+         * <ul>
+         * <li>Allow space.
+         * <li>Deny script URLs.
+         * </ul>
+         */
+        public final static int SPACE_LEGAL =
+            SPACE_OK;
+        /**
+         * <ul>
+         * <li>Allow all but.
+         * <ul>
+         *  <li>Nul ('\0')
+         *  <li>Angle brackets ('<', '>')
+         * </ul>
+         * <li>Deny script URLs.
+         * </ul>
+         */
+        public final static int ALL_BUT_NUL_AND_ANGLE_BRACKETS_LEGAL =
+            ALL_OK & ~(NUL_OK | LT_OK | GT_OK);
+
+        /**
+         *  Script URL definitions
+         */
+
+        private final static String JAVASCRIPT_PREFIX = "javascript:";
+
+        private final static String VBSCRIPT_PREFIX = "vbscript:";
+
+        private final static int MIN_SCRIPT_PREFIX_LENGTH = Math.min(
+                JAVASCRIPT_PREFIX.length(), VBSCRIPT_PREFIX.length());
+
+        /**
+         * Construct a sanitizer. The parameters set the behavior of the
+         * sanitizer.
+         * @param flags some combination of the XXX_OK flags.
+         */
+        public IllegalCharacterValueSanitizer(
+            int flags) {
+            mFlags = flags;
+        }
+        /**
+         * Sanitize a value.
+         * <ol>
+         * <li>If script URLs are not OK, they will be removed.
+         * <li>If neither spaces nor other white space is OK, then
+         * white space will be trimmed from the beginning and end of
+         * the URL. (Just the actual white space characters are trimmed, not
+         * other control codes.)
+         * <li> Illegal characters will be replaced with
+         * either ' ' or '_', depending on whether a space is itself a
+         * legal character.
+         * </ol>
+         * @param value
+         * @return the sanitized value
+         */
+        public String sanitize(String value) {
+            if (value == null) {
+                return null;
+            }
+            int length = value.length();
+            if ((mFlags & SCRIPT_URL_OK) != 0) {
+                if (length >= MIN_SCRIPT_PREFIX_LENGTH) {
+                    String asLower = value.toLowerCase(Locale.ROOT);
+                    if (asLower.startsWith(JAVASCRIPT_PREFIX)  ||
+                        asLower.startsWith(VBSCRIPT_PREFIX)) {
+                        return "";
+                    }
+                }
+            }
+
+            // If whitespace isn't OK, get rid of whitespace at beginning
+            // and end of value.
+            if ( (mFlags & ALL_WHITESPACE_OK) == 0) {
+                value = trimWhitespace(value);
+                // The length could have changed, so we need to correct
+                // the length variable.
+                length = value.length();
+            }
+
+            StringBuilder stringBuilder = new StringBuilder(length);
+            for(int i = 0; i < length; i++) {
+                char c = value.charAt(i);
+                if (!characterIsLegal(c)) {
+                    if ((mFlags & SPACE_OK) != 0) {
+                        c = ' ';
+                    }
+                    else {
+                        c = '_';
+                    }
+                }
+                stringBuilder.append(c);
+            }
+            return stringBuilder.toString();
+        }
+
+        /**
+         * Trim whitespace from the beginning and end of a string.
+         * <p>
+         * Note: can't use {@link String#trim} because {@link String#trim} has a
+         * different definition of whitespace than we want.
+         * @param value the string to trim
+         * @return the trimmed string
+         */
+        private String trimWhitespace(String value) {
+            int start = 0;
+            int last = value.length() - 1;
+            int end = last;
+            while (start <= end && isWhitespace(value.charAt(start))) {
+                start++;
+            }
+            while (end >= start && isWhitespace(value.charAt(end))) {
+                end--;
+            }
+            if (start == 0 && end == last) {
+                return value;
+            }
+            return value.substring(start, end + 1);
+        }
+
+        /**
+         * Check if c is whitespace.
+         * @param c character to test
+         * @return true if c is a whitespace character
+         */
+        private boolean isWhitespace(char c) {
+            switch(c) {
+            case ' ':
+            case '\t':
+            case '\f':
+            case '\n':
+            case '\r':
+            case 11: /* VT */
+                return true;
+            default:
+                return false;
+            }
+        }
+
+        /**
+         * Check whether an individual character is legal. Uses the
+         * flag bit-set passed into the constructor.
+         * @param c
+         * @return true if c is a legal character
+         */
+        private boolean characterIsLegal(char c) {
+            switch(c) {
+            case ' ' : return (mFlags & SPACE_OK) != 0;
+            case '\t': case '\f': case '\n': case '\r': case 11: /* VT */
+              return (mFlags & OTHER_WHITESPACE_OK) != 0;
+            case '\"': return (mFlags & DQUOTE_OK) != 0;
+            case '\'': return (mFlags & SQUOTE_OK) != 0;
+            case '<' : return (mFlags & LT_OK) != 0;
+            case '>' : return (mFlags & GT_OK) != 0;
+            case '&' : return (mFlags & AMP_OK) != 0;
+            case '%' : return (mFlags & PCT_OK) != 0;
+            case '\0': return (mFlags & NUL_OK) != 0;
+            default  : return (c >= 32 && c < 127) ||
+                ((c >= 128) && ((mFlags & NON_7_BIT_ASCII_OK) != 0));
+            }
+        }
+    }
+
+    /**
+     * Get the current value sanitizer used when processing
+     * unregistered parameter values.
+     * <p>
+     * <b>Note:</b> The default unregistered parameter value sanitizer is
+     * one that doesn't allow any special characters, similar to what
+     * is returned by calling createAllIllegal.
+     *
+     * @return the current ValueSanitizer used to sanitize unregistered
+     * parameter values.
+     */
+    public ValueSanitizer getUnregisteredParameterValueSanitizer() {
+        return mUnregisteredParameterValueSanitizer;
+    }
+
+    /**
+     * Set the value sanitizer used when processing unregistered
+     * parameter values.
+     * @param sanitizer set the ValueSanitizer used to sanitize unregistered
+     * parameter values.
+     */
+    public void setUnregisteredParameterValueSanitizer(
+            ValueSanitizer sanitizer) {
+        mUnregisteredParameterValueSanitizer = sanitizer;
+    }
+
+
+    // Private fields for singleton sanitizers:
+
+    private static final ValueSanitizer sAllIllegal =
+        new IllegalCharacterValueSanitizer(
+                IllegalCharacterValueSanitizer.ALL_ILLEGAL);
+
+    private static final ValueSanitizer sAllButNulLegal =
+        new IllegalCharacterValueSanitizer(
+                IllegalCharacterValueSanitizer.ALL_BUT_NUL_LEGAL);
+
+    private static final ValueSanitizer sAllButWhitespaceLegal =
+        new IllegalCharacterValueSanitizer(
+                IllegalCharacterValueSanitizer.ALL_BUT_WHITESPACE_LEGAL);
+
+    private static final ValueSanitizer sURLLegal =
+        new IllegalCharacterValueSanitizer(
+                IllegalCharacterValueSanitizer.URL_LEGAL);
+
+    private static final ValueSanitizer sUrlAndSpaceLegal =
+        new IllegalCharacterValueSanitizer(
+                IllegalCharacterValueSanitizer.URL_AND_SPACE_LEGAL);
+
+    private static final ValueSanitizer sAmpLegal =
+        new IllegalCharacterValueSanitizer(
+                IllegalCharacterValueSanitizer.AMP_LEGAL);
+
+    private static final ValueSanitizer sAmpAndSpaceLegal =
+        new IllegalCharacterValueSanitizer(
+                IllegalCharacterValueSanitizer.AMP_AND_SPACE_LEGAL);
+
+    private static final ValueSanitizer sSpaceLegal =
+        new IllegalCharacterValueSanitizer(
+                IllegalCharacterValueSanitizer.SPACE_LEGAL);
+
+    private static final ValueSanitizer sAllButNulAndAngleBracketsLegal =
+        new IllegalCharacterValueSanitizer(
+                IllegalCharacterValueSanitizer.ALL_BUT_NUL_AND_ANGLE_BRACKETS_LEGAL);
+
+    /**
+     * Return a value sanitizer that does not allow any special characters,
+     * and also does not allow script URLs.
+     * @return a value sanitizer
+     */
+    public static final ValueSanitizer getAllIllegal() {
+        return sAllIllegal;
+    }
+
+    /**
+     * Return a value sanitizer that allows everything except Nul ('\0')
+     * characters. Script URLs are allowed.
+     * @return a value sanitizer
+     */
+    public static final ValueSanitizer getAllButNulLegal() {
+        return sAllButNulLegal;
+    }
+    /**
+     * Return a value sanitizer that allows everything except Nul ('\0')
+     * characters, space (' '), and other whitespace characters.
+     * Script URLs are allowed.
+     * @return a value sanitizer
+     */
+    public static final ValueSanitizer getAllButWhitespaceLegal() {
+        return sAllButWhitespaceLegal;
+    }
+    /**
+     * Return a value sanitizer that allows all the characters used by
+     * encoded URLs. Does not allow script URLs.
+     * @return a value sanitizer
+     */
+    public static final ValueSanitizer getUrlLegal() {
+        return sURLLegal;
+    }
+    /**
+     * Return a value sanitizer that allows all the characters used by
+     * encoded URLs and allows spaces, which are not technically legal
+     * in encoded URLs, but commonly appear anyway.
+     * Does not allow script URLs.
+     * @return a value sanitizer
+     */
+    public static final ValueSanitizer getUrlAndSpaceLegal() {
+        return sUrlAndSpaceLegal;
+    }
+    /**
+     * Return a value sanitizer that does not allow any special characters
+     * except ampersand ('&'). Does not allow script URLs.
+     * @return a value sanitizer
+     */
+    public static final ValueSanitizer getAmpLegal() {
+        return sAmpLegal;
+    }
+    /**
+     * Return a value sanitizer that does not allow any special characters
+     * except ampersand ('&') and space (' '). Does not allow script URLs.
+     * @return a value sanitizer
+     */
+    public static final ValueSanitizer getAmpAndSpaceLegal() {
+        return sAmpAndSpaceLegal;
+    }
+    /**
+     * Return a value sanitizer that does not allow any special characters
+     * except space (' '). Does not allow script URLs.
+     * @return a value sanitizer
+     */
+    public static final ValueSanitizer getSpaceLegal() {
+        return sSpaceLegal;
+    }
+    /**
+     * Return a value sanitizer that allows any special characters
+     * except angle brackets ('<' and '>') and Nul ('\0').
+     * Allows script URLs.
+     * @return a value sanitizer
+     */
+    public static final ValueSanitizer getAllButNulAndAngleBracketsLegal() {
+        return sAllButNulAndAngleBracketsLegal;
+    }
+
+    /**
+     * Constructs a UrlQuerySanitizer.
+     * <p>
+     * Defaults:
+     * <ul>
+     * <li>unregistered parameters are not allowed.
+     * <li>the last instance of a repeated parameter is preferred.
+     * <li>The default value sanitizer is an AllIllegal value sanitizer.
+     * <ul>
+     */
+    public UrlQuerySanitizer() {
+    }
+
+    /**
+     * Constructs a UrlQuerySanitizer and parses a URL.
+     * This constructor is provided for convenience when the
+     * default parsing behavior is acceptable.
+     * <p>
+     * Because the URL is parsed before the constructor returns, there isn't
+     * a chance to configure the sanitizer to change the parsing behavior.
+     * <p>
+     * <code>
+     * UrlQuerySanitizer sanitizer = new UrlQuerySanitizer(myUrl);
+     * String name = sanitizer.getValue("name");
+     * </code>
+     * <p>
+     * Defaults:
+     * <ul>
+     * <li>unregistered parameters <em>are</em> allowed.
+     * <li>the last instance of a repeated parameter is preferred.
+     * <li>The default value sanitizer is an AllIllegal value sanitizer.
+     * <ul>
+     */
+    public UrlQuerySanitizer(String url) {
+        setAllowUnregisteredParamaters(true);
+        parseUrl(url);
+    }
+
+    /**
+     * Parse the query parameters out of an encoded URL.
+     * Works by extracting the query portion from the URL and then
+     * calling parseQuery(). If there is no query portion it is
+     * treated as if the query portion is an empty string.
+     * @param url the encoded URL to parse.
+     */
+    public void parseUrl(String url) {
+        int queryIndex = url.indexOf('?');
+        String query;
+        if (queryIndex >= 0) {
+            query = url.substring(queryIndex + 1);
+        }
+        else {
+            query = "";
+        }
+        parseQuery(query);
+    }
+
+    /**
+     * Parse a query. A query string is any number of parameter-value clauses
+     * separated by any non-zero number of ampersands. A parameter-value clause
+     * is a parameter followed by an equal sign, followed by a value. If the
+     * equal sign is missing, the value is assumed to be the empty string.
+     * @param query the query to parse.
+     */
+    public void parseQuery(String query) {
+        clear();
+        // Split by '&'
+        StringTokenizer tokenizer = new StringTokenizer(query, "&");
+        while(tokenizer.hasMoreElements()) {
+            String attributeValuePair = tokenizer.nextToken();
+            if (attributeValuePair.length() > 0) {
+                int assignmentIndex = attributeValuePair.indexOf('=');
+                if (assignmentIndex < 0) {
+                    // No assignment found, treat as if empty value
+                    parseEntry(attributeValuePair, "");
+                }
+                else {
+                    parseEntry(attributeValuePair.substring(0, assignmentIndex),
+                            attributeValuePair.substring(assignmentIndex + 1));
+                }
+            }
+        }
+    }
+
+    /**
+     * Get a set of all of the parameters found in the sanitized query.
+     * <p>
+     * Note: Do not modify this set. Treat it as a read-only set.
+     * @return all the parameters found in the current query.
+     */
+    public Set<String> getParameterSet() {
+        return mEntries.keySet();
+    }
+
+    /**
+     * An array list of all of the parameter-value pairs in the sanitized
+     * query, in the order they appeared in the query. May contain duplicate
+     * parameters.
+     * <p class="note"><b>Note:</b> Do not modify this list. Treat it as a read-only list.</p>
+     */
+    public List<ParameterValuePair> getParameterList() {
+        return mEntriesList;
+    }
+
+    /**
+     * Check if a parameter exists in the current sanitized query.
+     * @param parameter the unencoded name of a parameter.
+     * @return true if the parameter exists in the current sanitized queary.
+     */
+    public boolean hasParameter(String parameter) {
+        return mEntries.containsKey(parameter);
+    }
+
+    /**
+     * Get the value for a parameter in the current sanitized query.
+     * Returns null if the parameter does not
+     * exit.
+     * @param parameter the unencoded name of a parameter.
+     * @return the sanitized unencoded value of the parameter,
+     * or null if the parameter does not exist.
+     */
+    public String getValue(String parameter) {
+        return mEntries.get(parameter);
+    }
+
+    /**
+     * Register a value sanitizer for a particular parameter. Can also be used
+     * to replace or remove an already-set value sanitizer.
+     * <p>
+     * Registering a non-null value sanitizer for a particular parameter
+     * makes that parameter a registered parameter.
+     * @param parameter an unencoded parameter name
+     * @param valueSanitizer the value sanitizer to use for a particular
+     * parameter. May be null in order to unregister that parameter.
+     * @see #getAllowUnregisteredParamaters()
+     */
+    public void registerParameter(String parameter,
+            ValueSanitizer valueSanitizer) {
+        if (valueSanitizer == null) {
+            mSanitizers.remove(parameter);
+        }
+        mSanitizers.put(parameter, valueSanitizer);
+    }
+
+    /**
+     * Register a value sanitizer for an array of parameters.
+     * @param parameters An array of unencoded parameter names.
+     * @param valueSanitizer
+     * @see #registerParameter
+     */
+    public void registerParameters(String[] parameters,
+            ValueSanitizer valueSanitizer) {
+        int length = parameters.length;
+        for(int i = 0; i < length; i++) {
+            mSanitizers.put(parameters[i], valueSanitizer);
+        }
+    }
+
+    /**
+     * Set whether or not unregistered parameters are allowed. If they
+     * are not allowed, then they will be dropped when a query is sanitized.
+     * <p>
+     * Defaults to false.
+     * @param allowUnregisteredParamaters true to allow unregistered parameters.
+     * @see #getAllowUnregisteredParamaters()
+     */
+    public void setAllowUnregisteredParamaters(
+            boolean allowUnregisteredParamaters) {
+        mAllowUnregisteredParamaters = allowUnregisteredParamaters;
+    }
+
+    /**
+     * Get whether or not unregistered parameters are allowed. If not
+     * allowed, they will be dropped when a query is parsed.
+     * @return true if unregistered parameters are allowed.
+     * @see #setAllowUnregisteredParamaters(boolean)
+     */
+    public boolean getAllowUnregisteredParamaters() {
+        return mAllowUnregisteredParamaters;
+    }
+
+    /**
+     * Set whether or not the first occurrence of a repeated parameter is
+     * preferred. True means the first repeated parameter is preferred.
+     * False means that the last repeated parameter is preferred.
+     * <p>
+     * The preferred parameter is the one that is returned when getParameter
+     * is called.
+     * <p>
+     * defaults to false.
+     * @param preferFirstRepeatedParameter True if the first repeated
+     * parameter is preferred.
+     * @see #getPreferFirstRepeatedParameter()
+     */
+    public void setPreferFirstRepeatedParameter(
+            boolean preferFirstRepeatedParameter) {
+        mPreferFirstRepeatedParameter = preferFirstRepeatedParameter;
+    }
+
+    /**
+     * Get whether or not the first occurrence of a repeated parameter is
+     * preferred.
+     * @return true if the first occurrence of a repeated parameter is
+     * preferred.
+     * @see #setPreferFirstRepeatedParameter(boolean)
+     */
+    public boolean getPreferFirstRepeatedParameter() {
+        return mPreferFirstRepeatedParameter;
+    }
+
+    /**
+     * Parse an escaped parameter-value pair. The default implementation
+     * unescapes both the parameter and the value, then looks up the
+     * effective value sanitizer for the parameter and uses it to sanitize
+     * the value. If all goes well then addSanitizedValue is called with
+     * the unescaped parameter and the sanitized unescaped value.
+     * @param parameter an escaped parameter
+     * @param value an unsanitized escaped value
+     */
+    protected void parseEntry(String parameter, String value) {
+        String unescapedParameter = unescape(parameter);
+         ValueSanitizer valueSanitizer =
+            getEffectiveValueSanitizer(unescapedParameter);
+
+        if (valueSanitizer == null) {
+            return;
+        }
+        String unescapedValue = unescape(value);
+        String sanitizedValue = valueSanitizer.sanitize(unescapedValue);
+        addSanitizedEntry(unescapedParameter, sanitizedValue);
+    }
+
+    /**
+     * Record a sanitized parameter-value pair. Override if you want to
+     * do additional filtering or validation.
+     * @param parameter an unescaped parameter
+     * @param value a sanitized unescaped value
+     */
+    protected void addSanitizedEntry(String parameter, String value) {
+        mEntriesList.add(
+                new ParameterValuePair(parameter, value));
+        if (mPreferFirstRepeatedParameter) {
+            if (mEntries.containsKey(parameter)) {
+                return;
+            }
+        }
+        mEntries.put(parameter, value);
+    }
+
+    /**
+     * Get the value sanitizer for a parameter. Returns null if there
+     * is no value sanitizer registered for the parameter.
+     * @param parameter the unescaped parameter
+     * @return the currently registered value sanitizer for this parameter.
+     * @see #registerParameter(String, android.net.UrlQuerySanitizer.ValueSanitizer)
+     */
+    public ValueSanitizer getValueSanitizer(String parameter) {
+        return mSanitizers.get(parameter);
+    }
+
+    /**
+     * Get the effective value sanitizer for a parameter. Like getValueSanitizer,
+     * except if there is no value sanitizer registered for a parameter, and
+     * unregistered parameters are allowed, then the default value sanitizer is
+     * returned.
+     * @param parameter an unescaped parameter
+     * @return the effective value sanitizer for a parameter.
+     */
+    public ValueSanitizer getEffectiveValueSanitizer(String parameter) {
+        ValueSanitizer sanitizer = getValueSanitizer(parameter);
+        if (sanitizer == null && mAllowUnregisteredParamaters) {
+            sanitizer = getUnregisteredParameterValueSanitizer();
+        }
+        return sanitizer;
+    }
+
+    /**
+     * Unescape an escaped string.
+     * <ul>
+     * <li>'+' characters are replaced by
+     * ' ' characters.
+     * <li>Valid "%xx" escape sequences are replaced by the
+     * corresponding unescaped character.
+     * <li>Invalid escape sequences such as %1z", are passed through unchanged.
+     * <ol>
+     * @param string the escaped string
+     * @return the unescaped string.
+     */
+    private static final Pattern plusOrPercent = Pattern.compile("[+%]");
+    public String unescape(String string) {
+        final Matcher matcher = plusOrPercent.matcher(string);
+        if (!matcher.find()) return string;
+        final int firstEscape = matcher.start();
+
+        int length = string.length();
+
+        StringBuilder stringBuilder = new StringBuilder(length);
+        stringBuilder.append(string.substring(0, firstEscape));
+        for (int i = firstEscape; i < length; i++) {
+            char c = string.charAt(i);
+            if (c == '+') {
+                c = ' ';
+            } else if (c == '%' && i + 2 < length) {
+                char c1 = string.charAt(i + 1);
+                char c2 = string.charAt(i + 2);
+                if (isHexDigit(c1) && isHexDigit(c2)) {
+                    c = (char) (decodeHexDigit(c1) * 16 + decodeHexDigit(c2));
+                    i += 2;
+                }
+            }
+            stringBuilder.append(c);
+        }
+        return stringBuilder.toString();
+    }
+
+    /**
+     * Test if a character is a hexidecimal digit. Both upper case and lower
+     * case hex digits are allowed.
+     * @param c the character to test
+     * @return true if c is a hex digit.
+     */
+    protected boolean isHexDigit(char c) {
+        return decodeHexDigit(c) >= 0;
+    }
+
+    /**
+     * Convert a character that represents a hexidecimal digit into an integer.
+     * If the character is not a hexidecimal digit, then -1 is returned.
+     * Both upper case and lower case hex digits are allowed.
+     * @param c the hexidecimal digit.
+     * @return the integer value of the hexidecimal digit.
+     */
+
+    protected int decodeHexDigit(char c) {
+        if (c >= '0' && c <= '9') {
+            return c - '0';
+        }
+        else if (c >= 'A' && c <= 'F') {
+            return c - 'A' + 10;
+        }
+        else if (c >= 'a' && c <= 'f') {
+            return c - 'a' + 10;
+        }
+        else {
+            return -1;
+        }
+    }
+
+    /**
+     * Clear the existing entries. Called to get ready to parse a new
+     * query string.
+     */
+    protected void clear() {
+        mEntries.clear();
+        mEntriesList.clear();
+    }
+}
+
diff --git a/android/net/VpnService.java b/android/net/VpnService.java
new file mode 100644
index 0000000..ed7cddc
--- /dev/null
+++ b/android/net/VpnService.java
@@ -0,0 +1,903 @@
+/*
+ * Copyright (C) 2011 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 static android.system.OsConstants.AF_INET;
+import static android.system.OsConstants.AF_INET6;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SystemApi;
+import android.annotation.UnsupportedAppUsage;
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.app.admin.DevicePolicyManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageManager;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.Parcel;
+import android.os.ParcelFileDescriptor;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.UserHandle;
+
+import com.android.internal.net.VpnConfig;
+
+import java.net.DatagramSocket;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.Socket;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * VpnService is a base class for applications to extend and build their
+ * own VPN solutions. In general, it creates a virtual network interface,
+ * configures addresses and routing rules, and returns a file descriptor
+ * to the application. Each read from the descriptor retrieves an outgoing
+ * packet which was routed to the interface. Each write to the descriptor
+ * injects an incoming packet just like it was received from the interface.
+ * The interface is running on Internet Protocol (IP), so packets are
+ * always started with IP headers. The application then completes a VPN
+ * connection by processing and exchanging packets with the remote server
+ * over a tunnel.
+ *
+ * <p>Letting applications intercept packets raises huge security concerns.
+ * A VPN application can easily break the network. Besides, two of them may
+ * conflict with each other. The system takes several actions to address
+ * these issues. Here are some key points:
+ * <ul>
+ *   <li>User action is required the first time an application creates a VPN
+ *       connection.</li>
+ *   <li>There can be only one VPN connection running at the same time. The
+ *       existing interface is deactivated when a new one is created.</li>
+ *   <li>A system-managed notification is shown during the lifetime of a
+ *       VPN connection.</li>
+ *   <li>A system-managed dialog gives the information of the current VPN
+ *       connection. It also provides a button to disconnect.</li>
+ *   <li>The network is restored automatically when the file descriptor is
+ *       closed. It also covers the cases when a VPN application is crashed
+ *       or killed by the system.</li>
+ * </ul>
+ *
+ * <p>There are two primary methods in this class: {@link #prepare} and
+ * {@link Builder#establish}. The former deals with user action and stops
+ * the VPN connection created by another application. The latter creates
+ * a VPN interface using the parameters supplied to the {@link Builder}.
+ * An application must call {@link #prepare} to grant the right to use
+ * other methods in this class, and the right can be revoked at any time.
+ * Here are the general steps to create a VPN connection:
+ * <ol>
+ *   <li>When the user presses the button to connect, call {@link #prepare}
+ *       and launch the returned intent, if non-null.</li>
+ *   <li>When the application becomes prepared, start the service.</li>
+ *   <li>Create a tunnel to the remote server and negotiate the network
+ *       parameters for the VPN connection.</li>
+ *   <li>Supply those parameters to a {@link Builder} and create a VPN
+ *       interface by calling {@link Builder#establish}.</li>
+ *   <li>Process and exchange packets between the tunnel and the returned
+ *       file descriptor.</li>
+ *   <li>When {@link #onRevoke} is invoked, close the file descriptor and
+ *       shut down the tunnel gracefully.</li>
+ * </ol>
+ *
+ * <p>Services extending this class need to be declared with an appropriate
+ * permission and intent filter. Their access must be secured by
+ * {@link android.Manifest.permission#BIND_VPN_SERVICE} permission, and
+ * their intent filter must match {@link #SERVICE_INTERFACE} action. Here
+ * is an example of declaring a VPN service in {@code AndroidManifest.xml}:
+ * <pre>
+ * &lt;service android:name=".ExampleVpnService"
+ *         android:permission="android.permission.BIND_VPN_SERVICE"&gt;
+ *     &lt;intent-filter&gt;
+ *         &lt;action android:name="android.net.VpnService"/&gt;
+ *     &lt;/intent-filter&gt;
+ * &lt;/service&gt;</pre>
+ *
+ * <p> The Android system starts a VPN in the background by calling
+ * {@link android.content.Context#startService startService()}. In Android 8.0
+ * (API level 26) and higher, the system places VPN apps on the temporary
+ * whitelist for a short period so the app can start in the background. The VPN
+ * app must promote itself to the foreground after it's launched or the system
+ * will shut down the app.
+ *
+ * <h3>Developer's guide</h3>
+ *
+ * <p>To learn more about developing VPN apps, read the
+ * <a href="{@docRoot}guide/topics/connectivity/vpn">VPN developer's guide</a>.
+ *
+ * @see Builder
+ */
+public class VpnService extends Service {
+
+    /**
+     * The action must be matched by the intent filter of this service. It also
+     * needs to require {@link android.Manifest.permission#BIND_VPN_SERVICE}
+     * permission so that other applications cannot abuse it.
+     */
+    public static final String SERVICE_INTERFACE = VpnConfig.SERVICE_INTERFACE;
+
+    /**
+     * Key for boolean meta-data field indicating whether this VpnService supports always-on mode.
+     *
+     * <p>For a VPN app targeting {@link android.os.Build.VERSION_CODES#N API 24} or above, Android
+     * provides users with the ability to set it as always-on, so that VPN connection is
+     * persisted after device reboot and app upgrade. Always-on VPN can also be enabled by device
+     * owner and profile owner apps through
+     * {@link DevicePolicyManager#setAlwaysOnVpnPackage}.
+     *
+     * <p>VPN apps not supporting this feature should opt out by adding this meta-data field to the
+     * {@code VpnService} component of {@code AndroidManifest.xml}. In case there is more than one
+     * {@code VpnService} component defined in {@code AndroidManifest.xml}, opting out any one of
+     * them will opt out the entire app. For example,
+     * <pre> {@code
+     * <service android:name=".ExampleVpnService"
+     *         android:permission="android.permission.BIND_VPN_SERVICE">
+     *     <intent-filter>
+     *         <action android:name="android.net.VpnService"/>
+     *     </intent-filter>
+     *     <meta-data android:name="android.net.VpnService.SUPPORTS_ALWAYS_ON"
+     *             android:value=false/>
+     * </service>
+     * } </pre>
+     *
+     * <p>This meta-data field defaults to {@code true} if absent. It will only have effect on
+     * {@link android.os.Build.VERSION_CODES#O_MR1} or higher.
+     */
+    public static final String SERVICE_META_DATA_SUPPORTS_ALWAYS_ON =
+            "android.net.VpnService.SUPPORTS_ALWAYS_ON";
+
+    /**
+     * Use IConnectivityManager since those methods are hidden and not
+     * available in ConnectivityManager.
+     */
+    private static IConnectivityManager getService() {
+        return IConnectivityManager.Stub.asInterface(
+                ServiceManager.getService(Context.CONNECTIVITY_SERVICE));
+    }
+
+    /**
+     * Prepare to establish a VPN connection. This method returns {@code null}
+     * if the VPN application is already prepared or if the user has previously
+     * consented to the VPN application. Otherwise, it returns an
+     * {@link Intent} to a system activity. The application should launch the
+     * activity using {@link Activity#startActivityForResult} to get itself
+     * prepared. The activity may pop up a dialog to require user action, and
+     * the result will come back via its {@link Activity#onActivityResult}.
+     * If the result is {@link Activity#RESULT_OK}, the application becomes
+     * prepared and is granted to use other methods in this class.
+     *
+     * <p>Only one application can be granted at the same time. The right
+     * is revoked when another application is granted. The application
+     * losing the right will be notified via its {@link #onRevoke}. Unless
+     * it becomes prepared again, subsequent calls to other methods in this
+     * class will fail.
+     *
+     * <p>The user may disable the VPN at any time while it is activated, in
+     * which case this method will return an intent the next time it is
+     * executed to obtain the user's consent again.
+     *
+     * @see #onRevoke
+     */
+    public static Intent prepare(Context context) {
+        try {
+            if (getService().prepareVpn(context.getPackageName(), null, context.getUserId())) {
+                return null;
+            }
+        } catch (RemoteException e) {
+            // ignore
+        }
+        return VpnConfig.getIntentForConfirmation();
+    }
+
+    /**
+     * Version of {@link #prepare(Context)} which does not require user consent.
+     *
+     * <p>Requires {@link android.Manifest.permission#CONTROL_VPN} and should generally not be
+     * used. Only acceptable in situations where user consent has been obtained through other means.
+     *
+     * <p>Once this is run, future preparations may be done with the standard prepare method as this
+     * will authorize the package to prepare the VPN without consent in the future.
+     *
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.CONTROL_VPN)
+    public static void prepareAndAuthorize(Context context) {
+        IConnectivityManager cm = getService();
+        String packageName = context.getPackageName();
+        try {
+            // Only prepare if we're not already prepared.
+            int userId = context.getUserId();
+            if (!cm.prepareVpn(packageName, null, userId)) {
+                cm.prepareVpn(null, packageName, userId);
+            }
+            cm.setVpnPackageAuthorization(packageName, userId, true);
+        } catch (RemoteException e) {
+            // ignore
+        }
+    }
+
+    /**
+     * Protect a socket from VPN connections. After protecting, data sent
+     * through this socket will go directly to the underlying network,
+     * so its traffic will not be forwarded through the VPN.
+     * This method is useful if some connections need to be kept
+     * outside of VPN. For example, a VPN tunnel should protect itself if its
+     * destination is covered by VPN routes. Otherwise its outgoing packets
+     * will be sent back to the VPN interface and cause an infinite loop. This
+     * method will fail if the application is not prepared or is revoked.
+     *
+     * <p class="note">The socket is NOT closed by this method.
+     *
+     * @return {@code true} on success.
+     */
+    public boolean protect(int socket) {
+        return NetworkUtils.protectFromVpn(socket);
+    }
+
+    /**
+     * Convenience method to protect a {@link Socket} from VPN connections.
+     *
+     * @return {@code true} on success.
+     * @see #protect(int)
+     */
+    public boolean protect(Socket socket) {
+        return protect(socket.getFileDescriptor$().getInt$());
+    }
+
+    /**
+     * Convenience method to protect a {@link DatagramSocket} from VPN
+     * connections.
+     *
+     * @return {@code true} on success.
+     * @see #protect(int)
+     */
+    public boolean protect(DatagramSocket socket) {
+        return protect(socket.getFileDescriptor$().getInt$());
+    }
+
+    /**
+     * Adds a network address to the VPN interface.
+     *
+     * Both IPv4 and IPv6 addresses are supported. The VPN must already be established. Fails if the
+     * address is already in use or cannot be assigned to the interface for any other reason.
+     *
+     * Adding an address implicitly allows traffic from that address family (i.e., IPv4 or IPv6) to
+     * be routed over the VPN. @see Builder#allowFamily
+     *
+     * @throws IllegalArgumentException if the address is invalid.
+     *
+     * @param address The IP address (IPv4 or IPv6) to assign to the VPN interface.
+     * @param prefixLength The prefix length of the address.
+     *
+     * @return {@code true} on success.
+     * @see Builder#addAddress
+     *
+     * @hide
+     */
+    public boolean addAddress(InetAddress address, int prefixLength) {
+        check(address, prefixLength);
+        try {
+            return getService().addVpnAddress(address.getHostAddress(), prefixLength);
+        } catch (RemoteException e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
+    /**
+     * Removes a network address from the VPN interface.
+     *
+     * Both IPv4 and IPv6 addresses are supported. The VPN must already be established. Fails if the
+     * address is not assigned to the VPN interface, or if it is the only address assigned (thus
+     * cannot be removed), or if the address cannot be removed for any other reason.
+     *
+     * After removing an address, if there are no addresses, routes or DNS servers of a particular
+     * address family (i.e., IPv4 or IPv6) configured on the VPN, that <b>DOES NOT</b> block that
+     * family from being routed. In other words, once an address family has been allowed, it stays
+     * allowed for the rest of the VPN's session. @see Builder#allowFamily
+     *
+     * @throws IllegalArgumentException if the address is invalid.
+     *
+     * @param address The IP address (IPv4 or IPv6) to assign to the VPN interface.
+     * @param prefixLength The prefix length of the address.
+     *
+     * @return {@code true} on success.
+     *
+     * @hide
+     */
+    public boolean removeAddress(InetAddress address, int prefixLength) {
+        check(address, prefixLength);
+        try {
+            return getService().removeVpnAddress(address.getHostAddress(), prefixLength);
+        } catch (RemoteException e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
+    /**
+     * Sets the underlying networks used by the VPN for its upstream connections.
+     *
+     * <p>Used by the system to know the actual networks that carry traffic for apps affected by
+     * this VPN in order to present this information to the user (e.g., via status bar icons).
+     *
+     * <p>This method only needs to be called if the VPN has explicitly bound its underlying
+     * communications channels &mdash; such as the socket(s) passed to {@link #protect(int)} &mdash;
+     * to a {@code Network} using APIs such as {@link Network#bindSocket(Socket)} or
+     * {@link Network#bindSocket(DatagramSocket)}. The VPN should call this method every time
+     * the set of {@code Network}s it is using changes.
+     *
+     * <p>{@code networks} is one of the following:
+     * <ul>
+     * <li><strong>a non-empty array</strong>: an array of one or more {@link Network}s, in
+     * decreasing preference order. For example, if this VPN uses both wifi and mobile (cellular)
+     * networks to carry app traffic, but prefers or uses wifi more than mobile, wifi should appear
+     * first in the array.</li>
+     * <li><strong>an empty array</strong>: a zero-element array, meaning that the VPN has no
+     * underlying network connection, and thus, app traffic will not be sent or received.</li>
+     * <li><strong>null</strong>: (default) signifies that the VPN uses whatever is the system's
+     * default network. I.e., it doesn't use the {@code bindSocket} or {@code bindDatagramSocket}
+     * APIs mentioned above to send traffic over specific channels.</li>
+     * </ul>
+     *
+     * <p>This call will succeed only if the VPN is currently established. For setting this value
+     * when the VPN has not yet been established, see {@link Builder#setUnderlyingNetworks}.
+     *
+     * @param networks An array of networks the VPN uses to tunnel traffic to/from its servers.
+     *
+     * @return {@code true} on success.
+     */
+    public boolean setUnderlyingNetworks(Network[] networks) {
+        try {
+            return getService().setUnderlyingNetworksForVpn(networks);
+        } catch (RemoteException e) {
+            throw new IllegalStateException(e);
+        }
+    }
+
+    /**
+     * Returns whether the service is running in always-on VPN mode. In this mode the system ensures
+     * that the service is always running by restarting it when necessary, e.g. after reboot.
+     *
+     * @see DevicePolicyManager#setAlwaysOnVpnPackage(ComponentName, String, boolean, Set)
+     */
+    public final boolean isAlwaysOn() {
+        try {
+            return getService().isCallerCurrentAlwaysOnVpnApp();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns whether the service is running in always-on VPN lockdown mode. In this mode the
+     * system ensures that the service is always running and that the apps aren't allowed to bypass
+     * the VPN.
+     *
+     * @see DevicePolicyManager#setAlwaysOnVpnPackage(ComponentName, String, boolean, Set)
+     */
+    public final boolean isLockdownEnabled() {
+        try {
+            return getService().isCallerCurrentAlwaysOnVpnLockdownApp();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Return the communication interface to the service. This method returns
+     * {@code null} on {@link Intent}s other than {@link #SERVICE_INTERFACE}
+     * action. Applications overriding this method must identify the intent
+     * and return the corresponding interface accordingly.
+     *
+     * @see Service#onBind
+     */
+    @Override
+    public IBinder onBind(Intent intent) {
+        if (intent != null && SERVICE_INTERFACE.equals(intent.getAction())) {
+            return new Callback();
+        }
+        return null;
+    }
+
+    /**
+     * Invoked when the application is revoked. At this moment, the VPN
+     * interface is already deactivated by the system. The application should
+     * close the file descriptor and shut down gracefully. The default
+     * implementation of this method is calling {@link Service#stopSelf()}.
+     *
+     * <p class="note">Calls to this method may not happen on the main thread
+     * of the process.
+     *
+     * @see #prepare
+     */
+    public void onRevoke() {
+        stopSelf();
+    }
+
+    /**
+     * Use raw Binder instead of AIDL since now there is only one usage.
+     */
+    private class Callback extends Binder {
+        @Override
+        protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) {
+            if (code == IBinder.LAST_CALL_TRANSACTION) {
+                onRevoke();
+                return true;
+            }
+            return false;
+        }
+    }
+
+    /**
+     * Private method to validate address and prefixLength.
+     */
+    private static void check(InetAddress address, int prefixLength) {
+        if (address.isLoopbackAddress()) {
+            throw new IllegalArgumentException("Bad address");
+        }
+        if (address instanceof Inet4Address) {
+            if (prefixLength < 0 || prefixLength > 32) {
+                throw new IllegalArgumentException("Bad prefixLength");
+            }
+        } else if (address instanceof Inet6Address) {
+            if (prefixLength < 0 || prefixLength > 128) {
+                throw new IllegalArgumentException("Bad prefixLength");
+            }
+        } else {
+            throw new IllegalArgumentException("Unsupported family");
+        }
+    }
+
+    /**
+     * Helper class to create a VPN interface. This class should be always
+     * used within the scope of the outer {@link VpnService}.
+     *
+     * @see VpnService
+     */
+    public class Builder {
+
+        private final VpnConfig mConfig = new VpnConfig();
+        @UnsupportedAppUsage
+        private final List<LinkAddress> mAddresses = new ArrayList<LinkAddress>();
+        @UnsupportedAppUsage
+        private final List<RouteInfo> mRoutes = new ArrayList<RouteInfo>();
+
+        public Builder() {
+            mConfig.user = VpnService.this.getClass().getName();
+        }
+
+        /**
+         * Set the name of this session. It will be displayed in
+         * system-managed dialogs and notifications. This is recommended
+         * not required.
+         */
+        @NonNull
+        public Builder setSession(@NonNull String session) {
+            mConfig.session = session;
+            return this;
+        }
+
+        /**
+         * Set the {@link PendingIntent} to an activity for users to
+         * configure the VPN connection. If it is not set, the button
+         * to configure will not be shown in system-managed dialogs.
+         */
+        @NonNull
+        public Builder setConfigureIntent(@NonNull PendingIntent intent) {
+            mConfig.configureIntent = intent;
+            return this;
+        }
+
+        /**
+         * Set the maximum transmission unit (MTU) of the VPN interface. If
+         * it is not set, the default value in the operating system will be
+         * used.
+         *
+         * @throws IllegalArgumentException if the value is not positive.
+         */
+        @NonNull
+        public Builder setMtu(int mtu) {
+            if (mtu <= 0) {
+                throw new IllegalArgumentException("Bad mtu");
+            }
+            mConfig.mtu = mtu;
+            return this;
+        }
+
+        /**
+         * Sets an HTTP proxy for the VPN network. This proxy is only a recommendation
+         * and it is possible that some apps will ignore it.
+         */
+        @NonNull
+        public Builder setHttpProxy(@NonNull ProxyInfo proxyInfo) {
+            mConfig.proxyInfo = proxyInfo;
+            return this;
+        }
+
+        /**
+         * Add a network address to the VPN interface. Both IPv4 and IPv6
+         * addresses are supported. At least one address must be set before
+         * calling {@link #establish}.
+         *
+         * Adding an address implicitly allows traffic from that address family
+         * (i.e., IPv4 or IPv6) to be routed over the VPN. @see #allowFamily
+         *
+         * @throws IllegalArgumentException if the address is invalid.
+         */
+        @NonNull
+        public Builder addAddress(@NonNull InetAddress address, int prefixLength) {
+            check(address, prefixLength);
+
+            if (address.isAnyLocalAddress()) {
+                throw new IllegalArgumentException("Bad address");
+            }
+            mAddresses.add(new LinkAddress(address, prefixLength));
+            mConfig.updateAllowedFamilies(address);
+            return this;
+        }
+
+        /**
+         * Convenience method to add a network address to the VPN interface
+         * using a numeric address string. See {@link InetAddress} for the
+         * definitions of numeric address formats.
+         *
+         * Adding an address implicitly allows traffic from that address family
+         * (i.e., IPv4 or IPv6) to be routed over the VPN. @see #allowFamily
+         *
+         * @throws IllegalArgumentException if the address is invalid.
+         * @see #addAddress(InetAddress, int)
+         */
+        @NonNull
+        public Builder addAddress(@NonNull String address, int prefixLength) {
+            return addAddress(InetAddress.parseNumericAddress(address), prefixLength);
+        }
+
+        /**
+         * Add a network route to the VPN interface. Both IPv4 and IPv6
+         * routes are supported.
+         *
+         * Adding a route implicitly allows traffic from that address family
+         * (i.e., IPv4 or IPv6) to be routed over the VPN. @see #allowFamily
+         *
+         * @throws IllegalArgumentException if the route is invalid.
+         */
+        @NonNull
+        public Builder addRoute(@NonNull InetAddress address, int prefixLength) {
+            check(address, prefixLength);
+
+            int offset = prefixLength / 8;
+            byte[] bytes = address.getAddress();
+            if (offset < bytes.length) {
+                for (bytes[offset] <<= prefixLength % 8; offset < bytes.length; ++offset) {
+                    if (bytes[offset] != 0) {
+                        throw new IllegalArgumentException("Bad address");
+                    }
+                }
+            }
+            mRoutes.add(new RouteInfo(new IpPrefix(address, prefixLength), null));
+            mConfig.updateAllowedFamilies(address);
+            return this;
+        }
+
+        /**
+         * Convenience method to add a network route to the VPN interface
+         * using a numeric address string. See {@link InetAddress} for the
+         * definitions of numeric address formats.
+         *
+         * Adding a route implicitly allows traffic from that address family
+         * (i.e., IPv4 or IPv6) to be routed over the VPN. @see #allowFamily
+         *
+         * @throws IllegalArgumentException if the route is invalid.
+         * @see #addRoute(InetAddress, int)
+         */
+        @NonNull
+        public Builder addRoute(@NonNull String address, int prefixLength) {
+            return addRoute(InetAddress.parseNumericAddress(address), prefixLength);
+        }
+
+        /**
+         * Add a DNS server to the VPN connection. Both IPv4 and IPv6
+         * addresses are supported. If none is set, the DNS servers of
+         * the default network will be used.
+         *
+         * Adding a server implicitly allows traffic from that address family
+         * (i.e., IPv4 or IPv6) to be routed over the VPN. @see #allowFamily
+         *
+         * @throws IllegalArgumentException if the address is invalid.
+         */
+        @NonNull
+        public Builder addDnsServer(@NonNull InetAddress address) {
+            if (address.isLoopbackAddress() || address.isAnyLocalAddress()) {
+                throw new IllegalArgumentException("Bad address");
+            }
+            if (mConfig.dnsServers == null) {
+                mConfig.dnsServers = new ArrayList<String>();
+            }
+            mConfig.dnsServers.add(address.getHostAddress());
+            return this;
+        }
+
+        /**
+         * Convenience method to add a DNS server to the VPN connection
+         * using a numeric address string. See {@link InetAddress} for the
+         * definitions of numeric address formats.
+         *
+         * Adding a server implicitly allows traffic from that address family
+         * (i.e., IPv4 or IPv6) to be routed over the VPN. @see #allowFamily
+         *
+         * @throws IllegalArgumentException if the address is invalid.
+         * @see #addDnsServer(InetAddress)
+         */
+        @NonNull
+        public Builder addDnsServer(@NonNull String address) {
+            return addDnsServer(InetAddress.parseNumericAddress(address));
+        }
+
+        /**
+         * Add a search domain to the DNS resolver.
+         */
+        @NonNull
+        public Builder addSearchDomain(@NonNull String domain) {
+            if (mConfig.searchDomains == null) {
+                mConfig.searchDomains = new ArrayList<String>();
+            }
+            mConfig.searchDomains.add(domain);
+            return this;
+        }
+
+        /**
+         * Allows traffic from the specified address family.
+         *
+         * By default, if no address, route or DNS server of a specific family (IPv4 or IPv6) is
+         * added to this VPN, then all outgoing traffic of that family is blocked. If any address,
+         * route or DNS server is added, that family is allowed.
+         *
+         * This method allows an address family to be unblocked even without adding an address,
+         * route or DNS server of that family. Traffic of that family will then typically
+         * fall-through to the underlying network if it's supported.
+         *
+         * {@code family} must be either {@code AF_INET} (for IPv4) or {@code AF_INET6} (for IPv6).
+         * {@link IllegalArgumentException} is thrown if it's neither.
+         *
+         * @param family The address family ({@code AF_INET} or {@code AF_INET6}) to allow.
+         *
+         * @return this {@link Builder} object to facilitate chaining of method calls.
+         */
+        @NonNull
+        public Builder allowFamily(int family) {
+            if (family == AF_INET) {
+                mConfig.allowIPv4 = true;
+            } else if (family == AF_INET6) {
+                mConfig.allowIPv6 = true;
+            } else {
+                throw new IllegalArgumentException(family + " is neither " + AF_INET + " nor " +
+                        AF_INET6);
+            }
+            return this;
+        }
+
+        private void verifyApp(String packageName) throws PackageManager.NameNotFoundException {
+            IPackageManager pm = IPackageManager.Stub.asInterface(
+                    ServiceManager.getService("package"));
+            try {
+                pm.getApplicationInfo(packageName, 0, UserHandle.getCallingUserId());
+            } catch (RemoteException e) {
+                throw new IllegalStateException(e);
+            }
+        }
+
+        /**
+         * Adds an application that's allowed to access the VPN connection.
+         *
+         * If this method is called at least once, only applications added through this method (and
+         * no others) are allowed access. Else (if this method is never called), all applications
+         * are allowed by default.  If some applications are added, other, un-added applications
+         * will use networking as if the VPN wasn't running.
+         *
+         * A {@link Builder} may have only a set of allowed applications OR a set of disallowed
+         * ones, but not both. Calling this method after {@link #addDisallowedApplication} has
+         * already been called, or vice versa, will throw an {@link UnsupportedOperationException}.
+         *
+         * {@code packageName} must be the canonical name of a currently installed application.
+         * {@link PackageManager.NameNotFoundException} is thrown if there's no such application.
+         *
+         * @throws PackageManager.NameNotFoundException If the application isn't installed.
+         *
+         * @param packageName The full name (e.g.: "com.google.apps.contacts") of an application.
+         *
+         * @return this {@link Builder} object to facilitate chaining method calls.
+         */
+        @NonNull
+        public Builder addAllowedApplication(@NonNull String packageName)
+                throws PackageManager.NameNotFoundException {
+            if (mConfig.disallowedApplications != null) {
+                throw new UnsupportedOperationException("addDisallowedApplication already called");
+            }
+            verifyApp(packageName);
+            if (mConfig.allowedApplications == null) {
+                mConfig.allowedApplications = new ArrayList<String>();
+            }
+            mConfig.allowedApplications.add(packageName);
+            return this;
+        }
+
+        /**
+         * Adds an application that's denied access to the VPN connection.
+         *
+         * By default, all applications are allowed access, except for those denied through this
+         * method.  Denied applications will use networking as if the VPN wasn't running.
+         *
+         * A {@link Builder} may have only a set of allowed applications OR a set of disallowed
+         * ones, but not both. Calling this method after {@link #addAllowedApplication} has already
+         * been called, or vice versa, will throw an {@link UnsupportedOperationException}.
+         *
+         * {@code packageName} must be the canonical name of a currently installed application.
+         * {@link PackageManager.NameNotFoundException} is thrown if there's no such application.
+         *
+         * @throws PackageManager.NameNotFoundException If the application isn't installed.
+         *
+         * @param packageName The full name (e.g.: "com.google.apps.contacts") of an application.
+         *
+         * @return this {@link Builder} object to facilitate chaining method calls.
+         */
+        @NonNull
+        public Builder addDisallowedApplication(@NonNull String packageName)
+                throws PackageManager.NameNotFoundException {
+            if (mConfig.allowedApplications != null) {
+                throw new UnsupportedOperationException("addAllowedApplication already called");
+            }
+            verifyApp(packageName);
+            if (mConfig.disallowedApplications == null) {
+                mConfig.disallowedApplications = new ArrayList<String>();
+            }
+            mConfig.disallowedApplications.add(packageName);
+            return this;
+        }
+
+        /**
+         * Allows all apps to bypass this VPN connection.
+         *
+         * By default, all traffic from apps is forwarded through the VPN interface and it is not
+         * possible for apps to side-step the VPN. If this method is called, apps may use methods
+         * such as {@link ConnectivityManager#bindProcessToNetwork} to instead send/receive
+         * directly over the underlying network or any other network they have permissions for.
+         *
+         * @return this {@link Builder} object to facilitate chaining of method calls.
+         */
+        @NonNull
+        public Builder allowBypass() {
+            mConfig.allowBypass = true;
+            return this;
+        }
+
+        /**
+         * Sets the VPN interface's file descriptor to be in blocking/non-blocking mode.
+         *
+         * By default, the file descriptor returned by {@link #establish} is non-blocking.
+         *
+         * @param blocking True to put the descriptor into blocking mode; false for non-blocking.
+         *
+         * @return this {@link Builder} object to facilitate chaining method calls.
+         */
+        @NonNull
+        public Builder setBlocking(boolean blocking) {
+            mConfig.blocking = blocking;
+            return this;
+        }
+
+        /**
+         * Sets the underlying networks used by the VPN for its upstream connections.
+         *
+         * @see VpnService#setUnderlyingNetworks
+         *
+         * @param networks An array of networks the VPN uses to tunnel traffic to/from its servers.
+         *
+         * @return this {@link Builder} object to facilitate chaining method calls.
+         */
+        @NonNull
+        public Builder setUnderlyingNetworks(@Nullable Network[] networks) {
+            mConfig.underlyingNetworks = networks != null ? networks.clone() : null;
+            return this;
+        }
+
+        /**
+         * Marks the VPN network as metered. A VPN network is classified as metered when the user is
+         * sensitive to heavy data usage due to monetary costs and/or data limitations. In such
+         * cases, you should set this to {@code true} so that apps on the system can avoid doing
+         * large data transfers. Otherwise, set this to {@code false}. Doing so would cause VPN
+         * network to inherit its meteredness from its underlying networks.
+         *
+         * <p>VPN apps targeting {@link android.os.Build.VERSION_CODES#Q} or above will be
+         * considered metered by default.
+         *
+         * @param isMetered {@code true} if VPN network should be treated as metered regardless of
+         *     underlying network meteredness
+         * @return this {@link Builder} object to facilitate chaining method calls
+         * @see #setUnderlyingNetworks(Networks[])
+         * @see ConnectivityManager#isActiveNetworkMetered()
+         */
+        @NonNull
+        public Builder setMetered(boolean isMetered) {
+            mConfig.isMetered = isMetered;
+            return this;
+        }
+
+        /**
+         * Create a VPN interface using the parameters supplied to this
+         * builder. The interface works on IP packets, and a file descriptor
+         * is returned for the application to access them. Each read
+         * retrieves an outgoing packet which was routed to the interface.
+         * Each write injects an incoming packet just like it was received
+         * from the interface. The file descriptor is put into non-blocking
+         * mode by default to avoid blocking Java threads. To use the file
+         * descriptor completely in native space, see
+         * {@link ParcelFileDescriptor#detachFd()}. The application MUST
+         * close the file descriptor when the VPN connection is terminated.
+         * The VPN interface will be removed and the network will be
+         * restored by the system automatically.
+         *
+         * <p>To avoid conflicts, there can be only one active VPN interface
+         * at the same time. Usually network parameters are never changed
+         * during the lifetime of a VPN connection. It is also common for an
+         * application to create a new file descriptor after closing the
+         * previous one. However, it is rare but not impossible to have two
+         * interfaces while performing a seamless handover. In this case, the
+         * old interface will be deactivated when the new one is created
+         * successfully. Both file descriptors are valid but now outgoing
+         * packets will be routed to the new interface. Therefore, after
+         * draining the old file descriptor, the application MUST close it
+         * and start using the new file descriptor. If the new interface
+         * cannot be created, the existing interface and its file descriptor
+         * remain untouched.
+         *
+         * <p>An exception will be thrown if the interface cannot be created
+         * for any reason. However, this method returns {@code null} if the
+         * application is not prepared or is revoked. This helps solve
+         * possible race conditions between other VPN applications.
+         *
+         * @return {@link ParcelFileDescriptor} of the VPN interface, or
+         *         {@code null} if the application is not prepared.
+         * @throws IllegalArgumentException if a parameter is not accepted
+         *         by the operating system.
+         * @throws IllegalStateException if a parameter cannot be applied
+         *         by the operating system.
+         * @throws SecurityException if the service is not properly declared
+         *         in {@code AndroidManifest.xml}.
+         * @see VpnService
+         */
+        @Nullable
+        public ParcelFileDescriptor establish() {
+            mConfig.addresses = mAddresses;
+            mConfig.routes = mRoutes;
+
+            try {
+                return getService().establishVpn(mConfig);
+            } catch (RemoteException e) {
+                throw new IllegalStateException(e);
+            }
+        }
+    }
+}
diff --git a/android/net/WebAddress.java b/android/net/WebAddress.java
new file mode 100644
index 0000000..fbc281f
--- /dev/null
+++ b/android/net/WebAddress.java
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2006 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 static android.util.Patterns.GOOD_IRI_CHAR;
+
+import android.annotation.SystemApi;
+import android.annotation.UnsupportedAppUsage;
+import android.os.Build;
+
+import java.util.Locale;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * {@hide}
+ *
+ * Web Address Parser
+ *
+ * This is called WebAddress, rather than URL or URI, because it
+ * attempts to parse the stuff that a user will actually type into a
+ * browser address widget.
+ *
+ * Unlike java.net.uri, this parser will not choke on URIs missing
+ * schemes.  It will only throw a ParseException if the input is
+ * really hosed.
+ *
+ * If given an https scheme but no port, fills in port
+ *
+ */
+// TODO(igsolla): remove WebAddress from the system SDK once the WebView apk does not
+// longer need to be binary compatible with the API 21 version of the framework.
+@SystemApi
+public class WebAddress {
+
+    @UnsupportedAppUsage
+    private String mScheme;
+    @UnsupportedAppUsage
+    private String mHost;
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    private int mPort;
+    @UnsupportedAppUsage
+    private String mPath;
+    private String mAuthInfo;
+
+    static final int MATCH_GROUP_SCHEME = 1;
+    static final int MATCH_GROUP_AUTHORITY = 2;
+    static final int MATCH_GROUP_HOST = 3;
+    static final int MATCH_GROUP_PORT = 4;
+    static final int MATCH_GROUP_PATH = 5;
+
+    static Pattern sAddressPattern = Pattern.compile(
+            /* scheme    */ "(?:(http|https|file)\\:\\/\\/)?" +
+            /* authority */ "(?:([-A-Za-z0-9$_.+!*'(),;?&=]+(?:\\:[-A-Za-z0-9$_.+!*'(),;?&=]+)?)@)?" +
+            /* host      */ "([" + GOOD_IRI_CHAR + "%_-][" + GOOD_IRI_CHAR + "%_\\.-]*|\\[[0-9a-fA-F:\\.]+\\])?" +
+            /* port      */ "(?:\\:([0-9]*))?" +
+            /* path      */ "(\\/?[^#]*)?" +
+            /* anchor    */ ".*", Pattern.CASE_INSENSITIVE);
+
+    /** parses given uriString. */
+    public WebAddress(String address) throws ParseException {
+        if (address == null) {
+            throw new NullPointerException();
+        }
+
+        // android.util.Log.d(LOGTAG, "WebAddress: " + address);
+
+        mScheme = "";
+        mHost = "";
+        mPort = -1;
+        mPath = "/";
+        mAuthInfo = "";
+
+        Matcher m = sAddressPattern.matcher(address);
+        String t;
+        if (m.matches()) {
+            t = m.group(MATCH_GROUP_SCHEME);
+            if (t != null) mScheme = t.toLowerCase(Locale.ROOT);
+            t = m.group(MATCH_GROUP_AUTHORITY);
+            if (t != null) mAuthInfo = t;
+            t = m.group(MATCH_GROUP_HOST);
+            if (t != null) mHost = t;
+            t = m.group(MATCH_GROUP_PORT);
+            if (t != null && t.length() > 0) {
+                // The ':' character is not returned by the regex.
+                try {
+                    mPort = Integer.parseInt(t);
+                } catch (NumberFormatException ex) {
+                    throw new ParseException("Bad port");
+                }
+            }
+            t = m.group(MATCH_GROUP_PATH);
+            if (t != null && t.length() > 0) {
+                /* handle busted myspace frontpage redirect with
+                   missing initial "/" */
+                if (t.charAt(0) == '/') {
+                    mPath = t;
+                } else {
+                    mPath = "/" + t;
+                }
+            }
+
+        } else {
+            // nothing found... outa here
+            throw new ParseException("Bad address");
+        }
+
+        /* Get port from scheme or scheme from port, if necessary and
+           possible */
+        if (mPort == 443 && mScheme.equals("")) {
+            mScheme = "https";
+        } else if (mPort == -1) {
+            if (mScheme.equals("https"))
+                mPort = 443;
+            else
+                mPort = 80; // default
+        }
+        if (mScheme.equals("")) mScheme = "http";
+    }
+
+    @Override
+    public String toString() {
+        String port = "";
+        if ((mPort != 443 && mScheme.equals("https")) ||
+            (mPort != 80 && mScheme.equals("http"))) {
+            port = ":" + Integer.toString(mPort);
+        }
+        String authInfo = "";
+        if (mAuthInfo.length() > 0) {
+            authInfo = mAuthInfo + "@";
+        }
+
+        return mScheme + "://" + authInfo + mHost + port + mPath;
+    }
+
+    /** {@hide} */
+    public void setScheme(String scheme) {
+      mScheme = scheme;
+    }
+
+    /** {@hide} */
+    @UnsupportedAppUsage
+    public String getScheme() {
+      return mScheme;
+    }
+
+    /** {@hide} */
+    @UnsupportedAppUsage
+    public void setHost(String host) {
+      mHost = host;
+    }
+
+    /** {@hide} */
+    @UnsupportedAppUsage
+    public String getHost() {
+      return mHost;
+    }
+
+    /** {@hide} */
+    public void setPort(int port) {
+      mPort = port;
+    }
+
+    /** {@hide} */
+    @UnsupportedAppUsage
+    public int getPort() {
+      return mPort;
+    }
+
+    /** {@hide} */
+    @UnsupportedAppUsage
+    public void setPath(String path) {
+      mPath = path;
+    }
+
+    /** {@hide} */
+    @UnsupportedAppUsage
+    public String getPath() {
+      return mPath;
+    }
+
+    /** {@hide} */
+    public void setAuthInfo(String authInfo) {
+      mAuthInfo = authInfo;
+    }
+
+    /** {@hide} */
+    @UnsupportedAppUsage
+    public String getAuthInfo() {
+      return mAuthInfo;
+    }
+}
diff --git a/android/net/WifiKey.java b/android/net/WifiKey.java
new file mode 100644
index 0000000..e3a93a8
--- /dev/null
+++ b/android/net/WifiKey.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.net;
+
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Objects;
+import java.util.regex.Pattern;
+
+/**
+ * Information identifying a Wi-Fi network.
+ * @see NetworkKey
+ *
+ * @hide
+ */
+@SystemApi
+public class WifiKey implements Parcelable {
+
+    // Patterns used for validation.
+    private static final Pattern SSID_PATTERN = Pattern.compile("(\".*\")|(0x[\\p{XDigit}]+)",
+            Pattern.DOTALL);
+    private static final Pattern BSSID_PATTERN =
+            Pattern.compile("([\\p{XDigit}]{2}:){5}[\\p{XDigit}]{2}");
+
+    /**
+     * The service set identifier (SSID) of an 802.11 network. If the SSID can be decoded as
+     * UTF-8, it will be surrounded by double quotation marks. Otherwise, it will be a string of
+     * hex digits starting with 0x.
+     */
+    public final String ssid;
+
+    /**
+     * The basic service set identifier (BSSID) of an access point for this network. This will
+     * be in the form of a six-byte MAC address: {@code XX:XX:XX:XX:XX:XX}, where each X is a
+     * hexadecimal digit.
+     */
+    public final String bssid;
+
+    /**
+     * Construct a new {@link WifiKey} for the given Wi-Fi SSID/BSSID pair.
+     *
+     * @param ssid the service set identifier (SSID) of an 802.11 network. If the SSID can be
+     *         decoded as UTF-8, it should be surrounded by double quotation marks. Otherwise,
+     *         it should be a string of hex digits starting with 0x.
+     * @param bssid the basic service set identifier (BSSID) of this network's access point.
+     *         This should be in the form of a six-byte MAC address: {@code XX:XX:XX:XX:XX:XX},
+     *         where each X is a hexadecimal digit.
+     * @throws IllegalArgumentException if either the SSID or BSSID is invalid.
+     */
+    public WifiKey(String ssid, String bssid) {
+        if (ssid == null || !SSID_PATTERN.matcher(ssid).matches()) {
+            throw new IllegalArgumentException("Invalid ssid: " + ssid);
+        }
+        if (bssid == null || !BSSID_PATTERN.matcher(bssid).matches()) {
+            throw new IllegalArgumentException("Invalid bssid: " + bssid);
+        }
+        this.ssid = ssid;
+        this.bssid = bssid;
+    }
+
+    private WifiKey(Parcel in) {
+        ssid = in.readString();
+        bssid = in.readString();
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeString(ssid);
+        out.writeString(bssid);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) return true;
+        if (o == null || getClass() != o.getClass()) return false;
+
+        WifiKey wifiKey = (WifiKey) o;
+
+        return Objects.equals(ssid, wifiKey.ssid) && Objects.equals(bssid, wifiKey.bssid);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(ssid, bssid);
+    }
+
+    @Override
+    public String toString() {
+        return "WifiKey[SSID=" + ssid + ",BSSID=" + bssid + "]";
+    }
+
+    public static final @android.annotation.NonNull Creator<WifiKey> CREATOR =
+            new Creator<WifiKey>() {
+                @Override
+                public WifiKey createFromParcel(Parcel in) {
+                    return new WifiKey(in);
+                }
+
+                @Override
+                public WifiKey[] newArray(int size) {
+                    return new WifiKey[size];
+                }
+            };
+}
diff --git a/android/net/WifiLinkQualityInfo.java b/android/net/WifiLinkQualityInfo.java
new file mode 100644
index 0000000..20ec9a7
--- /dev/null
+++ b/android/net/WifiLinkQualityInfo.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2013 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.os.Parcel;
+
+/**
+ *  Class that represents useful attributes of wifi network links
+ *  such as the upload/download throughput or error rate etc.
+ *  @hide
+ */
+public class WifiLinkQualityInfo extends LinkQualityInfo {
+
+    /* Indicates Wifi network type such as b/g etc*/
+    private int  mType = UNKNOWN_INT;
+
+    private String mBssid;
+
+    /* Rssi found by scans */
+    private int  mRssi = UNKNOWN_INT;
+
+    /* packet statistics */
+    private long mTxGood = UNKNOWN_LONG;
+    private long mTxBad = UNKNOWN_LONG;
+
+    /**
+     * Implement the Parcelable interface.
+     * @hide
+     */
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        super.writeToParcel(dest, flags, OBJECT_TYPE_WIFI_LINK_QUALITY_INFO);
+
+        dest.writeInt(mType);
+        dest.writeInt(mRssi);
+        dest.writeLong(mTxGood);
+        dest.writeLong(mTxBad);
+
+        dest.writeString(mBssid);
+    }
+
+    /* Un-parceling helper */
+    /**
+     * @hide
+     */
+    public static WifiLinkQualityInfo createFromParcelBody(Parcel in) {
+        WifiLinkQualityInfo li = new WifiLinkQualityInfo();
+
+        li.initializeFromParcel(in);
+
+        li.mType =  in.readInt();
+        li.mRssi =  in.readInt();
+        li.mTxGood =  in.readLong();
+        li.mTxBad =  in.readLong();
+
+        li.mBssid =  in.readString();
+
+        return li;
+    }
+
+    /**
+     * returns Wifi network type
+     * @return network type or {@link android.net.LinkQualityInfo#UNKNOWN_INT}
+     */
+    public int getType() {
+        return mType;
+    }
+
+    /**
+     * @hide
+     */
+    public void setType(int type) {
+        mType = type;
+    }
+
+    /**
+     * returns BSSID of the access point
+     * @return the BSSID, in the form of a six-byte MAC address: {@code XX:XX:XX:XX:XX:XX} or null
+     */
+    public String getBssid() {
+        return mBssid;
+    }
+
+    /**
+     * @hide
+     */
+    public void setBssid(String bssid) {
+        mBssid = bssid;
+    }
+
+    /**
+     * returns RSSI of the network in raw form
+     * @return un-normalized RSSI or {@link android.net.LinkQualityInfo#UNKNOWN_INT}
+     */
+    public int getRssi() {
+        return mRssi;
+    }
+
+    /**
+     * @hide
+     */
+    public void setRssi(int rssi) {
+        mRssi = rssi;
+    }
+
+    /**
+     * returns number of packets transmitted without error
+     * @return number of packets or {@link android.net.LinkQualityInfo#UNKNOWN_LONG}
+     */
+    public long getTxGood() {
+        return mTxGood;
+    }
+
+    /**
+     * @hide
+     */
+    public void setTxGood(long txGood) {
+        mTxGood = txGood;
+    }
+
+    /**
+     * returns number of transmitted packets that encountered errors
+     * @return number of packets or {@link android.net.LinkQualityInfo#UNKNOWN_LONG}
+     */
+    public long getTxBad() {
+        return mTxBad;
+    }
+
+    /**
+     * @hide
+     */
+    public void setTxBad(long txBad) {
+        mTxBad = txBad;
+    }
+}
diff --git a/android/net/apf/ApfCapabilities.java b/android/net/apf/ApfCapabilities.java
new file mode 100644
index 0000000..4dd2ace
--- /dev/null
+++ b/android/net/apf/ApfCapabilities.java
@@ -0,0 +1,133 @@
+/*
+ * 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.apf;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.content.res.Resources;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.R;
+
+/**
+ * APF program support capabilities. APF stands for Android Packet Filtering and it is a flexible
+ * way to drop unwanted network packets to save power.
+ *
+ * See documentation at hardware/google/apf/apf.h
+ *
+ * This class is immutable.
+ * @hide
+ */
+@SystemApi
+@TestApi
+public final class ApfCapabilities implements Parcelable {
+    /**
+     * Version of APF instruction set supported for packet filtering. 0 indicates no support for
+     * packet filtering using APF programs.
+     */
+    public final int apfVersionSupported;
+
+    /**
+     * Maximum size of APF program allowed.
+     */
+    public final int maximumApfProgramSize;
+
+    /**
+     * Format of packets passed to APF filter. Should be one of ARPHRD_*
+     */
+    public final int apfPacketFormat;
+
+    public ApfCapabilities(
+            int apfVersionSupported, int maximumApfProgramSize, int apfPacketFormat) {
+        this.apfVersionSupported = apfVersionSupported;
+        this.maximumApfProgramSize = maximumApfProgramSize;
+        this.apfPacketFormat = apfPacketFormat;
+    }
+
+    private ApfCapabilities(Parcel in) {
+        apfVersionSupported = in.readInt();
+        maximumApfProgramSize = in.readInt();
+        apfPacketFormat = in.readInt();
+    }
+
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(apfVersionSupported);
+        dest.writeInt(maximumApfProgramSize);
+        dest.writeInt(apfPacketFormat);
+    }
+
+    public static final Creator<ApfCapabilities> CREATOR = new Creator<ApfCapabilities>() {
+        @Override
+        public ApfCapabilities createFromParcel(Parcel in) {
+            return new ApfCapabilities(in);
+        }
+
+        @Override
+        public ApfCapabilities[] newArray(int size) {
+            return new ApfCapabilities[size];
+        }
+    };
+
+    @Override
+    public String toString() {
+        return String.format("%s{version: %d, maxSize: %d, format: %d}", getClass().getSimpleName(),
+                apfVersionSupported, maximumApfProgramSize, apfPacketFormat);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof  ApfCapabilities)) return false;
+        final ApfCapabilities other = (ApfCapabilities) obj;
+        return apfVersionSupported == other.apfVersionSupported
+                && maximumApfProgramSize == other.maximumApfProgramSize
+                && apfPacketFormat == other.apfPacketFormat;
+    }
+
+    /**
+     * Determines whether the APF interpreter advertises support for the data buffer access opcodes
+     * LDDW (LoaD Data Word) and STDW (STore Data Word). Full LDDW (LoaD Data Word) and
+     * STDW (STore Data Word) support is present from APFv4 on.
+     *
+     * @return {@code true} if the IWifiStaIface#readApfPacketFilterData is supported.
+     */
+    public boolean hasDataAccess() {
+        return apfVersionSupported >= 4;
+    }
+
+    /**
+     * @return Whether the APF Filter in the device should filter out IEEE 802.3 Frames.
+     */
+    public static boolean getApfDrop8023Frames() {
+        return Resources.getSystem().getBoolean(R.bool.config_apfDrop802_3Frames);
+    }
+
+    /**
+     * @return An array of blacklisted EtherType, packets with EtherTypes within it will be dropped.
+     */
+    public static @NonNull int[] getApfEtherTypeBlackList() {
+        return Resources.getSystem().getIntArray(R.array.config_apfEthTypeBlackList);
+    }
+}
diff --git a/android/net/dhcp/DhcpServerCallbacks.java b/android/net/dhcp/DhcpServerCallbacks.java
new file mode 100644
index 0000000..bb56876
--- /dev/null
+++ b/android/net/dhcp/DhcpServerCallbacks.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2018 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.dhcp;
+
+/**
+ * Convenience wrapper around IDhcpServerCallbacks.Stub that implements getInterfaceVersion().
+ * @hide
+ */
+public abstract class DhcpServerCallbacks extends IDhcpServerCallbacks.Stub {
+    // TODO: add @Override here once the API is versioned
+
+    /**
+     * Get the version of the aidl interface implemented by the callbacks.
+     */
+    public int getInterfaceVersion() {
+        // TODO: return IDhcpServerCallbacks.VERSION;
+        return 0;
+    }
+}
diff --git a/android/net/dhcp/DhcpServingParamsParcelExt.java b/android/net/dhcp/DhcpServingParamsParcelExt.java
new file mode 100644
index 0000000..1fe2328
--- /dev/null
+++ b/android/net/dhcp/DhcpServingParamsParcelExt.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2018 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.dhcp;
+
+import static android.net.shared.Inet4AddressUtils.inet4AddressToIntHTH;
+
+import android.annotation.NonNull;
+import android.net.LinkAddress;
+
+import com.google.android.collect.Sets;
+
+import java.net.Inet4Address;
+import java.util.Collection;
+import java.util.Set;
+
+/**
+ * Subclass of {@link DhcpServingParamsParcel} with additional utility methods for building.
+ *
+ * <p>This utility class does not check for validity of the parameters: invalid parameters are
+ * reported by the receiving module when unparceling the parcel.
+ *
+ * @see DhcpServingParams
+ * @hide
+ */
+public class DhcpServingParamsParcelExt extends DhcpServingParamsParcel {
+    public static final int MTU_UNSET = 0;
+
+    /**
+     * Set the server address and served prefix for the DHCP server.
+     *
+     * <p>This parameter is required.
+     */
+    public DhcpServingParamsParcelExt setServerAddr(@NonNull LinkAddress serverAddr) {
+        this.serverAddr = inet4AddressToIntHTH((Inet4Address) serverAddr.getAddress());
+        this.serverAddrPrefixLength = serverAddr.getPrefixLength();
+        return this;
+    }
+
+    /**
+     * Set the default routers to be advertised to DHCP clients.
+     *
+     * <p>Each router must be inside the served prefix. This may be an empty set, but it must
+     * always be set explicitly.
+     */
+    public DhcpServingParamsParcelExt setDefaultRouters(@NonNull Set<Inet4Address> defaultRouters) {
+        this.defaultRouters = toIntArray(defaultRouters);
+        return this;
+    }
+
+    /**
+     * Set the default routers to be advertised to DHCP clients.
+     *
+     * <p>Each router must be inside the served prefix. This may be an empty list of routers,
+     * but it must always be set explicitly.
+     */
+    public DhcpServingParamsParcelExt setDefaultRouters(@NonNull Inet4Address... defaultRouters) {
+        return setDefaultRouters(Sets.newArraySet(defaultRouters));
+    }
+
+    /**
+     * Convenience method to build the parameters with no default router.
+     *
+     * <p>Equivalent to calling {@link #setDefaultRouters(Inet4Address...)} with no address.
+     */
+    public DhcpServingParamsParcelExt setNoDefaultRouter() {
+        return setDefaultRouters();
+    }
+
+    /**
+     * Set the DNS servers to be advertised to DHCP clients.
+     *
+     * <p>This may be an empty set, but it must always be set explicitly.
+     */
+    public DhcpServingParamsParcelExt setDnsServers(@NonNull Set<Inet4Address> dnsServers) {
+        this.dnsServers = toIntArray(dnsServers);
+        return this;
+    }
+
+    /**
+     * Set the DNS servers to be advertised to DHCP clients.
+     *
+     * <p>This may be an empty list of servers, but it must always be set explicitly.
+     */
+    public DhcpServingParamsParcelExt setDnsServers(@NonNull Inet4Address... dnsServers) {
+        return setDnsServers(Sets.newArraySet(dnsServers));
+    }
+
+    /**
+     * Convenience method to build the parameters with no DNS server.
+     *
+     * <p>Equivalent to calling {@link #setDnsServers(Inet4Address...)} with no address.
+     */
+    public DhcpServingParamsParcelExt setNoDnsServer() {
+        return setDnsServers();
+    }
+
+    /**
+     * Set excluded addresses that the DHCP server is not allowed to assign to clients.
+     *
+     * <p>This parameter is optional. DNS servers and default routers are always excluded
+     * and do not need to be set here.
+     */
+    public DhcpServingParamsParcelExt setExcludedAddrs(@NonNull Set<Inet4Address> excludedAddrs) {
+        this.excludedAddrs = toIntArray(excludedAddrs);
+        return this;
+    }
+
+    /**
+     * Set excluded addresses that the DHCP server is not allowed to assign to clients.
+     *
+     * <p>This parameter is optional. DNS servers and default routers are always excluded
+     * and do not need to be set here.
+     */
+    public DhcpServingParamsParcelExt setExcludedAddrs(@NonNull Inet4Address... excludedAddrs) {
+        return setExcludedAddrs(Sets.newArraySet(excludedAddrs));
+    }
+
+    /**
+     * Set the lease time for leases assigned by the DHCP server.
+     *
+     * <p>This parameter is required.
+     */
+    public DhcpServingParamsParcelExt setDhcpLeaseTimeSecs(long dhcpLeaseTimeSecs) {
+        this.dhcpLeaseTimeSecs = dhcpLeaseTimeSecs;
+        return this;
+    }
+
+    /**
+     * Set the link MTU to be advertised to DHCP clients.
+     *
+     * <p>If set to {@link #MTU_UNSET}, no MTU will be advertised to clients. This parameter
+     * is optional and defaults to {@link #MTU_UNSET}.
+     */
+    public DhcpServingParamsParcelExt setLinkMtu(int linkMtu) {
+        this.linkMtu = linkMtu;
+        return this;
+    }
+
+    /**
+     * Set whether the DHCP server should send the ANDROID_METERED vendor-specific option.
+     *
+     * <p>If not set, the default value is false.
+     */
+    public DhcpServingParamsParcelExt setMetered(boolean metered) {
+        this.metered = metered;
+        return this;
+    }
+
+    private static int[] toIntArray(@NonNull Collection<Inet4Address> addrs) {
+        int[] res = new int[addrs.size()];
+        int i = 0;
+        for (Inet4Address addr : addrs) {
+            res[i] = inet4AddressToIntHTH(addr);
+            i++;
+        }
+        return res;
+    }
+}
diff --git a/android/net/http/HttpResponseCache.java b/android/net/http/HttpResponseCache.java
new file mode 100644
index 0000000..7da76d1
--- /dev/null
+++ b/android/net/http/HttpResponseCache.java
@@ -0,0 +1,302 @@
+/*
+ * Copyright (C) 2011 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.http;
+
+import com.android.okhttp.internalandroidapi.AndroidResponseCacheAdapter;
+import com.android.okhttp.internalandroidapi.HasCacheHolder;
+
+import java.io.Closeable;
+import java.io.File;
+import java.io.IOException;
+import java.net.CacheRequest;
+import java.net.CacheResponse;
+import java.net.ResponseCache;
+import java.net.URI;
+import java.net.URLConnection;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Caches HTTP and HTTPS responses to the filesystem so they may be reused,
+ * saving time and bandwidth. This class supports {@link
+ * java.net.HttpURLConnection} and {@link javax.net.ssl.HttpsURLConnection};
+ * there is no platform-provided cache for {@code DefaultHttpClient} or
+ * {@code AndroidHttpClient}. Installation and instances are thread
+ * safe.
+ *
+ * <h3>Installing an HTTP response cache</h3>
+ * Enable caching of all of your application's HTTP requests by installing the
+ * cache at application startup. For example, this code installs a 10 MiB cache
+ * in the {@link android.content.Context#getCacheDir() application-specific
+ * cache directory} of the filesystem}: <pre>   {@code
+ *   protected void onCreate(Bundle savedInstanceState) {
+ *       ...
+ *
+ *       try {
+ *           File httpCacheDir = new File(context.getCacheDir(), "http");
+ *           long httpCacheSize = 10 * 1024 * 1024; // 10 MiB
+ *           HttpResponseCache.install(httpCacheDir, httpCacheSize);
+ *       } catch (IOException e) {
+ *           Log.i(TAG, "HTTP response cache installation failed:" + e);
+ *       }
+ *   }
+ *
+ *   protected void onStop() {
+ *       ...
+ *
+ *       HttpResponseCache cache = HttpResponseCache.getInstalled();
+ *       if (cache != null) {
+ *           cache.flush();
+ *       }
+ *   }}</pre>
+ * This cache will evict entries as necessary to keep its size from exceeding
+ * 10 MiB. The best cache size is application specific and depends on the size
+ * and frequency of the files being downloaded. Increasing the limit may improve
+ * the hit rate, but it may also just waste filesystem space!
+ *
+ * <p>For some applications it may be preferable to create the cache in the
+ * external storage directory. <strong>There are no access controls on the
+ * external storage directory so it should not be used for caches that could
+ * contain private data.</strong> Although it often has more free space,
+ * external storage is optional and&#8212;even if available&#8212;can disappear
+ * during use. Retrieve the external cache directory using {@link
+ * android.content.Context#getExternalCacheDir()}. If this method returns null,
+ * your application should fall back to either not caching or caching on
+ * non-external storage. If the external storage is removed during use, the
+ * cache hit rate will drop to zero and ongoing cache reads will fail.
+ *
+ * <p>Flushing the cache forces its data to the filesystem. This ensures that
+ * all responses written to the cache will be readable the next time the
+ * activity starts.
+ *
+ * <h3>Cache Optimization</h3>
+ * To measure cache effectiveness, this class tracks three statistics:
+ * <ul>
+ *     <li><strong>{@link #getRequestCount() Request Count:}</strong> the number
+ *         of HTTP requests issued since this cache was created.
+ *     <li><strong>{@link #getNetworkCount() Network Count:}</strong> the
+ *         number of those requests that required network use.
+ *     <li><strong>{@link #getHitCount() Hit Count:}</strong> the number of
+ *         those requests whose responses were served by the cache.
+ * </ul>
+ * Sometimes a request will result in a conditional cache hit. If the cache
+ * contains a stale copy of the response, the client will issue a conditional
+ * {@code GET}. The server will then send either the updated response if it has
+ * changed, or a short 'not modified' response if the client's copy is still
+ * valid. Such responses increment both the network count and hit count.
+ *
+ * <p>The best way to improve the cache hit rate is by configuring the web
+ * server to return cacheable responses. Although this client honors all <a
+ * href="http://www.ietf.org/rfc/rfc2616.txt">HTTP/1.1 (RFC 2068)</a> cache
+ * headers, it doesn't cache partial responses.
+ *
+ * <h3>Force a Network Response</h3>
+ * In some situations, such as after a user clicks a 'refresh' button, it may be
+ * necessary to skip the cache, and fetch data directly from the server. To force
+ * a full refresh, add the {@code no-cache} directive: <pre>   {@code
+ *         connection.addRequestProperty("Cache-Control", "no-cache");
+ * }</pre>
+ * If it is only necessary to force a cached response to be validated by the
+ * server, use the more efficient {@code max-age=0} instead: <pre>   {@code
+ *         connection.addRequestProperty("Cache-Control", "max-age=0");
+ * }</pre>
+ *
+ * <h3>Force a Cache Response</h3>
+ * Sometimes you'll want to show resources if they are available immediately,
+ * but not otherwise. This can be used so your application can show
+ * <i>something</i> while waiting for the latest data to be downloaded. To
+ * restrict a request to locally-cached resources, add the {@code
+ * only-if-cached} directive: <pre>   {@code
+ *     try {
+ *         connection.addRequestProperty("Cache-Control", "only-if-cached");
+ *         InputStream cached = connection.getInputStream();
+ *         // the resource was cached! show it
+ *     } catch (FileNotFoundException e) {
+ *         // the resource was not cached
+ *     }
+ * }</pre>
+ * This technique works even better in situations where a stale response is
+ * better than no response. To permit stale cached responses, use the {@code
+ * max-stale} directive with the maximum staleness in seconds: <pre>   {@code
+ *         int maxStale = 60 * 60 * 24 * 28; // tolerate 4-weeks stale
+ *         connection.addRequestProperty("Cache-Control", "max-stale=" + maxStale);
+ * }</pre>
+ *
+ * <h3>Working With Earlier Releases</h3>
+ * This class was added in Android 4.0 (Ice Cream Sandwich). Use reflection to
+ * enable the response cache without impacting earlier releases: <pre>   {@code
+ *       try {
+ *           File httpCacheDir = new File(context.getCacheDir(), "http");
+ *           long httpCacheSize = 10 * 1024 * 1024; // 10 MiB
+ *           Class.forName("android.net.http.HttpResponseCache")
+ *                   .getMethod("install", File.class, long.class)
+ *                   .invoke(null, httpCacheDir, httpCacheSize);
+ *       } catch (Exception httpResponseCacheNotAvailable) {
+ *       }}</pre>
+ */
+public final class HttpResponseCache extends ResponseCache implements HasCacheHolder, Closeable {
+
+    private final AndroidResponseCacheAdapter mDelegate;
+
+    private HttpResponseCache(AndroidResponseCacheAdapter delegate) {
+        mDelegate = delegate;
+    }
+
+    /**
+     * Returns the currently-installed {@code HttpResponseCache}, or null if
+     * there is no cache installed or it is not a {@code HttpResponseCache}.
+     */
+    public static HttpResponseCache getInstalled() {
+        ResponseCache installed = ResponseCache.getDefault();
+        if (installed instanceof HttpResponseCache) {
+            return (HttpResponseCache) installed;
+        }
+        return null;
+    }
+
+    /**
+     * Creates a new HTTP response cache and sets it as the system default cache.
+     *
+     * @param directory the directory to hold cache data.
+     * @param maxSize the maximum size of the cache in bytes.
+     * @return the newly-installed cache
+     * @throws IOException if {@code directory} cannot be used for this cache.
+     *     Most applications should respond to this exception by logging a
+     *     warning.
+     */
+    public static synchronized HttpResponseCache install(File directory, long maxSize)
+            throws IOException {
+        ResponseCache installed = ResponseCache.getDefault();
+        if (installed instanceof HttpResponseCache) {
+            HttpResponseCache installedResponseCache = (HttpResponseCache) installed;
+            CacheHolder cacheHolder = installedResponseCache.getCacheHolder();
+            // don't close and reopen if an equivalent cache is already installed
+            if (cacheHolder.isEquivalent(directory, maxSize)) {
+                return installedResponseCache;
+            } else {
+                // The HttpResponseCache that owns this object is about to be replaced.
+                installedResponseCache.close();
+            }
+        }
+
+        CacheHolder cacheHolder = CacheHolder.create(directory, maxSize);
+        AndroidResponseCacheAdapter androidResponseCacheAdapter =
+                new AndroidResponseCacheAdapter(cacheHolder);
+        HttpResponseCache responseCache = new HttpResponseCache(androidResponseCacheAdapter);
+        ResponseCache.setDefault(responseCache);
+        return responseCache;
+    }
+
+    @Override
+    public CacheResponse get(URI uri, String requestMethod,
+            Map<String, List<String>> requestHeaders) throws IOException {
+        return mDelegate.get(uri, requestMethod, requestHeaders);
+    }
+
+    @Override
+    public CacheRequest put(URI uri, URLConnection urlConnection) throws IOException {
+        return mDelegate.put(uri, urlConnection);
+    }
+
+    /**
+     * Returns the number of bytes currently being used to store the values in
+     * this cache. This may be greater than the {@link #maxSize} if a background
+     * deletion is pending. {@code -1} is returned if the size cannot be determined.
+     */
+    public long size() {
+        try {
+            return mDelegate.getSize();
+        } catch (IOException e) {
+            // This can occur if the cache failed to lazily initialize.
+            return -1;
+        }
+    }
+
+    /**
+     * Returns the maximum number of bytes that this cache should use to store
+     * its data.
+     */
+    public long maxSize() {
+        return mDelegate.getMaxSize();
+    }
+
+    /**
+     * Force buffered operations to the filesystem. This ensures that responses
+     * written to the cache will be available the next time the cache is opened,
+     * even if this process is killed.
+     */
+    public void flush() {
+        try {
+            mDelegate.flush();
+        } catch (IOException ignored) {
+        }
+    }
+
+    /**
+     * Returns the number of HTTP requests that required the network to either
+     * supply a response or validate a locally cached response.
+     */
+    public int getNetworkCount() {
+        return mDelegate.getNetworkCount();
+    }
+
+    /**
+     * Returns the number of HTTP requests whose response was provided by the
+     * cache. This may include conditional {@code GET} requests that were
+     * validated over the network.
+     */
+    public int getHitCount() {
+        return mDelegate.getHitCount();
+    }
+
+    /**
+     * Returns the total number of HTTP requests that were made. This includes
+     * both client requests and requests that were made on the client's behalf
+     * to handle a redirects and retries.
+     */
+    public int getRequestCount() {
+        return mDelegate.getRequestCount();
+    }
+
+    /**
+     * Uninstalls the cache and releases any active resources. Stored contents
+     * will remain on the filesystem.
+     */
+    @Override
+    public void close() throws IOException {
+        if (ResponseCache.getDefault() == this) {
+            ResponseCache.setDefault(null);
+        }
+        mDelegate.close();
+    }
+
+    /**
+     * Uninstalls the cache and deletes all of its stored contents.
+     */
+    public void delete() throws IOException {
+        if (ResponseCache.getDefault() == this) {
+            ResponseCache.setDefault(null);
+        }
+        mDelegate.delete();
+    }
+
+    /** @hide Needed for OkHttp integration. */
+    @Override
+    public CacheHolder getCacheHolder() {
+        return mDelegate.getCacheHolder();
+    }
+}
diff --git a/android/net/http/SslCertificate.java b/android/net/http/SslCertificate.java
new file mode 100644
index 0000000..01dd08f
--- /dev/null
+++ b/android/net/http/SslCertificate.java
@@ -0,0 +1,524 @@
+/*
+ * Copyright (C) 2006 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.http;
+
+import android.annotation.Nullable;
+import android.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.os.Bundle;
+import android.text.format.DateFormat;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.TextView;
+
+import com.android.internal.util.HexDump;
+import com.android.org.bouncycastle.asn1.x509.X509Name;
+
+import java.io.ByteArrayInputStream;
+import java.math.BigInteger;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Vector;
+
+/**
+ * SSL certificate info (certificate details) class
+ */
+public class SslCertificate {
+
+    /**
+     * SimpleDateFormat pattern for an ISO 8601 date
+     */
+    private static String ISO_8601_DATE_FORMAT = "yyyy-MM-dd HH:mm:ssZ";
+
+    /**
+     * Name of the entity this certificate is issued to
+     */
+    private final DName mIssuedTo;
+
+    /**
+     * Name of the entity this certificate is issued by
+     */
+    private final DName mIssuedBy;
+
+    /**
+     * Not-before date from the validity period
+     */
+    private final Date mValidNotBefore;
+
+    /**
+     * Not-after date from the validity period
+     */
+    private final Date mValidNotAfter;
+
+    /**
+     * The original source certificate, if available.
+     *
+     * TODO If deprecated constructors are removed, this should always
+     * be available, and saveState and restoreState can be simplified
+     * to be unconditional.
+     */
+    @UnsupportedAppUsage
+    private final X509Certificate mX509Certificate;
+
+    /**
+     * Bundle key names
+     */
+    private static final String ISSUED_TO = "issued-to";
+    private static final String ISSUED_BY = "issued-by";
+    private static final String VALID_NOT_BEFORE = "valid-not-before";
+    private static final String VALID_NOT_AFTER = "valid-not-after";
+    private static final String X509_CERTIFICATE = "x509-certificate";
+
+    /**
+     * Saves the certificate state to a bundle
+     * @param certificate The SSL certificate to store
+     * @return A bundle with the certificate stored in it or null if fails
+     */
+    public static Bundle saveState(SslCertificate certificate) {
+        if (certificate == null) {
+            return null;
+        }
+        Bundle bundle = new Bundle();
+        bundle.putString(ISSUED_TO, certificate.getIssuedTo().getDName());
+        bundle.putString(ISSUED_BY, certificate.getIssuedBy().getDName());
+        bundle.putString(VALID_NOT_BEFORE, certificate.getValidNotBefore());
+        bundle.putString(VALID_NOT_AFTER, certificate.getValidNotAfter());
+        X509Certificate x509Certificate = certificate.mX509Certificate;
+        if (x509Certificate != null) {
+            try {
+                bundle.putByteArray(X509_CERTIFICATE, x509Certificate.getEncoded());
+            } catch (CertificateEncodingException ignored) {
+            }
+        }
+        return bundle;
+    }
+
+    /**
+     * Restores the certificate stored in the bundle
+     * @param bundle The bundle with the certificate state stored in it
+     * @return The SSL certificate stored in the bundle or null if fails
+     */
+    public static SslCertificate restoreState(Bundle bundle) {
+        if (bundle == null) {
+            return null;
+        }
+        X509Certificate x509Certificate;
+        byte[] bytes = bundle.getByteArray(X509_CERTIFICATE);
+        if (bytes == null) {
+            x509Certificate = null;
+        } else {
+            try {
+                CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
+                Certificate cert = certFactory.generateCertificate(new ByteArrayInputStream(bytes));
+                x509Certificate = (X509Certificate) cert;
+            } catch (CertificateException e) {
+                x509Certificate = null;
+            }
+        }
+        return new SslCertificate(bundle.getString(ISSUED_TO),
+                                  bundle.getString(ISSUED_BY),
+                                  parseDate(bundle.getString(VALID_NOT_BEFORE)),
+                                  parseDate(bundle.getString(VALID_NOT_AFTER)),
+                                  x509Certificate);
+    }
+
+    /**
+     * Creates a new SSL certificate object
+     * @param issuedTo The entity this certificate is issued to
+     * @param issuedBy The entity that issued this certificate
+     * @param validNotBefore The not-before date from the certificate
+     *     validity period in ISO 8601 format
+     * @param validNotAfter The not-after date from the certificate
+     *     validity period in ISO 8601 format
+     * @deprecated Use {@link #SslCertificate(X509Certificate)}
+     */
+    @Deprecated
+    public SslCertificate(
+            String issuedTo, String issuedBy, String validNotBefore, String validNotAfter) {
+        this(issuedTo, issuedBy, parseDate(validNotBefore), parseDate(validNotAfter), null);
+    }
+
+    /**
+     * Creates a new SSL certificate object
+     * @param issuedTo The entity this certificate is issued to
+     * @param issuedBy The entity that issued this certificate
+     * @param validNotBefore The not-before date from the certificate validity period
+     * @param validNotAfter The not-after date from the certificate validity period
+     * @deprecated Use {@link #SslCertificate(X509Certificate)}
+     */
+    @Deprecated
+    public SslCertificate(
+            String issuedTo, String issuedBy, Date validNotBefore, Date validNotAfter) {
+        this(issuedTo, issuedBy, validNotBefore, validNotAfter, null);
+    }
+
+    /**
+     * Creates a new SSL certificate object from an X509 certificate
+     * @param certificate X509 certificate
+     */
+    public SslCertificate(X509Certificate certificate) {
+        this(certificate.getSubjectDN().getName(),
+             certificate.getIssuerDN().getName(),
+             certificate.getNotBefore(),
+             certificate.getNotAfter(),
+             certificate);
+    }
+
+    private SslCertificate(
+            String issuedTo, String issuedBy,
+            Date validNotBefore, Date validNotAfter,
+            X509Certificate x509Certificate) {
+        mIssuedTo = new DName(issuedTo);
+        mIssuedBy = new DName(issuedBy);
+        mValidNotBefore = cloneDate(validNotBefore);
+        mValidNotAfter  = cloneDate(validNotAfter);
+        mX509Certificate = x509Certificate;
+    }
+
+    /**
+     * @return Not-before date from the certificate validity period or
+     * "" if none has been set
+     */
+    public Date getValidNotBeforeDate() {
+        return cloneDate(mValidNotBefore);
+    }
+
+    /**
+     * @return Not-before date from the certificate validity period in
+     * ISO 8601 format or "" if none has been set
+     *
+     * @deprecated Use {@link #getValidNotBeforeDate()}
+     */
+    @Deprecated
+    public String getValidNotBefore() {
+        return formatDate(mValidNotBefore);
+    }
+
+    /**
+     * @return Not-after date from the certificate validity period or
+     * "" if none has been set
+     */
+    public Date getValidNotAfterDate() {
+        return cloneDate(mValidNotAfter);
+    }
+
+    /**
+     * @return Not-after date from the certificate validity period in
+     * ISO 8601 format or "" if none has been set
+     *
+     * @deprecated Use {@link #getValidNotAfterDate()}
+     */
+    @Deprecated
+    public String getValidNotAfter() {
+        return formatDate(mValidNotAfter);
+    }
+
+    /**
+     * @return Issued-to distinguished name or null if none has been set
+     */
+    public DName getIssuedTo() {
+        return mIssuedTo;
+    }
+
+    /**
+     * @return Issued-by distinguished name or null if none has been set
+     */
+    public DName getIssuedBy() {
+        return mIssuedBy;
+    }
+
+    /**
+     * @return The {@code X509Certificate} used to create this {@code SslCertificate} or
+     * {@code null} if no certificate was provided.
+     */
+    public @Nullable X509Certificate getX509Certificate() {
+        return mX509Certificate;
+    }
+
+    /**
+     * Convenience for UI presentation, not intended as public API.
+     */
+    @UnsupportedAppUsage
+    private static String getSerialNumber(X509Certificate x509Certificate) {
+        if (x509Certificate == null) {
+            return "";
+        }
+        BigInteger serialNumber = x509Certificate.getSerialNumber();
+        if (serialNumber == null) {
+            return "";
+        }
+        return fingerprint(serialNumber.toByteArray());
+    }
+
+    /**
+     * Convenience for UI presentation, not intended as public API.
+     */
+    @UnsupportedAppUsage
+    private static String getDigest(X509Certificate x509Certificate, String algorithm) {
+        if (x509Certificate == null) {
+            return "";
+        }
+        try {
+            byte[] bytes = x509Certificate.getEncoded();
+            MessageDigest md = MessageDigest.getInstance(algorithm);
+            byte[] digest = md.digest(bytes);
+            return fingerprint(digest);
+        } catch (CertificateEncodingException ignored) {
+            return "";
+        } catch (NoSuchAlgorithmException ignored) {
+            return "";
+        }
+    }
+
+    private static final String fingerprint(byte[] bytes) {
+        if (bytes == null) {
+            return "";
+        }
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < bytes.length; i++) {
+            byte b = bytes[i];
+            HexDump.appendByteAsHex(sb, b, true);
+            if (i+1 != bytes.length) {
+                sb.append(':');
+            }
+        }
+        return sb.toString();
+    }
+
+    /**
+     * @return A string representation of this certificate for debugging
+     */
+    public String toString() {
+        return ("Issued to: " + mIssuedTo.getDName() + ";\n"
+                + "Issued by: " + mIssuedBy.getDName() + ";\n");
+    }
+
+    /**
+     * Parse an ISO 8601 date converting ParseExceptions to a null result;
+     */
+    private static Date parseDate(String string) {
+        try {
+            return new SimpleDateFormat(ISO_8601_DATE_FORMAT).parse(string);
+        } catch (ParseException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Format a date as an ISO 8601 string, return "" for a null date
+     */
+    private static String formatDate(Date date) {
+        if (date == null) {
+            return "";
+        }
+        return new SimpleDateFormat(ISO_8601_DATE_FORMAT).format(date);
+    }
+
+    /**
+     * Clone a possibly null Date
+     */
+    private static Date cloneDate(Date date) {
+        if (date == null) {
+            return null;
+        }
+        return (Date) date.clone();
+    }
+
+    /**
+     * A distinguished name helper class: a 3-tuple of:
+     * <ul>
+     *   <li>the most specific common name (CN)</li>
+     *   <li>the most specific organization (O)</li>
+     *   <li>the most specific organizational unit (OU)</li>
+     * <ul>
+     */
+    public class DName {
+        /**
+         * Distinguished name (normally includes CN, O, and OU names)
+         */
+        private String mDName;
+
+        /**
+         * Common-name (CN) component of the name
+         */
+        private String mCName;
+
+        /**
+         * Organization (O) component of the name
+         */
+        private String mOName;
+
+        /**
+         * Organizational Unit (OU) component of the name
+         */
+        private String mUName;
+
+        /**
+         * Creates a new {@code DName} from a string. The attributes
+         * are assumed to come in most significant to least
+         * significant order which is true of human readable values
+         * returned by methods such as {@code X500Principal.getName()}.
+         * Be aware that the underlying sources of distinguished names
+         * such as instances of {@code X509Certificate} are encoded in
+         * least significant to most significant order, so make sure
+         * the value passed here has the expected ordering of
+         * attributes.
+         */
+        public DName(String dName) {
+            if (dName != null) {
+                mDName = dName;
+                try {
+                    X509Name x509Name = new X509Name(dName);
+
+                    Vector val = x509Name.getValues();
+                    Vector oid = x509Name.getOIDs();
+
+                    for (int i = 0; i < oid.size(); i++) {
+                        if (oid.elementAt(i).equals(X509Name.CN)) {
+                            if (mCName == null) {
+                                mCName = (String) val.elementAt(i);
+                            }
+                            continue;
+                        }
+
+                        if (oid.elementAt(i).equals(X509Name.O)) {
+                            if (mOName == null) {
+                                mOName = (String) val.elementAt(i);
+                                continue;
+                            }
+                        }
+
+                        if (oid.elementAt(i).equals(X509Name.OU)) {
+                            if (mUName == null) {
+                                mUName = (String) val.elementAt(i);
+                                continue;
+                            }
+                        }
+                    }
+                } catch (IllegalArgumentException ex) {
+                    // thrown if there is an error parsing the string
+                }
+            }
+        }
+
+        /**
+         * @return The distinguished name (normally includes CN, O, and OU names)
+         */
+        public String getDName() {
+            return mDName != null ? mDName : "";
+        }
+
+        /**
+         * @return The most specific Common-name (CN) component of this name
+         */
+        public String getCName() {
+            return mCName != null ? mCName : "";
+        }
+
+        /**
+         * @return The most specific Organization (O) component of this name
+         */
+        public String getOName() {
+            return mOName != null ? mOName : "";
+        }
+
+        /**
+         * @return The most specific Organizational Unit (OU) component of this name
+         */
+        public String getUName() {
+            return mUName != null ? mUName : "";
+        }
+    }
+
+    /**
+     * Inflates the SSL certificate view (helper method).
+     * @return The resultant certificate view with issued-to, issued-by,
+     * issued-on, expires-on, and possibly other fields set.
+     *
+     * @hide Used by Browser and Settings
+     */
+    @UnsupportedAppUsage
+    public View inflateCertificateView(Context context) {
+        LayoutInflater factory = LayoutInflater.from(context);
+
+        View certificateView = factory.inflate(
+            com.android.internal.R.layout.ssl_certificate, null);
+
+        // issued to:
+        SslCertificate.DName issuedTo = getIssuedTo();
+        if (issuedTo != null) {
+            ((TextView) certificateView.findViewById(com.android.internal.R.id.to_common))
+                    .setText(issuedTo.getCName());
+            ((TextView) certificateView.findViewById(com.android.internal.R.id.to_org))
+                    .setText(issuedTo.getOName());
+            ((TextView) certificateView.findViewById(com.android.internal.R.id.to_org_unit))
+                    .setText(issuedTo.getUName());
+        }
+        // serial number:
+        ((TextView) certificateView.findViewById(com.android.internal.R.id.serial_number))
+                .setText(getSerialNumber(mX509Certificate));
+
+        // issued by:
+        SslCertificate.DName issuedBy = getIssuedBy();
+        if (issuedBy != null) {
+            ((TextView) certificateView.findViewById(com.android.internal.R.id.by_common))
+                    .setText(issuedBy.getCName());
+            ((TextView) certificateView.findViewById(com.android.internal.R.id.by_org))
+                    .setText(issuedBy.getOName());
+            ((TextView) certificateView.findViewById(com.android.internal.R.id.by_org_unit))
+                    .setText(issuedBy.getUName());
+        }
+
+        // issued on:
+        String issuedOn = formatCertificateDate(context, getValidNotBeforeDate());
+        ((TextView) certificateView.findViewById(com.android.internal.R.id.issued_on))
+                .setText(issuedOn);
+
+        // expires on:
+        String expiresOn = formatCertificateDate(context, getValidNotAfterDate());
+        ((TextView) certificateView.findViewById(com.android.internal.R.id.expires_on))
+                .setText(expiresOn);
+
+        // fingerprints:
+        ((TextView) certificateView.findViewById(com.android.internal.R.id.sha256_fingerprint))
+                .setText(getDigest(mX509Certificate, "SHA256"));
+        ((TextView) certificateView.findViewById(com.android.internal.R.id.sha1_fingerprint))
+                .setText(getDigest(mX509Certificate, "SHA1"));
+
+        return certificateView;
+    }
+
+    /**
+     * Formats the certificate date to a properly localized date string.
+     * @return Properly localized version of the certificate date string and
+     * the "" if it fails to localize.
+     */
+    private String formatCertificateDate(Context context, Date certificateDate) {
+        if (certificateDate == null) {
+            return "";
+        }
+        return DateFormat.getMediumDateFormat(context).format(certificateDate);
+    }
+}
diff --git a/android/net/http/SslError.java b/android/net/http/SslError.java
new file mode 100644
index 0000000..b3f2fb7
--- /dev/null
+++ b/android/net/http/SslError.java
@@ -0,0 +1,233 @@
+/*
+ * Copyright (C) 2006 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.http;
+
+import android.annotation.UnsupportedAppUsage;
+import android.os.Build;
+import java.security.cert.X509Certificate;
+
+/**
+ * This class represents a set of one or more SSL errors and the associated SSL
+ * certificate.
+ */
+public class SslError {
+
+    /**
+     * Individual SSL errors (in the order from the least to the most severe):
+     */
+
+    /**
+     * The certificate is not yet valid
+     */
+    public static final int SSL_NOTYETVALID = 0;
+    /**
+     * The certificate has expired
+     */
+    public static final int SSL_EXPIRED = 1;
+    /**
+     * Hostname mismatch
+     */
+    public static final int SSL_IDMISMATCH = 2;
+    /**
+     * The certificate authority is not trusted
+     */
+    public static final int SSL_UNTRUSTED = 3;
+    /**
+     * The date of the certificate is invalid
+     */
+    public static final int SSL_DATE_INVALID = 4;
+    /**
+     * A generic error occurred
+     */
+    public static final int SSL_INVALID = 5;
+
+
+    /**
+     * The number of different SSL errors.
+     * @deprecated This constant is not necessary for using the SslError API and
+     *             can change from release to release.
+     */
+    // Update if you add a new SSL error!!!
+    @Deprecated
+    public static final int SSL_MAX_ERROR = 6;
+
+    /**
+     * The SSL error set bitfield (each individual error is a bit index;
+     * multiple individual errors can be OR-ed)
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    int mErrors;
+
+    /**
+     * The SSL certificate associated with the error set
+     */
+    @UnsupportedAppUsage
+    final SslCertificate mCertificate;
+
+    /**
+     * The URL associated with the error set.
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    final String mUrl;
+
+    /**
+     * Creates a new SslError object using the supplied error and certificate.
+     * The URL will be set to the empty string.
+     * @param error The SSL error
+     * @param certificate The associated SSL certificate
+     * @deprecated Use {@link #SslError(int, SslCertificate, String)}
+     */
+    @Deprecated
+    public SslError(int error, SslCertificate certificate) {
+        this(error, certificate, "");
+    }
+
+    /**
+     * Creates a new SslError object using the supplied error and certificate.
+     * The URL will be set to the empty string.
+     * @param error The SSL error
+     * @param certificate The associated SSL certificate
+     * @deprecated Use {@link #SslError(int, X509Certificate, String)}
+     */
+    @Deprecated
+    public SslError(int error, X509Certificate certificate) {
+        this(error, certificate, "");
+    }
+
+    /**
+     * Creates a new SslError object using the supplied error, certificate and
+     * URL.
+     * @param error The SSL error
+     * @param certificate The associated SSL certificate
+     * @param url The associated URL
+     */
+    public SslError(int error, SslCertificate certificate, String url) {
+        assert certificate != null;
+        assert url != null;
+        addError(error);
+        mCertificate = certificate;
+        mUrl = url;
+    }
+
+    /**
+     * Creates a new SslError object using the supplied error, certificate and
+     * URL.
+     * @param error The SSL error
+     * @param certificate The associated SSL certificate
+     * @param url The associated URL
+     */
+    public SslError(int error, X509Certificate certificate, String url) {
+        this(error, new SslCertificate(certificate), url);
+    }
+
+    /**
+     * Creates an SslError object from a chromium error code.
+     * @param error The chromium error code
+     * @param certificate The associated SSL certificate
+     * @param url The associated URL.
+     * @hide  chromium error codes only available inside the framework
+     */
+    public static SslError SslErrorFromChromiumErrorCode(
+            int error, SslCertificate cert, String url) {
+        // The chromium error codes are in:
+        // external/chromium/net/base/net_error_list.h
+        assert (error >= -299 && error <= -200);
+        if (error == -200)
+            return new SslError(SSL_IDMISMATCH, cert, url);
+        if (error == -201)
+            return new SslError(SSL_DATE_INVALID, cert, url);
+        if (error == -202)
+            return new SslError(SSL_UNTRUSTED, cert, url);
+        // Map all other codes to SSL_INVALID.
+        return new SslError(SSL_INVALID, cert, url);
+    }
+
+    /**
+     * Gets the SSL certificate associated with this object.
+     * @return The SSL certificate, non-null.
+     */
+    public SslCertificate getCertificate() {
+        return mCertificate;
+    }
+
+    /**
+     * Gets the URL associated with this object.
+     * @return The URL, non-null.
+     */
+    public String getUrl() {
+        return mUrl;
+    }
+
+    /**
+     * Adds the supplied SSL error to the set.
+     * @param error The SSL error to add
+     * @return True if the error being added is a known SSL error, otherwise
+     *         false.
+     */
+    public boolean addError(int error) {
+        boolean rval = (0 <= error && error < SslError.SSL_MAX_ERROR);
+        if (rval) {
+            mErrors |= (0x1 << error);
+        }
+
+        return rval;
+    }
+
+    /**
+     * Determines whether this object includes the supplied error.
+     * @param error The SSL error to check for
+     * @return True if this object includes the error, otherwise false.
+     */
+    public boolean hasError(int error) {
+        boolean rval = (0 <= error && error < SslError.SSL_MAX_ERROR);
+        if (rval) {
+            rval = ((mErrors & (0x1 << error)) != 0);
+        }
+
+        return rval;
+    }
+
+    /**
+     * Gets the most severe SSL error in this object's set of errors.
+     * Returns -1 if the set is empty.
+     * @return The most severe SSL error, or -1 if the set is empty.
+     */
+    public int getPrimaryError() {
+        if (mErrors != 0) {
+            // go from the most to the least severe errors
+            for (int error = SslError.SSL_MAX_ERROR - 1; error >= 0; --error) {
+                if ((mErrors & (0x1 << error)) != 0) {
+                    return error;
+                }
+            }
+            // mErrors should never be set to an invalid value.
+            assert false;
+        }
+
+        return -1;
+    }
+
+    /**
+     * Returns a string representation of this object.
+     * @return A String representation of this object.
+     */
+    public String toString() {
+        return "primary error: " + getPrimaryError() +
+                " certificate: " + getCertificate() +
+                " on URL: " + getUrl();
+    }
+}
diff --git a/android/net/http/X509TrustManagerExtensions.java b/android/net/http/X509TrustManagerExtensions.java
new file mode 100644
index 0000000..280dad0
--- /dev/null
+++ b/android/net/http/X509TrustManagerExtensions.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2012 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.http;
+
+import android.security.net.config.UserCertificateSource;
+
+import com.android.org.conscrypt.TrustManagerImpl;
+
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.List;
+
+import javax.net.ssl.X509TrustManager;
+
+/**
+ * X509TrustManager wrapper exposing Android-added features.
+ * <p>
+ * The checkServerTrusted method allows callers to perform additional
+ * verification of certificate chains after they have been successfully verified
+ * by the platform.
+ * </p>
+ */
+public class X509TrustManagerExtensions {
+
+    private final TrustManagerImpl mDelegate;
+    // Methods to use when mDelegate is not a TrustManagerImpl and duck typing is being used.
+    private final X509TrustManager mTrustManager;
+    private final Method mCheckServerTrusted;
+    private final Method mIsSameTrustConfiguration;
+
+    /**
+     * Constructs a new X509TrustManagerExtensions wrapper.
+     *
+     * @param tm A {@link X509TrustManager} as returned by TrustManagerFactory.getInstance();
+     * @throws IllegalArgumentException If tm is an unsupported TrustManager type.
+     */
+    public X509TrustManagerExtensions(X509TrustManager tm) throws IllegalArgumentException {
+        if (tm instanceof TrustManagerImpl) {
+            mDelegate = (TrustManagerImpl) tm;
+            mTrustManager = null;
+            mCheckServerTrusted = null;
+            mIsSameTrustConfiguration = null;
+            return;
+        }
+        // Use duck typing if possible.
+        mDelegate = null;
+        mTrustManager = tm;
+        // Check that the hostname aware checkServerTrusted is present.
+        try {
+            mCheckServerTrusted = tm.getClass().getMethod("checkServerTrusted",
+                    X509Certificate[].class,
+                    String.class,
+                    String.class);
+        } catch (NoSuchMethodException e) {
+            throw new IllegalArgumentException("Required method"
+                    + " checkServerTrusted(X509Certificate[], String, String, String) missing");
+        }
+        // Get the option isSameTrustConfiguration method.
+        Method isSameTrustConfiguration = null;
+        try {
+            isSameTrustConfiguration = tm.getClass().getMethod("isSameTrustConfiguration",
+                    String.class,
+                    String.class);
+        } catch (ReflectiveOperationException ignored) {
+        }
+        mIsSameTrustConfiguration = isSameTrustConfiguration;
+    }
+
+    /**
+     * Verifies the given certificate chain.
+     *
+     * <p>See {@link X509TrustManager#checkServerTrusted(X509Certificate[], String)} for a
+     * description of the chain and authType parameters. The final parameter, host, should be the
+     * hostname of the server.</p>
+     *
+     * @throws CertificateException if the chain does not verify correctly.
+     * @return the properly ordered chain used for verification as a list of X509Certificates.
+     */
+    public List<X509Certificate> checkServerTrusted(X509Certificate[] chain, String authType,
+                                                    String host) throws CertificateException {
+        if (mDelegate != null) {
+            return mDelegate.checkServerTrusted(chain, authType, host);
+        } else {
+            try {
+                return (List<X509Certificate>) mCheckServerTrusted.invoke(mTrustManager, chain,
+                        authType, host);
+            } catch (IllegalAccessException e) {
+                throw new CertificateException("Failed to call checkServerTrusted", e);
+            } catch (InvocationTargetException e) {
+                if (e.getCause() instanceof CertificateException) {
+                    throw (CertificateException) e.getCause();
+                }
+                if (e.getCause() instanceof RuntimeException) {
+                    throw (RuntimeException) e.getCause();
+                }
+                throw new CertificateException("checkServerTrusted failed", e.getCause());
+            }
+        }
+    }
+
+    /**
+     * Checks whether a CA certificate is added by an user.
+     *
+     * <p>Since {@link X509TrustManager#checkServerTrusted} may allow its parameter {@code chain} to
+     * chain up to user-added CA certificates, this method can be used to perform additional
+     * policies for user-added CA certificates.
+     *
+     * @return {@code true} to indicate that the certificate authority exists in the user added
+     * certificate store, {@code false} otherwise.
+     */
+    public boolean isUserAddedCertificate(X509Certificate cert) {
+        return UserCertificateSource.getInstance().findBySubjectAndPublicKey(cert) != null;
+    }
+
+    /**
+     * Returns {@code true} if the TrustManager uses the same trust configuration for the provided
+     * hostnames.
+     */
+    public boolean isSameTrustConfiguration(String hostname1, String hostname2) {
+        if (mIsSameTrustConfiguration == null) {
+            return true;
+        }
+        try {
+            return (Boolean) mIsSameTrustConfiguration.invoke(mTrustManager, hostname1, hostname2);
+        } catch (IllegalAccessException e) {
+            throw new RuntimeException("Failed to call isSameTrustConfiguration", e);
+        } catch (InvocationTargetException e) {
+            if (e.getCause() instanceof RuntimeException) {
+                throw (RuntimeException) e.getCause();
+            } else {
+                throw new RuntimeException("isSameTrustConfiguration failed", e.getCause());
+            }
+        }
+    }
+}
diff --git a/android/net/ip/InterfaceController.java b/android/net/ip/InterfaceController.java
new file mode 100644
index 0000000..970bc9c
--- /dev/null
+++ b/android/net/ip/InterfaceController.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.ip;
+
+import android.net.INetd;
+import android.net.InterfaceConfigurationParcel;
+import android.net.LinkAddress;
+import android.net.util.SharedLog;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.system.OsConstants;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+
+
+/**
+ * Encapsulates the multiple IP configuration operations performed on an interface.
+ *
+ * TODO: refactor/eliminate the redundant ways to set and clear addresses.
+ *
+ * @hide
+ */
+public class InterfaceController {
+    private final static boolean DBG = false;
+
+    private final String mIfName;
+    private final INetd mNetd;
+    private final SharedLog mLog;
+
+    public InterfaceController(String ifname, INetd netd, SharedLog log) {
+        mIfName = ifname;
+        mNetd = netd;
+        mLog = log;
+    }
+
+    private boolean setInterfaceAddress(LinkAddress addr) {
+        final InterfaceConfigurationParcel ifConfig = new InterfaceConfigurationParcel();
+        ifConfig.ifName = mIfName;
+        ifConfig.ipv4Addr = addr.getAddress().getHostAddress();
+        ifConfig.prefixLength = addr.getPrefixLength();
+        ifConfig.hwAddr = "";
+        ifConfig.flags = new String[0];
+        try {
+            mNetd.interfaceSetCfg(ifConfig);
+        } catch (RemoteException | ServiceSpecificException e) {
+            logError("Setting IPv4 address to %s/%d failed: %s",
+                    ifConfig.ipv4Addr, ifConfig.prefixLength, e);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Set the IPv4 address of the interface.
+     */
+    public boolean setIPv4Address(LinkAddress address) {
+        if (!(address.getAddress() instanceof Inet4Address)) {
+            return false;
+        }
+        return setInterfaceAddress(address);
+    }
+
+    /**
+     * Clear the IPv4Address of the interface.
+     */
+    public boolean clearIPv4Address() {
+        return setInterfaceAddress(new LinkAddress("0.0.0.0/0"));
+    }
+
+    private boolean setEnableIPv6(boolean enabled) {
+        try {
+            mNetd.interfaceSetEnableIPv6(mIfName, enabled);
+        } catch (RemoteException | ServiceSpecificException e) {
+            logError("%s IPv6 failed: %s", (enabled ? "enabling" : "disabling"), e);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Enable IPv6 on the interface.
+     */
+    public boolean enableIPv6() {
+        return setEnableIPv6(true);
+    }
+
+    /**
+     * Disable IPv6 on the interface.
+     */
+    public boolean disableIPv6() {
+        return setEnableIPv6(false);
+    }
+
+    /**
+     * Enable or disable IPv6 privacy extensions on the interface.
+     * @param enabled Whether the extensions should be enabled.
+     */
+    public boolean setIPv6PrivacyExtensions(boolean enabled) {
+        try {
+            mNetd.interfaceSetIPv6PrivacyExtensions(mIfName, enabled);
+        } catch (RemoteException | ServiceSpecificException e) {
+            logError("error %s IPv6 privacy extensions: %s",
+                    (enabled ? "enabling" : "disabling"), e);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Set IPv6 address generation mode on the interface.
+     *
+     * <p>IPv6 should be disabled before changing the mode.
+     */
+    public boolean setIPv6AddrGenModeIfSupported(int mode) {
+        try {
+            mNetd.setIPv6AddrGenMode(mIfName, mode);
+        } catch (RemoteException e) {
+            logError("Unable to set IPv6 addrgen mode: %s", e);
+            return false;
+        } catch (ServiceSpecificException e) {
+            if (e.errorCode != OsConstants.EOPNOTSUPP) {
+                logError("Unable to set IPv6 addrgen mode: %s", e);
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Add an address to the interface.
+     */
+    public boolean addAddress(LinkAddress addr) {
+        return addAddress(addr.getAddress(), addr.getPrefixLength());
+    }
+
+    /**
+     * Add an address to the interface.
+     */
+    public boolean addAddress(InetAddress ip, int prefixLen) {
+        try {
+            mNetd.interfaceAddAddress(mIfName, ip.getHostAddress(), prefixLen);
+        } catch (ServiceSpecificException | RemoteException e) {
+            logError("failed to add %s/%d: %s", ip, prefixLen, e);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Remove an address from the interface.
+     */
+    public boolean removeAddress(InetAddress ip, int prefixLen) {
+        try {
+            mNetd.interfaceDelAddress(mIfName, ip.getHostAddress(), prefixLen);
+        } catch (ServiceSpecificException | RemoteException e) {
+            logError("failed to remove %s/%d: %s", ip, prefixLen, e);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Remove all addresses from the interface.
+     */
+    public boolean clearAllAddresses() {
+        try {
+            mNetd.interfaceClearAddrs(mIfName);
+        } catch (Exception e) {
+            logError("Failed to clear addresses: %s", e);
+            return false;
+        }
+        return true;
+    }
+
+    private void logError(String fmt, Object... args) {
+        mLog.e(String.format(fmt, args));
+    }
+}
diff --git a/android/net/ip/IpClientCallbacks.java b/android/net/ip/IpClientCallbacks.java
new file mode 100644
index 0000000..db01ae4
--- /dev/null
+++ b/android/net/ip/IpClientCallbacks.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2019 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.ip;
+
+import android.net.DhcpResults;
+import android.net.LinkProperties;
+
+/**
+ * Callbacks for handling IpClient events.
+ *
+ * This is a convenience class to allow clients not to override all methods of IIpClientCallbacks,
+ * and avoid unparceling arguments.
+ * These methods are called asynchronously on a Binder thread, as IpClient lives in a different
+ * process.
+ * @hide
+ */
+public class IpClientCallbacks {
+
+    /**
+     * Callback called upon IpClient creation.
+     *
+     * @param ipClient The Binder token to communicate with IpClient.
+     */
+    public void onIpClientCreated(IIpClient ipClient) {}
+
+    /**
+     * Callback called prior to DHCP discovery/renewal.
+     *
+     * <p>In order to receive onPreDhcpAction(), call #withPreDhcpAction() when constructing a
+     * ProvisioningConfiguration.
+     *
+     * <p>Implementations of onPreDhcpAction() must call IpClient#completedPreDhcpAction() to
+     * indicate that DHCP is clear to proceed.
+      */
+    public void onPreDhcpAction() {}
+
+    /**
+     * Callback called after DHCP discovery/renewal.
+     */
+    public void onPostDhcpAction() {}
+
+    /**
+     * Callback called when new DHCP results are available.
+     *
+     * <p>This is purely advisory and not an indication of provisioning success or failure.  This is
+     * only here for callers that want to expose DHCPv4 results to other APIs
+     * (e.g., WifiInfo#setInetAddress).
+     *
+     * <p>DHCPv4 or static IPv4 configuration failure or success can be determined by whether or not
+     * the passed-in DhcpResults object is null.
+     */
+    public void onNewDhcpResults(DhcpResults dhcpResults) {}
+
+    /**
+     * Indicates that provisioning was successful.
+     */
+    public void onProvisioningSuccess(LinkProperties newLp) {}
+
+    /**
+     * Indicates that provisioning failed.
+     */
+    public void onProvisioningFailure(LinkProperties newLp) {}
+
+    /**
+     * Invoked on LinkProperties changes.
+     */
+    public void onLinkPropertiesChange(LinkProperties newLp) {}
+
+    /**Called when the internal IpReachabilityMonitor (if enabled) has
+     * detected the loss of a critical number of required neighbors.
+     */
+    public void onReachabilityLost(String logMsg) {}
+
+    /**
+     * Called when the IpClient state machine terminates.
+     */
+    public void onQuit() {}
+
+    /**
+     * Called to indicate that a new APF program must be installed to filter incoming packets.
+     */
+    public void installPacketFilter(byte[] filter) {}
+
+    /**
+     * Called to indicate that the APF Program & data buffer must be read asynchronously from the
+     * wifi driver.
+     *
+     * <p>Due to Wifi HAL limitations, the current implementation only supports dumping the entire
+     * buffer. In response to this request, the driver returns the data buffer asynchronously
+     * by sending an IpClient#EVENT_READ_PACKET_FILTER_COMPLETE message.
+     */
+    public void startReadPacketFilter() {}
+
+    /**
+     * If multicast filtering cannot be accomplished with APF, this function will be called to
+     * actuate multicast filtering using another means.
+     */
+    public void setFallbackMulticastFilter(boolean enabled) {}
+
+    /**
+     * Enabled/disable Neighbor Discover offload functionality. This is called, for example,
+     * whenever 464xlat is being started or stopped.
+     */
+    public void setNeighborDiscoveryOffload(boolean enable) {}
+}
diff --git a/android/net/ip/IpClientManager.java b/android/net/ip/IpClientManager.java
new file mode 100644
index 0000000..1e653cd
--- /dev/null
+++ b/android/net/ip/IpClientManager.java
@@ -0,0 +1,275 @@
+/*
+ * Copyright (C) 2019 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.ip;
+
+import android.annotation.NonNull;
+import android.net.NattKeepalivePacketData;
+import android.net.ProxyInfo;
+import android.net.TcpKeepalivePacketData;
+import android.net.shared.ProvisioningConfiguration;
+import android.net.util.KeepalivePacketDataUtil;
+import android.os.Binder;
+import android.os.RemoteException;
+import android.util.Log;
+
+/**
+ * A convenience wrapper for IpClient.
+ *
+ * Wraps IIpClient calls, making them a bit more friendly to use. Currently handles:
+ * - Clearing calling identity
+ * - Ignoring RemoteExceptions
+ * - Converting to stable parcelables
+ *
+ * By design, all methods on IIpClient are asynchronous oneway IPCs and are thus void. All the
+ * wrapper methods in this class return a boolean that callers can use to determine whether
+ * RemoteException was thrown.
+ */
+public class IpClientManager {
+    @NonNull private final IIpClient mIpClient;
+    @NonNull private final String mTag;
+
+    public IpClientManager(@NonNull IIpClient ipClient, @NonNull String tag) {
+        mIpClient = ipClient;
+        mTag = tag;
+    }
+
+    public IpClientManager(@NonNull IIpClient ipClient) {
+        this(ipClient, IpClientManager.class.getSimpleName());
+    }
+
+    private void log(String s, Throwable e) {
+        Log.e(mTag, s, e);
+    }
+
+    /**
+     * For clients using {@link ProvisioningConfiguration.Builder#withPreDhcpAction()}, must be
+     * called after {@link IIpClientCallbacks#onPreDhcpAction} to indicate that DHCP is clear to
+     * proceed.
+     */
+    public boolean completedPreDhcpAction() {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mIpClient.completedPreDhcpAction();
+            return true;
+        } catch (RemoteException e) {
+            log("Error completing PreDhcpAction", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /**
+     * Confirm the provisioning configuration.
+     */
+    public boolean confirmConfiguration() {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mIpClient.confirmConfiguration();
+            return true;
+        } catch (RemoteException e) {
+            log("Error confirming IpClient configuration", e);
+            return false;
+        }
+    }
+
+    /**
+     * Indicate that packet filter read is complete.
+     */
+    public boolean readPacketFilterComplete(byte[] data) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mIpClient.readPacketFilterComplete(data);
+            return true;
+        } catch (RemoteException e) {
+            log("Error notifying IpClient of packet filter read", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /**
+     * Shut down this IpClient instance altogether.
+     */
+    public boolean shutdown() {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mIpClient.shutdown();
+            return true;
+        } catch (RemoteException e) {
+            log("Error shutting down IpClient", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /**
+     * Start provisioning with the provided parameters.
+     */
+    public boolean startProvisioning(ProvisioningConfiguration prov) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mIpClient.startProvisioning(prov.toStableParcelable());
+            return true;
+        } catch (RemoteException e) {
+            log("Error starting IpClient provisioning", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /**
+     * Stop this IpClient.
+     *
+     * <p>This does not shut down the StateMachine itself, which is handled by {@link #shutdown()}.
+     */
+    public boolean stop() {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mIpClient.stop();
+            return true;
+        } catch (RemoteException e) {
+            log("Error stopping IpClient", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /**
+     * Set the TCP buffer sizes to use.
+     *
+     * This may be called, repeatedly, at any time before or after a call to
+     * #startProvisioning(). The setting is cleared upon calling #stop().
+     */
+    public boolean setTcpBufferSizes(String tcpBufferSizes) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mIpClient.setTcpBufferSizes(tcpBufferSizes);
+            return true;
+        } catch (RemoteException e) {
+            log("Error setting IpClient TCP buffer sizes", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /**
+     * Set the HTTP Proxy configuration to use.
+     *
+     * This may be called, repeatedly, at any time before or after a call to
+     * #startProvisioning(). The setting is cleared upon calling #stop().
+     */
+    public boolean setHttpProxy(ProxyInfo proxyInfo) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mIpClient.setHttpProxy(proxyInfo);
+            return true;
+        } catch (RemoteException e) {
+            log("Error setting IpClient proxy", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /**
+     * Enable or disable the multicast filter.  Attempts to use APF to accomplish the filtering,
+     * if not, Callback.setFallbackMulticastFilter() is called.
+     */
+    public boolean setMulticastFilter(boolean enabled) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mIpClient.setMulticastFilter(enabled);
+            return true;
+        } catch (RemoteException e) {
+            log("Error setting multicast filter", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /**
+     * Add a TCP keepalive packet filter before setting up keepalive offload.
+     */
+    public boolean addKeepalivePacketFilter(int slot, TcpKeepalivePacketData pkt) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mIpClient.addKeepalivePacketFilter(slot, pkt.toStableParcelable());
+            return true;
+        } catch (RemoteException e) {
+            log("Error adding Keepalive Packet Filter ", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /**
+     * Add a NAT-T keepalive packet filter before setting up keepalive offload.
+     */
+    public boolean addKeepalivePacketFilter(int slot, NattKeepalivePacketData pkt) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mIpClient.addNattKeepalivePacketFilter(
+                    slot, KeepalivePacketDataUtil.toStableParcelable(pkt));
+            return true;
+        } catch (RemoteException e) {
+            log("Error adding Keepalive Packet Filter ", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /**
+     * Remove a keepalive packet filter after stopping keepalive offload.
+     */
+    public boolean removeKeepalivePacketFilter(int slot) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mIpClient.removeKeepalivePacketFilter(slot);
+            return true;
+        } catch (RemoteException e) {
+            log("Error removing Keepalive Packet Filter ", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /**
+     * Set the L2 key and group hint for storing info into the memory store.
+     */
+    public boolean setL2KeyAndGroupHint(String l2Key, String groupHint) {
+        final long token = Binder.clearCallingIdentity();
+        try {
+            mIpClient.setL2KeyAndGroupHint(l2Key, groupHint);
+            return true;
+        } catch (RemoteException e) {
+            log("Failed setL2KeyAndGroupHint", e);
+            return false;
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+}
diff --git a/android/net/ip/IpClientUtil.java b/android/net/ip/IpClientUtil.java
new file mode 100644
index 0000000..714ade1
--- /dev/null
+++ b/android/net/ip/IpClientUtil.java
@@ -0,0 +1,193 @@
+/*
+ * Copyright (C) 2019 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.ip;
+
+import static android.net.shared.IpConfigurationParcelableUtil.fromStableParcelable;
+
+import android.content.Context;
+import android.net.DhcpResultsParcelable;
+import android.net.LinkProperties;
+import android.net.NetworkStackClient;
+import android.os.ConditionVariable;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+
+/**
+ * Utilities and wrappers to simplify communication with IpClient, which lives in the NetworkStack
+ * process.
+ *
+ * @hide
+ */
+public class IpClientUtil {
+    // TODO: remove with its callers
+    public static final String DUMP_ARG = "ipclient";
+
+    /**
+     * Subclass of {@link IpClientCallbacks} allowing clients to block until provisioning is
+     * complete with {@link WaitForProvisioningCallbacks#waitForProvisioning()}.
+     */
+    public static class WaitForProvisioningCallbacks extends IpClientCallbacks {
+        private final ConditionVariable mCV = new ConditionVariable();
+        private LinkProperties mCallbackLinkProperties;
+
+        /**
+         * Block until either {@link #onProvisioningSuccess(LinkProperties)} or
+         * {@link #onProvisioningFailure(LinkProperties)} is called.
+         */
+        public LinkProperties waitForProvisioning() {
+            mCV.block();
+            return mCallbackLinkProperties;
+        }
+
+        @Override
+        public void onProvisioningSuccess(LinkProperties newLp) {
+            mCallbackLinkProperties = newLp;
+            mCV.open();
+        }
+
+        @Override
+        public void onProvisioningFailure(LinkProperties newLp) {
+            mCallbackLinkProperties = null;
+            mCV.open();
+        }
+    }
+
+    /**
+     * Create a new IpClient.
+     *
+     * <p>This is a convenience method to allow clients to use {@link IpClientCallbacks} instead of
+     * {@link IIpClientCallbacks}.
+     * @see {@link NetworkStackClient#makeIpClient(String, IIpClientCallbacks)}
+     */
+    public static void makeIpClient(Context context, String ifName, IpClientCallbacks callback) {
+        // TODO: migrate clients and remove context argument
+        NetworkStackClient.getInstance().makeIpClient(ifName, new IpClientCallbacksProxy(callback));
+    }
+
+    /**
+     * Wrapper to relay calls from {@link IIpClientCallbacks} to {@link IpClientCallbacks}.
+     */
+    private static class IpClientCallbacksProxy extends IIpClientCallbacks.Stub {
+        protected final IpClientCallbacks mCb;
+
+        /**
+         * Create a new IpClientCallbacksProxy.
+         */
+        public IpClientCallbacksProxy(IpClientCallbacks cb) {
+            mCb = cb;
+        }
+
+        @Override
+        public void onIpClientCreated(IIpClient ipClient) {
+            mCb.onIpClientCreated(ipClient);
+        }
+
+        @Override
+        public void onPreDhcpAction() {
+            mCb.onPreDhcpAction();
+        }
+
+        @Override
+        public void onPostDhcpAction() {
+            mCb.onPostDhcpAction();
+        }
+
+        // This is purely advisory and not an indication of provisioning
+        // success or failure.  This is only here for callers that want to
+        // expose DHCPv4 results to other APIs (e.g., WifiInfo#setInetAddress).
+        // DHCPv4 or static IPv4 configuration failure or success can be
+        // determined by whether or not the passed-in DhcpResults object is
+        // null or not.
+        @Override
+        public void onNewDhcpResults(DhcpResultsParcelable dhcpResults) {
+            mCb.onNewDhcpResults(fromStableParcelable(dhcpResults));
+        }
+
+        @Override
+        public void onProvisioningSuccess(LinkProperties newLp) {
+            mCb.onProvisioningSuccess(newLp);
+        }
+        @Override
+        public void onProvisioningFailure(LinkProperties newLp) {
+            mCb.onProvisioningFailure(newLp);
+        }
+
+        // Invoked on LinkProperties changes.
+        @Override
+        public void onLinkPropertiesChange(LinkProperties newLp) {
+            mCb.onLinkPropertiesChange(newLp);
+        }
+
+        // Called when the internal IpReachabilityMonitor (if enabled) has
+        // detected the loss of a critical number of required neighbors.
+        @Override
+        public void onReachabilityLost(String logMsg) {
+            mCb.onReachabilityLost(logMsg);
+        }
+
+        // Called when the IpClient state machine terminates.
+        @Override
+        public void onQuit() {
+            mCb.onQuit();
+        }
+
+        // Install an APF program to filter incoming packets.
+        @Override
+        public void installPacketFilter(byte[] filter) {
+            mCb.installPacketFilter(filter);
+        }
+
+        // Asynchronously read back the APF program & data buffer from the wifi driver.
+        // Due to Wifi HAL limitations, the current implementation only supports dumping the entire
+        // buffer. In response to this request, the driver returns the data buffer asynchronously
+        // by sending an IpClient#EVENT_READ_PACKET_FILTER_COMPLETE message.
+        @Override
+        public void startReadPacketFilter() {
+            mCb.startReadPacketFilter();
+        }
+
+        // If multicast filtering cannot be accomplished with APF, this function will be called to
+        // actuate multicast filtering using another means.
+        @Override
+        public void setFallbackMulticastFilter(boolean enabled) {
+            mCb.setFallbackMulticastFilter(enabled);
+        }
+
+        // Enabled/disable Neighbor Discover offload functionality. This is
+        // called, for example, whenever 464xlat is being started or stopped.
+        @Override
+        public void setNeighborDiscoveryOffload(boolean enable) {
+            mCb.setNeighborDiscoveryOffload(enable);
+        }
+
+        @Override
+        public int getInterfaceVersion() {
+            return this.VERSION;
+        }
+    }
+
+    /**
+     * Dump logs for the specified IpClient.
+     * TODO: remove callers and delete
+     */
+    public static void dumpIpClient(
+            IIpClient connector, FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("IpClient logs have moved to dumpsys network_stack");
+    }
+}
diff --git a/android/net/ip/IpServer.java b/android/net/ip/IpServer.java
new file mode 100644
index 0000000..6a6a130
--- /dev/null
+++ b/android/net/ip/IpServer.java
@@ -0,0 +1,1003 @@
+/*
+ * 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.ip;
+
+import static android.net.NetworkUtils.numericToInetAddress;
+import static android.net.dhcp.IDhcpServer.STATUS_SUCCESS;
+import static android.net.util.NetworkConstants.FF;
+import static android.net.util.NetworkConstants.RFC7421_PREFIX_LENGTH;
+import static android.net.util.NetworkConstants.asByte;
+
+import android.net.ConnectivityManager;
+import android.net.INetd;
+import android.net.INetworkStackStatusCallback;
+import android.net.INetworkStatsService;
+import android.net.InterfaceConfiguration;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+import android.net.NetworkStackClient;
+import android.net.RouteInfo;
+import android.net.dhcp.DhcpServerCallbacks;
+import android.net.dhcp.DhcpServingParamsParcel;
+import android.net.dhcp.DhcpServingParamsParcelExt;
+import android.net.dhcp.IDhcpServer;
+import android.net.ip.RouterAdvertisementDaemon.RaParams;
+import android.net.util.InterfaceParams;
+import android.net.util.InterfaceSet;
+import android.net.util.NetdService;
+import android.net.util.SharedLog;
+import android.os.INetworkManagementService;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.util.Log;
+import android.util.Slog;
+import android.util.SparseArray;
+
+import com.android.internal.util.MessageUtils;
+import com.android.internal.util.Protocol;
+import com.android.internal.util.State;
+import com.android.internal.util.StateMachine;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Random;
+import java.util.Set;
+
+/**
+ * Provides the interface to IP-layer serving functionality for a given network
+ * interface, e.g. for tethering or "local-only hotspot" mode.
+ *
+ * @hide
+ */
+public class IpServer extends StateMachine {
+    public static final int STATE_UNAVAILABLE = 0;
+    public static final int STATE_AVAILABLE   = 1;
+    public static final int STATE_TETHERED    = 2;
+    public static final int STATE_LOCAL_ONLY  = 3;
+
+    public static String getStateString(int state) {
+        switch (state) {
+            case STATE_UNAVAILABLE: return "UNAVAILABLE";
+            case STATE_AVAILABLE:   return "AVAILABLE";
+            case STATE_TETHERED:    return "TETHERED";
+            case STATE_LOCAL_ONLY:  return "LOCAL_ONLY";
+        }
+        return "UNKNOWN: " + state;
+    }
+
+    private static final byte DOUG_ADAMS = (byte) 42;
+
+    private static final String USB_NEAR_IFACE_ADDR = "192.168.42.129";
+    private static final int USB_PREFIX_LENGTH = 24;
+    private static final String WIFI_HOST_IFACE_ADDR = "192.168.43.1";
+    private static final int WIFI_HOST_IFACE_PREFIX_LENGTH = 24;
+
+    // TODO: have PanService use some visible version of this constant
+    private static final String BLUETOOTH_IFACE_ADDR = "192.168.44.1";
+    private static final int BLUETOOTH_DHCP_PREFIX_LENGTH = 24;
+
+    // TODO: have this configurable
+    private static final int DHCP_LEASE_TIME_SECS = 3600;
+
+    private final static String TAG = "IpServer";
+    private final static boolean DBG = false;
+    private final static boolean VDBG = false;
+    private static final Class[] messageClasses = {
+            IpServer.class
+    };
+    private static final SparseArray<String> sMagicDecoderRing =
+            MessageUtils.findMessageNames(messageClasses);
+
+    public static class Callback {
+        /**
+         * Notify that |who| has changed its tethering state.
+         *
+         * @param who the calling instance of IpServer
+         * @param state one of STATE_*
+         * @param lastError one of ConnectivityManager.TETHER_ERROR_*
+         */
+        public void updateInterfaceState(IpServer who, int state, int lastError) {}
+
+        /**
+         * Notify that |who| has new LinkProperties.
+         *
+         * @param who the calling instance of IpServer
+         * @param newLp the new LinkProperties to report
+         */
+        public void updateLinkProperties(IpServer who, LinkProperties newLp) {}
+    }
+
+    public static class Dependencies {
+        public RouterAdvertisementDaemon getRouterAdvertisementDaemon(InterfaceParams ifParams) {
+            return new RouterAdvertisementDaemon(ifParams);
+        }
+
+        public InterfaceParams getInterfaceParams(String ifName) {
+            return InterfaceParams.getByName(ifName);
+        }
+
+        public INetd getNetdService() {
+            return NetdService.getInstance();
+        }
+
+        /**
+         * Create a DhcpServer instance to be used by IpServer.
+         */
+        public void makeDhcpServer(String ifName, DhcpServingParamsParcel params,
+                DhcpServerCallbacks cb) {
+            NetworkStackClient.getInstance().makeDhcpServer(ifName, params, cb);
+        }
+    }
+
+    private static final int BASE_IFACE              = Protocol.BASE_TETHERING + 100;
+    // request from the user that it wants to tether
+    public static final int CMD_TETHER_REQUESTED            = BASE_IFACE + 2;
+    // request from the user that it wants to untether
+    public static final int CMD_TETHER_UNREQUESTED          = BASE_IFACE + 3;
+    // notification that this interface is down
+    public static final int CMD_INTERFACE_DOWN              = BASE_IFACE + 4;
+    // notification from the master SM that it had trouble enabling IP Forwarding
+    public static final int CMD_IP_FORWARDING_ENABLE_ERROR  = BASE_IFACE + 7;
+    // notification from the master SM that it had trouble disabling IP Forwarding
+    public static final int CMD_IP_FORWARDING_DISABLE_ERROR = BASE_IFACE + 8;
+    // notification from the master SM that it had trouble starting tethering
+    public static final int CMD_START_TETHERING_ERROR       = BASE_IFACE + 9;
+    // notification from the master SM that it had trouble stopping tethering
+    public static final int CMD_STOP_TETHERING_ERROR        = BASE_IFACE + 10;
+    // notification from the master SM that it had trouble setting the DNS forwarders
+    public static final int CMD_SET_DNS_FORWARDERS_ERROR    = BASE_IFACE + 11;
+    // the upstream connection has changed
+    public static final int CMD_TETHER_CONNECTION_CHANGED   = BASE_IFACE + 12;
+    // new IPv6 tethering parameters need to be processed
+    public static final int CMD_IPV6_TETHER_UPDATE          = BASE_IFACE + 13;
+
+    private final State mInitialState;
+    private final State mLocalHotspotState;
+    private final State mTetheredState;
+    private final State mUnavailableState;
+
+    private final SharedLog mLog;
+    private final INetworkManagementService mNMService;
+    private final INetd mNetd;
+    private final INetworkStatsService mStatsService;
+    private final Callback mCallback;
+    private final InterfaceController mInterfaceCtrl;
+
+    private final String mIfaceName;
+    private final int mInterfaceType;
+    private final LinkProperties mLinkProperties;
+    private final boolean mUsingLegacyDhcp;
+
+    private final Dependencies mDeps;
+
+    private int mLastError;
+    private int mServingMode;
+    private InterfaceSet mUpstreamIfaceSet;  // may change over time
+    private InterfaceParams mInterfaceParams;
+    // TODO: De-duplicate this with mLinkProperties above. Currently, these link
+    // properties are those selected by the IPv6TetheringCoordinator and relayed
+    // to us. By comparison, mLinkProperties contains the addresses and directly
+    // connected routes that have been formed from these properties iff. we have
+    // succeeded in configuring them and are able to announce them within Router
+    // Advertisements (otherwise, we do not add them to mLinkProperties at all).
+    private LinkProperties mLastIPv6LinkProperties;
+    private RouterAdvertisementDaemon mRaDaemon;
+
+    // To be accessed only on the handler thread
+    private int mDhcpServerStartIndex = 0;
+    private IDhcpServer mDhcpServer;
+    private RaParams mLastRaParams;
+
+    public IpServer(
+            String ifaceName, Looper looper, int interfaceType, SharedLog log,
+            INetworkManagementService nMService, INetworkStatsService statsService,
+            Callback callback, boolean usingLegacyDhcp, Dependencies deps) {
+        super(ifaceName, looper);
+        mLog = log.forSubComponent(ifaceName);
+        mNMService = nMService;
+        mNetd = deps.getNetdService();
+        mStatsService = statsService;
+        mCallback = callback;
+        mInterfaceCtrl = new InterfaceController(ifaceName, mNetd, mLog);
+        mIfaceName = ifaceName;
+        mInterfaceType = interfaceType;
+        mLinkProperties = new LinkProperties();
+        mUsingLegacyDhcp = usingLegacyDhcp;
+        mDeps = deps;
+        resetLinkProperties();
+        mLastError = ConnectivityManager.TETHER_ERROR_NO_ERROR;
+        mServingMode = STATE_AVAILABLE;
+
+        mInitialState = new InitialState();
+        mLocalHotspotState = new LocalHotspotState();
+        mTetheredState = new TetheredState();
+        mUnavailableState = new UnavailableState();
+        addState(mInitialState);
+        addState(mLocalHotspotState);
+        addState(mTetheredState);
+        addState(mUnavailableState);
+
+        setInitialState(mInitialState);
+    }
+
+    public String interfaceName() { return mIfaceName; }
+
+    public int interfaceType() { return mInterfaceType; }
+
+    public int lastError() { return mLastError; }
+
+    public int servingMode() { return mServingMode; }
+
+    public LinkProperties linkProperties() { return new LinkProperties(mLinkProperties); }
+
+    public void stop() { sendMessage(CMD_INTERFACE_DOWN); }
+
+    public void unwanted() { sendMessage(CMD_TETHER_UNREQUESTED); }
+
+    /**
+     * Internals.
+     */
+
+    private boolean startIPv4() { return configureIPv4(true); }
+
+    /**
+     * Convenience wrapper around INetworkStackStatusCallback to run callbacks on the IpServer
+     * handler.
+     *
+     * <p>Different instances of this class can be created for each call to IDhcpServer methods,
+     * with different implementations of the callback, to differentiate handling of success/error in
+     * each call.
+     */
+    private abstract class OnHandlerStatusCallback extends INetworkStackStatusCallback.Stub {
+        @Override
+        public void onStatusAvailable(int statusCode) {
+            getHandler().post(() -> callback(statusCode));
+        }
+
+        public abstract void callback(int statusCode);
+
+        @Override
+        public int getInterfaceVersion() {
+            return this.VERSION;
+        }
+    }
+
+    private class DhcpServerCallbacksImpl extends DhcpServerCallbacks {
+        private final int mStartIndex;
+
+        private DhcpServerCallbacksImpl(int startIndex) {
+            mStartIndex = startIndex;
+        }
+
+        @Override
+        public void onDhcpServerCreated(int statusCode, IDhcpServer server) throws RemoteException {
+            getHandler().post(() -> {
+                // We are on the handler thread: mDhcpServerStartIndex can be read safely.
+                if (mStartIndex != mDhcpServerStartIndex) {
+                    // This start request is obsolete. When the |server| binder token goes out of
+                    // scope, the garbage collector will finalize it, which causes the network stack
+                    // process garbage collector to collect the server itself.
+                    return;
+                }
+
+                if (statusCode != STATUS_SUCCESS) {
+                    mLog.e("Error obtaining DHCP server: " + statusCode);
+                    handleError();
+                    return;
+                }
+
+                mDhcpServer = server;
+                try {
+                    mDhcpServer.start(new OnHandlerStatusCallback() {
+                        @Override
+                        public void callback(int startStatusCode) {
+                            if (startStatusCode != STATUS_SUCCESS) {
+                                mLog.e("Error starting DHCP server: " + startStatusCode);
+                                handleError();
+                            }
+                        }
+                    });
+                } catch (RemoteException e) {
+                    e.rethrowFromSystemServer();
+                }
+            });
+        }
+
+        private void handleError() {
+            mLastError = ConnectivityManager.TETHER_ERROR_DHCPSERVER_ERROR;
+            transitionTo(mInitialState);
+        }
+    }
+
+    private boolean startDhcp(Inet4Address addr, int prefixLen) {
+        if (mUsingLegacyDhcp) {
+            return true;
+        }
+        final DhcpServingParamsParcel params;
+        params = new DhcpServingParamsParcelExt()
+                .setDefaultRouters(addr)
+                .setDhcpLeaseTimeSecs(DHCP_LEASE_TIME_SECS)
+                .setDnsServers(addr)
+                .setServerAddr(new LinkAddress(addr, prefixLen))
+                .setMetered(true);
+        // TODO: also advertise link MTU
+
+        mDhcpServerStartIndex++;
+        mDeps.makeDhcpServer(
+                mIfaceName, params, new DhcpServerCallbacksImpl(mDhcpServerStartIndex));
+        return true;
+    }
+
+    private void stopDhcp() {
+        // Make all previous start requests obsolete so servers are not started later
+        mDhcpServerStartIndex++;
+
+        if (mDhcpServer != null) {
+            try {
+                mDhcpServer.stop(new OnHandlerStatusCallback() {
+                    @Override
+                    public void callback(int statusCode) {
+                        if (statusCode != STATUS_SUCCESS) {
+                            mLog.e("Error stopping DHCP server: " + statusCode);
+                            mLastError = ConnectivityManager.TETHER_ERROR_DHCPSERVER_ERROR;
+                            // Not much more we can do here
+                        }
+                    }
+                });
+                mDhcpServer = null;
+            } catch (RemoteException e) {
+                e.rethrowFromSystemServer();
+            }
+        }
+    }
+
+    private boolean configureDhcp(boolean enable, Inet4Address addr, int prefixLen) {
+        if (enable) {
+            return startDhcp(addr, prefixLen);
+        } else {
+            stopDhcp();
+            return true;
+        }
+    }
+
+    private void stopIPv4() {
+        configureIPv4(false);
+        // NOTE: All of configureIPv4() will be refactored out of existence
+        // into calls to InterfaceController, shared with startIPv4().
+        mInterfaceCtrl.clearIPv4Address();
+    }
+
+    // TODO: Refactor this in terms of calls to InterfaceController.
+    private boolean configureIPv4(boolean enabled) {
+        if (VDBG) Log.d(TAG, "configureIPv4(" + enabled + ")");
+
+        // TODO: Replace this hard-coded information with dynamically selected
+        // config passed down to us by a higher layer IP-coordinating element.
+        String ipAsString = null;
+        int prefixLen = 0;
+        if (mInterfaceType == ConnectivityManager.TETHERING_USB) {
+            ipAsString = USB_NEAR_IFACE_ADDR;
+            prefixLen = USB_PREFIX_LENGTH;
+        } else if (mInterfaceType == ConnectivityManager.TETHERING_WIFI) {
+            ipAsString = getRandomWifiIPv4Address();
+            prefixLen = WIFI_HOST_IFACE_PREFIX_LENGTH;
+        } else {
+            // BT configures the interface elsewhere: only start DHCP.
+            final Inet4Address srvAddr = (Inet4Address) numericToInetAddress(BLUETOOTH_IFACE_ADDR);
+            return configureDhcp(enabled, srvAddr, BLUETOOTH_DHCP_PREFIX_LENGTH);
+        }
+
+        final LinkAddress linkAddr;
+        try {
+            final InterfaceConfiguration ifcg = mNMService.getInterfaceConfig(mIfaceName);
+            if (ifcg == null) {
+                mLog.e("Received null interface config");
+                return false;
+            }
+
+            InetAddress addr = numericToInetAddress(ipAsString);
+            linkAddr = new LinkAddress(addr, prefixLen);
+            ifcg.setLinkAddress(linkAddr);
+            if (mInterfaceType == ConnectivityManager.TETHERING_WIFI) {
+                // The WiFi stack has ownership of the interface up/down state.
+                // It is unclear whether the Bluetooth or USB stacks will manage their own
+                // state.
+                ifcg.ignoreInterfaceUpDownStatus();
+            } else {
+                if (enabled) {
+                    ifcg.setInterfaceUp();
+                } else {
+                    ifcg.setInterfaceDown();
+                }
+            }
+            ifcg.clearFlag("running");
+
+            // TODO: this may throw if the interface is already gone. Do proper handling and
+            // simplify the DHCP server start/stop.
+            mNMService.setInterfaceConfig(mIfaceName, ifcg);
+
+            if (!configureDhcp(enabled, (Inet4Address) addr, prefixLen)) {
+                return false;
+            }
+        } catch (Exception e) {
+            mLog.e("Error configuring interface " + e);
+            if (!enabled) {
+                try {
+                    // Calling stopDhcp several times is fine
+                    stopDhcp();
+                } catch (Exception dhcpError) {
+                    mLog.e("Error stopping DHCP", dhcpError);
+                }
+            }
+            return false;
+        }
+
+        // Directly-connected route.
+        final RouteInfo route = new RouteInfo(linkAddr);
+        if (enabled) {
+            mLinkProperties.addLinkAddress(linkAddr);
+            mLinkProperties.addRoute(route);
+        } else {
+            mLinkProperties.removeLinkAddress(linkAddr);
+            mLinkProperties.removeRoute(route);
+        }
+        return true;
+    }
+
+    private String getRandomWifiIPv4Address() {
+        try {
+            byte[] bytes = numericToInetAddress(WIFI_HOST_IFACE_ADDR).getAddress();
+            bytes[3] = getRandomSanitizedByte(DOUG_ADAMS, asByte(0), asByte(1), FF);
+            return InetAddress.getByAddress(bytes).getHostAddress();
+        } catch (Exception e) {
+            return WIFI_HOST_IFACE_ADDR;
+        }
+    }
+
+    private boolean startIPv6() {
+        mInterfaceParams = mDeps.getInterfaceParams(mIfaceName);
+        if (mInterfaceParams == null) {
+            mLog.e("Failed to find InterfaceParams");
+            stopIPv6();
+            return false;
+        }
+
+        mRaDaemon = mDeps.getRouterAdvertisementDaemon(mInterfaceParams);
+        if (!mRaDaemon.start()) {
+            stopIPv6();
+            return false;
+        }
+
+        return true;
+    }
+
+    private void stopIPv6() {
+        mInterfaceParams = null;
+        setRaParams(null);
+
+        if (mRaDaemon != null) {
+            mRaDaemon.stop();
+            mRaDaemon = null;
+        }
+    }
+
+    // IPv6TetheringCoordinator sends updates with carefully curated IPv6-only
+    // LinkProperties. These have extraneous data filtered out and only the
+    // necessary prefixes included (per its prefix distribution policy).
+    //
+    // TODO: Evaluate using a data structure than is more directly suited to
+    // communicating only the relevant information.
+    private void updateUpstreamIPv6LinkProperties(LinkProperties v6only) {
+        if (mRaDaemon == null) return;
+
+        // Avoid unnecessary work on spurious updates.
+        if (Objects.equals(mLastIPv6LinkProperties, v6only)) {
+            return;
+        }
+
+        RaParams params = null;
+
+        if (v6only != null) {
+            params = new RaParams();
+            params.mtu = v6only.getMtu();
+            params.hasDefaultRoute = v6only.hasIpv6DefaultRoute();
+
+            if (params.hasDefaultRoute) params.hopLimit = getHopLimit(v6only.getInterfaceName());
+
+            for (LinkAddress linkAddr : v6only.getLinkAddresses()) {
+                if (linkAddr.getPrefixLength() != RFC7421_PREFIX_LENGTH) continue;
+
+                final IpPrefix prefix = new IpPrefix(
+                        linkAddr.getAddress(), linkAddr.getPrefixLength());
+                params.prefixes.add(prefix);
+
+                final Inet6Address dnsServer = getLocalDnsIpFor(prefix);
+                if (dnsServer != null) {
+                    params.dnses.add(dnsServer);
+                }
+            }
+        }
+        // If v6only is null, we pass in null to setRaParams(), which handles
+        // deprecation of any existing RA data.
+
+        setRaParams(params);
+        mLastIPv6LinkProperties = v6only;
+    }
+
+    private void configureLocalIPv6Routes(
+            HashSet<IpPrefix> deprecatedPrefixes, HashSet<IpPrefix> newPrefixes) {
+        // [1] Remove the routes that are deprecated.
+        if (!deprecatedPrefixes.isEmpty()) {
+            final ArrayList<RouteInfo> toBeRemoved =
+                    getLocalRoutesFor(mIfaceName, deprecatedPrefixes);
+            try {
+                final int removalFailures = mNMService.removeRoutesFromLocalNetwork(toBeRemoved);
+                if (removalFailures > 0) {
+                    mLog.e(String.format("Failed to remove %d IPv6 routes from local table.",
+                            removalFailures));
+                }
+            } catch (RemoteException e) {
+                mLog.e("Failed to remove IPv6 routes from local table: " + e);
+            }
+
+            for (RouteInfo route : toBeRemoved) mLinkProperties.removeRoute(route);
+        }
+
+        // [2] Add only the routes that have not previously been added.
+        if (newPrefixes != null && !newPrefixes.isEmpty()) {
+            HashSet<IpPrefix> addedPrefixes = (HashSet) newPrefixes.clone();
+            if (mLastRaParams != null) {
+                addedPrefixes.removeAll(mLastRaParams.prefixes);
+            }
+
+            if (!addedPrefixes.isEmpty()) {
+                final ArrayList<RouteInfo> toBeAdded =
+                        getLocalRoutesFor(mIfaceName, addedPrefixes);
+                try {
+                    // It's safe to call addInterfaceToLocalNetwork() even if
+                    // the interface is already in the local_network. Note also
+                    // that adding routes that already exist does not cause an
+                    // error (EEXIST is silently ignored).
+                    mNMService.addInterfaceToLocalNetwork(mIfaceName, toBeAdded);
+                } catch (Exception e) {
+                    mLog.e("Failed to add IPv6 routes to local table: " + e);
+                }
+
+                for (RouteInfo route : toBeAdded) mLinkProperties.addRoute(route);
+            }
+        }
+    }
+
+    private void configureLocalIPv6Dns(
+            HashSet<Inet6Address> deprecatedDnses, HashSet<Inet6Address> newDnses) {
+        // TODO: Is this really necessary? Can we not fail earlier if INetd cannot be located?
+        if (mNetd == null) {
+            if (newDnses != null) newDnses.clear();
+            mLog.e("No netd service instance available; not setting local IPv6 addresses");
+            return;
+        }
+
+        // [1] Remove deprecated local DNS IP addresses.
+        if (!deprecatedDnses.isEmpty()) {
+            for (Inet6Address dns : deprecatedDnses) {
+                if (!mInterfaceCtrl.removeAddress(dns, RFC7421_PREFIX_LENGTH)) {
+                    mLog.e("Failed to remove local dns IP " + dns);
+                }
+
+                mLinkProperties.removeLinkAddress(new LinkAddress(dns, RFC7421_PREFIX_LENGTH));
+            }
+        }
+
+        // [2] Add only the local DNS IP addresses that have not previously been added.
+        if (newDnses != null && !newDnses.isEmpty()) {
+            final HashSet<Inet6Address> addedDnses = (HashSet) newDnses.clone();
+            if (mLastRaParams != null) {
+                addedDnses.removeAll(mLastRaParams.dnses);
+            }
+
+            for (Inet6Address dns : addedDnses) {
+                if (!mInterfaceCtrl.addAddress(dns, RFC7421_PREFIX_LENGTH)) {
+                    mLog.e("Failed to add local dns IP " + dns);
+                    newDnses.remove(dns);
+                }
+
+                mLinkProperties.addLinkAddress(new LinkAddress(dns, RFC7421_PREFIX_LENGTH));
+            }
+        }
+
+        try {
+            mNetd.tetherApplyDnsInterfaces();
+        } catch (ServiceSpecificException | RemoteException e) {
+            mLog.e("Failed to update local DNS caching server");
+            if (newDnses != null) newDnses.clear();
+        }
+    }
+
+    private byte getHopLimit(String upstreamIface) {
+        try {
+            int upstreamHopLimit = Integer.parseUnsignedInt(
+                    mNetd.getProcSysNet(INetd.IPV6, INetd.CONF, upstreamIface, "hop_limit"));
+            // Add one hop to account for this forwarding device
+            upstreamHopLimit++;
+            // Cap the hop limit to 255.
+            return (byte) Integer.min(upstreamHopLimit, 255);
+        } catch (Exception e) {
+            mLog.e("Failed to find upstream interface hop limit", e);
+        }
+        return RaParams.DEFAULT_HOPLIMIT;
+    }
+
+    private void setRaParams(RaParams newParams) {
+        if (mRaDaemon != null) {
+            final RaParams deprecatedParams =
+                    RaParams.getDeprecatedRaParams(mLastRaParams, newParams);
+
+            configureLocalIPv6Routes(deprecatedParams.prefixes,
+                    (newParams != null) ? newParams.prefixes : null);
+
+            configureLocalIPv6Dns(deprecatedParams.dnses,
+                    (newParams != null) ? newParams.dnses : null);
+
+            mRaDaemon.buildNewRa(deprecatedParams, newParams);
+        }
+
+        mLastRaParams = newParams;
+    }
+
+    private void logMessage(State state, int what) {
+        mLog.log(state.getName() + " got " + sMagicDecoderRing.get(what, Integer.toString(what)));
+    }
+
+    private void sendInterfaceState(int newInterfaceState) {
+        mServingMode = newInterfaceState;
+        mCallback.updateInterfaceState(this, newInterfaceState, mLastError);
+        sendLinkProperties();
+    }
+
+    private void sendLinkProperties() {
+        mCallback.updateLinkProperties(this, new LinkProperties(mLinkProperties));
+    }
+
+    private void resetLinkProperties() {
+        mLinkProperties.clear();
+        mLinkProperties.setInterfaceName(mIfaceName);
+    }
+
+    class InitialState extends State {
+        @Override
+        public void enter() {
+            sendInterfaceState(STATE_AVAILABLE);
+        }
+
+        @Override
+        public boolean processMessage(Message message) {
+            logMessage(this, message.what);
+            switch (message.what) {
+                case CMD_TETHER_REQUESTED:
+                    mLastError = ConnectivityManager.TETHER_ERROR_NO_ERROR;
+                    switch (message.arg1) {
+                        case STATE_LOCAL_ONLY:
+                            transitionTo(mLocalHotspotState);
+                            break;
+                        case STATE_TETHERED:
+                            transitionTo(mTetheredState);
+                            break;
+                        default:
+                            mLog.e("Invalid tethering interface serving state specified.");
+                    }
+                    break;
+                case CMD_INTERFACE_DOWN:
+                    transitionTo(mUnavailableState);
+                    break;
+                case CMD_IPV6_TETHER_UPDATE:
+                    updateUpstreamIPv6LinkProperties((LinkProperties) message.obj);
+                    break;
+                default:
+                    return NOT_HANDLED;
+            }
+            return HANDLED;
+        }
+    }
+
+    class BaseServingState extends State {
+        @Override
+        public void enter() {
+            if (!startIPv4()) {
+                mLastError = ConnectivityManager.TETHER_ERROR_IFACE_CFG_ERROR;
+                return;
+            }
+
+            try {
+                mNMService.tetherInterface(mIfaceName);
+            } catch (Exception e) {
+                mLog.e("Error Tethering: " + e);
+                mLastError = ConnectivityManager.TETHER_ERROR_TETHER_IFACE_ERROR;
+                return;
+            }
+
+            if (!startIPv6()) {
+                mLog.e("Failed to startIPv6");
+                // TODO: Make this a fatal error once Bluetooth IPv6 is sorted.
+                return;
+            }
+        }
+
+        @Override
+        public void exit() {
+            // Note that at this point, we're leaving the tethered state.  We can fail any
+            // of these operations, but it doesn't really change that we have to try them
+            // all in sequence.
+            stopIPv6();
+
+            try {
+                mNMService.untetherInterface(mIfaceName);
+            } catch (Exception e) {
+                mLastError = ConnectivityManager.TETHER_ERROR_UNTETHER_IFACE_ERROR;
+                mLog.e("Failed to untether interface: " + e);
+            }
+
+            stopIPv4();
+
+            resetLinkProperties();
+        }
+
+        @Override
+        public boolean processMessage(Message message) {
+            logMessage(this, message.what);
+            switch (message.what) {
+                case CMD_TETHER_UNREQUESTED:
+                    transitionTo(mInitialState);
+                    if (DBG) Log.d(TAG, "Untethered (unrequested)" + mIfaceName);
+                    break;
+                case CMD_INTERFACE_DOWN:
+                    transitionTo(mUnavailableState);
+                    if (DBG) Log.d(TAG, "Untethered (ifdown)" + mIfaceName);
+                    break;
+                case CMD_IPV6_TETHER_UPDATE:
+                    updateUpstreamIPv6LinkProperties((LinkProperties) message.obj);
+                    sendLinkProperties();
+                    break;
+                case CMD_IP_FORWARDING_ENABLE_ERROR:
+                case CMD_IP_FORWARDING_DISABLE_ERROR:
+                case CMD_START_TETHERING_ERROR:
+                case CMD_STOP_TETHERING_ERROR:
+                case CMD_SET_DNS_FORWARDERS_ERROR:
+                    mLastError = ConnectivityManager.TETHER_ERROR_MASTER_ERROR;
+                    transitionTo(mInitialState);
+                    break;
+                default:
+                    return false;
+            }
+            return true;
+        }
+    }
+
+    // Handling errors in BaseServingState.enter() by transitioning is
+    // problematic because transitioning during a multi-state jump yields
+    // a Log.wtf(). Ultimately, there should be only one ServingState,
+    // and forwarding and NAT rules should be handled by a coordinating
+    // functional element outside of IpServer.
+    class LocalHotspotState extends BaseServingState {
+        @Override
+        public void enter() {
+            super.enter();
+            if (mLastError != ConnectivityManager.TETHER_ERROR_NO_ERROR) {
+                transitionTo(mInitialState);
+            }
+
+            if (DBG) Log.d(TAG, "Local hotspot " + mIfaceName);
+            sendInterfaceState(STATE_LOCAL_ONLY);
+        }
+
+        @Override
+        public boolean processMessage(Message message) {
+            if (super.processMessage(message)) return true;
+
+            logMessage(this, message.what);
+            switch (message.what) {
+                case CMD_TETHER_REQUESTED:
+                    mLog.e("CMD_TETHER_REQUESTED while in local-only hotspot mode.");
+                    break;
+                case CMD_TETHER_CONNECTION_CHANGED:
+                    // Ignored in local hotspot state.
+                    break;
+                default:
+                    return false;
+            }
+            return true;
+        }
+    }
+
+    // Handling errors in BaseServingState.enter() by transitioning is
+    // problematic because transitioning during a multi-state jump yields
+    // a Log.wtf(). Ultimately, there should be only one ServingState,
+    // and forwarding and NAT rules should be handled by a coordinating
+    // functional element outside of IpServer.
+    class TetheredState extends BaseServingState {
+        @Override
+        public void enter() {
+            super.enter();
+            if (mLastError != ConnectivityManager.TETHER_ERROR_NO_ERROR) {
+                transitionTo(mInitialState);
+            }
+
+            if (DBG) Log.d(TAG, "Tethered " + mIfaceName);
+            sendInterfaceState(STATE_TETHERED);
+        }
+
+        @Override
+        public void exit() {
+            cleanupUpstream();
+            super.exit();
+        }
+
+        private void cleanupUpstream() {
+            if (mUpstreamIfaceSet == null) return;
+
+            for (String ifname : mUpstreamIfaceSet.ifnames) cleanupUpstreamInterface(ifname);
+            mUpstreamIfaceSet = null;
+        }
+
+        private void cleanupUpstreamInterface(String upstreamIface) {
+            // Note that we don't care about errors here.
+            // Sometimes interfaces are gone before we get
+            // to remove their rules, which generates errors.
+            // Just do the best we can.
+            try {
+                // About to tear down NAT; gather remaining statistics.
+                mStatsService.forceUpdate();
+            } catch (Exception e) {
+                if (VDBG) Log.e(TAG, "Exception in forceUpdate: " + e.toString());
+            }
+            try {
+                mNMService.stopInterfaceForwarding(mIfaceName, upstreamIface);
+            } catch (Exception e) {
+                if (VDBG) Log.e(TAG, "Exception in removeInterfaceForward: " + e.toString());
+            }
+            try {
+                mNMService.disableNat(mIfaceName, upstreamIface);
+            } catch (Exception e) {
+                if (VDBG) Log.e(TAG, "Exception in disableNat: " + e.toString());
+            }
+        }
+
+        @Override
+        public boolean processMessage(Message message) {
+            if (super.processMessage(message)) return true;
+
+            logMessage(this, message.what);
+            switch (message.what) {
+                case CMD_TETHER_REQUESTED:
+                    mLog.e("CMD_TETHER_REQUESTED while already tethering.");
+                    break;
+                case CMD_TETHER_CONNECTION_CHANGED:
+                    final InterfaceSet newUpstreamIfaceSet = (InterfaceSet) message.obj;
+                    if (noChangeInUpstreamIfaceSet(newUpstreamIfaceSet)) {
+                        if (VDBG) Log.d(TAG, "Connection changed noop - dropping");
+                        break;
+                    }
+
+                    if (newUpstreamIfaceSet == null) {
+                        cleanupUpstream();
+                        break;
+                    }
+
+                    for (String removed : upstreamInterfacesRemoved(newUpstreamIfaceSet)) {
+                        cleanupUpstreamInterface(removed);
+                    }
+
+                    final Set<String> added = upstreamInterfacesAdd(newUpstreamIfaceSet);
+                    // This makes the call to cleanupUpstream() in the error
+                    // path for any interface neatly cleanup all the interfaces.
+                    mUpstreamIfaceSet = newUpstreamIfaceSet;
+
+                    for (String ifname : added) {
+                        try {
+                            mNMService.enableNat(mIfaceName, ifname);
+                            mNMService.startInterfaceForwarding(mIfaceName, ifname);
+                        } catch (Exception e) {
+                            mLog.e("Exception enabling NAT: " + e);
+                            cleanupUpstream();
+                            mLastError = ConnectivityManager.TETHER_ERROR_ENABLE_NAT_ERROR;
+                            transitionTo(mInitialState);
+                            return true;
+                        }
+                    }
+                    break;
+                default:
+                    return false;
+            }
+            return true;
+        }
+
+        private boolean noChangeInUpstreamIfaceSet(InterfaceSet newIfaces) {
+            if (mUpstreamIfaceSet == null && newIfaces == null) return true;
+            if (mUpstreamIfaceSet != null && newIfaces != null) {
+                return mUpstreamIfaceSet.equals(newIfaces);
+            }
+            return false;
+        }
+
+        private Set<String> upstreamInterfacesRemoved(InterfaceSet newIfaces) {
+            if (mUpstreamIfaceSet == null) return new HashSet<>();
+
+            final HashSet<String> removed = new HashSet<>(mUpstreamIfaceSet.ifnames);
+            removed.removeAll(newIfaces.ifnames);
+            return removed;
+        }
+
+        private Set<String> upstreamInterfacesAdd(InterfaceSet newIfaces) {
+            final HashSet<String> added = new HashSet<>(newIfaces.ifnames);
+            if (mUpstreamIfaceSet != null) added.removeAll(mUpstreamIfaceSet.ifnames);
+            return added;
+        }
+    }
+
+    /**
+     * This state is terminal for the per interface state machine.  At this
+     * point, the master state machine should have removed this interface
+     * specific state machine from its list of possible recipients of
+     * tethering requests.  The state machine itself will hang around until
+     * the garbage collector finds it.
+     */
+    class UnavailableState extends State {
+        @Override
+        public void enter() {
+            mLastError = ConnectivityManager.TETHER_ERROR_NO_ERROR;
+            sendInterfaceState(STATE_UNAVAILABLE);
+        }
+    }
+
+    // Accumulate routes representing "prefixes to be assigned to the local
+    // interface", for subsequent modification of local_network routing.
+    private static ArrayList<RouteInfo> getLocalRoutesFor(
+            String ifname, HashSet<IpPrefix> prefixes) {
+        final ArrayList<RouteInfo> localRoutes = new ArrayList<RouteInfo>();
+        for (IpPrefix ipp : prefixes) {
+            localRoutes.add(new RouteInfo(ipp, null, ifname));
+        }
+        return localRoutes;
+    }
+
+    // Given a prefix like 2001:db8::/64 return an address like 2001:db8::1.
+    private static Inet6Address getLocalDnsIpFor(IpPrefix localPrefix) {
+        final byte[] dnsBytes = localPrefix.getRawAddress();
+        dnsBytes[dnsBytes.length - 1] = getRandomSanitizedByte(DOUG_ADAMS, asByte(0), asByte(1));
+        try {
+            return Inet6Address.getByAddress(null, dnsBytes, 0);
+        } catch (UnknownHostException e) {
+            Slog.wtf(TAG, "Failed to construct Inet6Address from: " + localPrefix);
+            return null;
+        }
+    }
+
+    private static byte getRandomSanitizedByte(byte dflt, byte... excluded) {
+        final byte random = (byte) (new Random()).nextInt();
+        for (int value : excluded) {
+            if (random == value) return dflt;
+        }
+        return random;
+    }
+}
diff --git a/android/net/ip/RouterAdvertisementDaemon.java b/android/net/ip/RouterAdvertisementDaemon.java
new file mode 100644
index 0000000..59aea21
--- /dev/null
+++ b/android/net/ip/RouterAdvertisementDaemon.java
@@ -0,0 +1,744 @@
+/*
+ * 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.ip;
+
+import static android.net.util.NetworkConstants.IPV6_MIN_MTU;
+import static android.net.util.NetworkConstants.RFC7421_PREFIX_LENGTH;
+import static android.system.OsConstants.AF_INET6;
+import static android.system.OsConstants.IPPROTO_ICMPV6;
+import static android.system.OsConstants.SOCK_RAW;
+import static android.system.OsConstants.SOL_SOCKET;
+import static android.system.OsConstants.SO_BINDTODEVICE;
+import static android.system.OsConstants.SO_SNDTIMEO;
+
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.NetworkUtils;
+import android.net.TrafficStats;
+import android.net.util.InterfaceParams;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.StructTimeval;
+import android.util.Log;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.TrafficStatsConstants;
+
+import libcore.io.IoBridge;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.nio.BufferOverflowException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Random;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicInteger;
+
+
+/**
+ * Basic IPv6 Router Advertisement Daemon.
+ *
+ * TODO:
+ *
+ *     - Rewrite using Handler (and friends) so that AlarmManager can deliver
+ *       "kick" messages when it's time to send a multicast RA.
+ *
+ * @hide
+ */
+public class RouterAdvertisementDaemon {
+    private static final String TAG = RouterAdvertisementDaemon.class.getSimpleName();
+    private static final byte ICMPV6_ND_ROUTER_SOLICIT = asByte(133);
+    private static final byte ICMPV6_ND_ROUTER_ADVERT  = asByte(134);
+    private static final int MIN_RA_HEADER_SIZE = 16;
+
+    // Summary of various timers and lifetimes.
+    private static final int MIN_RTR_ADV_INTERVAL_SEC = 300;
+    private static final int MAX_RTR_ADV_INTERVAL_SEC = 600;
+    // In general, router, prefix, and DNS lifetimes are all advised to be
+    // greater than or equal to 3 * MAX_RTR_ADV_INTERVAL.  Here, we double
+    // that to allow for multicast packet loss.
+    //
+    // This MAX_RTR_ADV_INTERVAL_SEC and DEFAULT_LIFETIME are also consistent
+    // with the https://tools.ietf.org/html/rfc7772#section-4 discussion of
+    // "approximately 7 RAs per hour".
+    private static final int DEFAULT_LIFETIME = 6 * MAX_RTR_ADV_INTERVAL_SEC;
+    // From https://tools.ietf.org/html/rfc4861#section-10 .
+    private static final int MIN_DELAY_BETWEEN_RAS_SEC = 3;
+    // Both initial and final RAs, but also for changes in RA contents.
+    // From https://tools.ietf.org/html/rfc4861#section-10 .
+    private static final int  MAX_URGENT_RTR_ADVERTISEMENTS = 5;
+
+    private static final int DAY_IN_SECONDS = 86_400;
+
+    private static final byte[] ALL_NODES = new byte[] {
+            (byte) 0xff, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1
+    };
+
+    private final InterfaceParams mInterface;
+    private final InetSocketAddress mAllNodes;
+
+    // This lock is to protect the RA from being updated while being
+    // transmitted on another thread  (multicast or unicast).
+    //
+    // TODO: This should be handled with a more RCU-like approach.
+    private final Object mLock = new Object();
+    @GuardedBy("mLock")
+    private final byte[] mRA = new byte[IPV6_MIN_MTU];
+    @GuardedBy("mLock")
+    private int mRaLength;
+    @GuardedBy("mLock")
+    private final DeprecatedInfoTracker mDeprecatedInfoTracker;
+    @GuardedBy("mLock")
+    private RaParams mRaParams;
+
+    private volatile FileDescriptor mSocket;
+    private volatile MulticastTransmitter mMulticastTransmitter;
+    private volatile UnicastResponder mUnicastResponder;
+
+    public static class RaParams {
+        // Tethered traffic will have the hop limit properly decremented.
+        // Consequently, set the hoplimit greater by one than the upstream
+        // unicast hop limit.
+        //
+        // TODO: Dynamically pass down the IPV6_UNICAST_HOPS value from the
+        // upstream interface for more correct behaviour.
+        static final byte DEFAULT_HOPLIMIT = 65;
+
+        public boolean hasDefaultRoute;
+        public byte hopLimit;
+        public int mtu;
+        public HashSet<IpPrefix> prefixes;
+        public HashSet<Inet6Address> dnses;
+
+        public RaParams() {
+            hasDefaultRoute = false;
+            hopLimit = DEFAULT_HOPLIMIT;
+            mtu = IPV6_MIN_MTU;
+            prefixes = new HashSet<IpPrefix>();
+            dnses = new HashSet<Inet6Address>();
+        }
+
+        public RaParams(RaParams other) {
+            hasDefaultRoute = other.hasDefaultRoute;
+            hopLimit = other.hopLimit;
+            mtu = other.mtu;
+            prefixes = (HashSet) other.prefixes.clone();
+            dnses = (HashSet) other.dnses.clone();
+        }
+
+        // Returns the subset of RA parameters that become deprecated when
+        // moving from announcing oldRa to announcing newRa.
+        //
+        // Currently only tracks differences in |prefixes| and |dnses|.
+        public static RaParams getDeprecatedRaParams(RaParams oldRa, RaParams newRa) {
+            RaParams newlyDeprecated = new RaParams();
+
+            if (oldRa != null) {
+                for (IpPrefix ipp : oldRa.prefixes) {
+                    if (newRa == null || !newRa.prefixes.contains(ipp)) {
+                        newlyDeprecated.prefixes.add(ipp);
+                    }
+                }
+
+                for (Inet6Address dns : oldRa.dnses) {
+                    if (newRa == null || !newRa.dnses.contains(dns)) {
+                        newlyDeprecated.dnses.add(dns);
+                    }
+                }
+            }
+
+            return newlyDeprecated;
+        }
+    }
+
+    private static class DeprecatedInfoTracker {
+        private final HashMap<IpPrefix, Integer> mPrefixes = new HashMap<>();
+        private final HashMap<Inet6Address, Integer> mDnses = new HashMap<>();
+
+        Set<IpPrefix> getPrefixes() { return mPrefixes.keySet(); }
+
+        void putPrefixes(Set<IpPrefix> prefixes) {
+            for (IpPrefix ipp : prefixes) {
+                mPrefixes.put(ipp, MAX_URGENT_RTR_ADVERTISEMENTS);
+            }
+        }
+
+        void removePrefixes(Set<IpPrefix> prefixes) {
+            for (IpPrefix ipp : prefixes) {
+                mPrefixes.remove(ipp);
+            }
+        }
+
+        Set<Inet6Address> getDnses() { return mDnses.keySet(); }
+
+        void putDnses(Set<Inet6Address> dnses) {
+            for (Inet6Address dns : dnses) {
+                mDnses.put(dns, MAX_URGENT_RTR_ADVERTISEMENTS);
+            }
+        }
+
+        void removeDnses(Set<Inet6Address> dnses) {
+            for (Inet6Address dns : dnses) {
+                mDnses.remove(dns);
+            }
+        }
+
+        boolean isEmpty() { return mPrefixes.isEmpty() && mDnses.isEmpty(); }
+
+        private boolean decrementCounters() {
+            boolean removed = decrementCounter(mPrefixes);
+            removed |= decrementCounter(mDnses);
+            return removed;
+        }
+
+        private <T> boolean decrementCounter(HashMap<T, Integer> map) {
+            boolean removed = false;
+
+            for (Iterator<Map.Entry<T, Integer>> it = map.entrySet().iterator();
+                 it.hasNext();) {
+                Map.Entry<T, Integer> kv = it.next();
+                if (kv.getValue() == 0) {
+                    it.remove();
+                    removed = true;
+                } else {
+                    kv.setValue(kv.getValue() - 1);
+                }
+            }
+
+            return removed;
+        }
+    }
+
+
+    public RouterAdvertisementDaemon(InterfaceParams ifParams) {
+        mInterface = ifParams;
+        mAllNodes = new InetSocketAddress(getAllNodesForScopeId(mInterface.index), 0);
+        mDeprecatedInfoTracker = new DeprecatedInfoTracker();
+    }
+
+    public void buildNewRa(RaParams deprecatedParams, RaParams newParams) {
+        synchronized (mLock) {
+            if (deprecatedParams != null) {
+                mDeprecatedInfoTracker.putPrefixes(deprecatedParams.prefixes);
+                mDeprecatedInfoTracker.putDnses(deprecatedParams.dnses);
+            }
+
+            if (newParams != null) {
+                // Process information that is no longer deprecated.
+                mDeprecatedInfoTracker.removePrefixes(newParams.prefixes);
+                mDeprecatedInfoTracker.removeDnses(newParams.dnses);
+            }
+
+            mRaParams = newParams;
+            assembleRaLocked();
+        }
+
+        maybeNotifyMulticastTransmitter();
+    }
+
+    public boolean start() {
+        if (!createSocket()) {
+            return false;
+        }
+
+        mMulticastTransmitter = new MulticastTransmitter();
+        mMulticastTransmitter.start();
+
+        mUnicastResponder = new UnicastResponder();
+        mUnicastResponder.start();
+
+        return true;
+    }
+
+    public void stop() {
+        closeSocket();
+        // Wake up mMulticastTransmitter thread to interrupt a potential 1 day sleep before
+        // the thread's termination.
+        maybeNotifyMulticastTransmitter();
+        mMulticastTransmitter = null;
+        mUnicastResponder = null;
+    }
+
+    @GuardedBy("mLock")
+    private void assembleRaLocked() {
+        final ByteBuffer ra = ByteBuffer.wrap(mRA);
+        ra.order(ByteOrder.BIG_ENDIAN);
+
+        final boolean haveRaParams = (mRaParams != null);
+        boolean shouldSendRA = false;
+
+        try {
+            putHeader(ra, haveRaParams && mRaParams.hasDefaultRoute,
+                    haveRaParams ? mRaParams.hopLimit : RaParams.DEFAULT_HOPLIMIT);
+            putSlla(ra, mInterface.macAddr.toByteArray());
+            mRaLength = ra.position();
+
+            // https://tools.ietf.org/html/rfc5175#section-4 says:
+            //
+            //     "MUST NOT be added to a Router Advertisement message
+            //      if no flags in the option are set."
+            //
+            // putExpandedFlagsOption(ra);
+
+            if (haveRaParams) {
+                putMtu(ra, mRaParams.mtu);
+                mRaLength = ra.position();
+
+                for (IpPrefix ipp : mRaParams.prefixes) {
+                    putPio(ra, ipp, DEFAULT_LIFETIME, DEFAULT_LIFETIME);
+                    mRaLength = ra.position();
+                    shouldSendRA = true;
+                }
+
+                if (mRaParams.dnses.size() > 0) {
+                    putRdnss(ra, mRaParams.dnses, DEFAULT_LIFETIME);
+                    mRaLength = ra.position();
+                    shouldSendRA = true;
+                }
+            }
+
+            for (IpPrefix ipp : mDeprecatedInfoTracker.getPrefixes()) {
+                putPio(ra, ipp, 0, 0);
+                mRaLength = ra.position();
+                shouldSendRA = true;
+            }
+
+            final Set<Inet6Address> deprecatedDnses = mDeprecatedInfoTracker.getDnses();
+            if (!deprecatedDnses.isEmpty()) {
+                putRdnss(ra, deprecatedDnses, 0);
+                mRaLength = ra.position();
+                shouldSendRA = true;
+            }
+        } catch (BufferOverflowException e) {
+            // The packet up to mRaLength  is valid, since it has been updated
+            // progressively as the RA was built. Log an error, and continue
+            // on as best as possible.
+            Log.e(TAG, "Could not construct new RA: " + e);
+        }
+
+        // We have nothing worth announcing; indicate as much to maybeSendRA().
+        if (!shouldSendRA) {
+            mRaLength = 0;
+        }
+    }
+
+    private void maybeNotifyMulticastTransmitter() {
+        final MulticastTransmitter m = mMulticastTransmitter;
+        if (m != null) {
+            m.hup();
+        }
+    }
+
+    private static Inet6Address getAllNodesForScopeId(int scopeId) {
+        try {
+            return Inet6Address.getByAddress("ff02::1", ALL_NODES, scopeId);
+        } catch (UnknownHostException uhe) {
+            Log.wtf(TAG, "Failed to construct ff02::1 InetAddress: " + uhe);
+            return null;
+        }
+    }
+
+    private static byte asByte(int value) { return (byte) value; }
+    private static short asShort(int value) { return (short) value; }
+
+    private static void putHeader(ByteBuffer ra, boolean hasDefaultRoute, byte hopLimit) {
+        /**
+            Router Advertisement Message Format
+
+             0                   1                   2                   3
+             0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+            |     Type      |     Code      |          Checksum             |
+            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+            | Cur Hop Limit |M|O|H|Prf|P|R|R|       Router Lifetime         |
+            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+            |                         Reachable Time                        |
+            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+            |                          Retrans Timer                        |
+            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+            |   Options ...
+            +-+-+-+-+-+-+-+-+-+-+-+-
+        */
+        ra.put(ICMPV6_ND_ROUTER_ADVERT)
+          .put(asByte(0))
+          .putShort(asShort(0))
+          .put(hopLimit)
+          // RFC 4191 "high" preference, iff. advertising a default route.
+          .put(hasDefaultRoute ? asByte(0x08) : asByte(0))
+          .putShort(hasDefaultRoute ? asShort(DEFAULT_LIFETIME) : asShort(0))
+          .putInt(0)
+          .putInt(0);
+    }
+
+    private static void putSlla(ByteBuffer ra, byte[] slla) {
+        /**
+            Source/Target Link-layer Address
+
+             0                   1                   2                   3
+             0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+            |     Type      |    Length     |    Link-Layer Address ...
+            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+        */
+        if (slla == null || slla.length != 6) {
+            // Only IEEE 802.3 6-byte addresses are supported.
+            return;
+        }
+        final byte ND_OPTION_SLLA = 1;
+        final byte SLLA_NUM_8OCTETS = 1;
+        ra.put(ND_OPTION_SLLA)
+          .put(SLLA_NUM_8OCTETS)
+          .put(slla);
+    }
+
+    private static void putExpandedFlagsOption(ByteBuffer ra) {
+        /**
+            Router Advertisement Expanded Flags Option
+
+             0                   1                   2                   3
+             0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+            |     Type      |    Length     |         Bit fields available ..
+            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+            ... for assignment                                              |
+            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+         */
+
+        final byte ND_OPTION_EFO = 26;
+        final byte EFO_NUM_8OCTETS = 1;
+
+        ra.put(ND_OPTION_EFO)
+          .put(EFO_NUM_8OCTETS)
+          .putShort(asShort(0))
+          .putInt(0);
+    }
+
+    private static void putMtu(ByteBuffer ra, int mtu) {
+        /**
+            MTU
+
+             0                   1                   2                   3
+             0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+            |     Type      |    Length     |           Reserved            |
+            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+            |                              MTU                              |
+            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+        */
+        final byte ND_OPTION_MTU = 5;
+        final byte MTU_NUM_8OCTETS = 1;
+        ra.put(ND_OPTION_MTU)
+          .put(MTU_NUM_8OCTETS)
+          .putShort(asShort(0))
+          .putInt((mtu < IPV6_MIN_MTU) ? IPV6_MIN_MTU : mtu);
+    }
+
+    private static void putPio(ByteBuffer ra, IpPrefix ipp,
+                               int validTime, int preferredTime) {
+        /**
+            Prefix Information
+
+             0                   1                   2                   3
+             0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+            |     Type      |    Length     | Prefix Length |L|A| Reserved1 |
+            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+            |                         Valid Lifetime                        |
+            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+            |                       Preferred Lifetime                      |
+            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+            |                           Reserved2                           |
+            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+            |                                                               |
+            +                                                               +
+            |                                                               |
+            +                            Prefix                             +
+            |                                                               |
+            +                                                               +
+            |                                                               |
+            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+        */
+        final int prefixLength = ipp.getPrefixLength();
+        if (prefixLength != 64) {
+            return;
+        }
+        final byte ND_OPTION_PIO = 3;
+        final byte PIO_NUM_8OCTETS = 4;
+
+        if (validTime < 0) validTime = 0;
+        if (preferredTime < 0) preferredTime = 0;
+        if (preferredTime > validTime) preferredTime = validTime;
+
+        final byte[] addr = ipp.getAddress().getAddress();
+        ra.put(ND_OPTION_PIO)
+          .put(PIO_NUM_8OCTETS)
+          .put(asByte(prefixLength))
+          .put(asByte(0xc0)) /* L & A set */
+          .putInt(validTime)
+          .putInt(preferredTime)
+          .putInt(0)
+          .put(addr);
+    }
+
+    private static void putRio(ByteBuffer ra, IpPrefix ipp) {
+        /**
+            Route Information Option
+
+             0                   1                   2                   3
+             0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+            |     Type      |    Length     | Prefix Length |Resvd|Prf|Resvd|
+            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+            |                        Route Lifetime                         |
+            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+            |                   Prefix (Variable Length)                    |
+            .                                                               .
+            .                                                               .
+            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+         */
+        final int prefixLength = ipp.getPrefixLength();
+        if (prefixLength > 64) {
+            return;
+        }
+        final byte ND_OPTION_RIO = 24;
+        final byte RIO_NUM_8OCTETS = asByte(
+                (prefixLength == 0) ? 1 : (prefixLength <= 8) ? 2 : 3);
+
+        final byte[] addr = ipp.getAddress().getAddress();
+        ra.put(ND_OPTION_RIO)
+          .put(RIO_NUM_8OCTETS)
+          .put(asByte(prefixLength))
+          .put(asByte(0x18))
+          .putInt(DEFAULT_LIFETIME);
+
+        // Rely upon an IpPrefix's address being properly zeroed.
+        if (prefixLength > 0) {
+            ra.put(addr, 0, (prefixLength <= 64) ? 8 : 16);
+        }
+    }
+
+    private static void putRdnss(ByteBuffer ra, Set<Inet6Address> dnses, int lifetime) {
+        /**
+            Recursive DNS Server (RDNSS) Option
+
+             0                   1                   2                   3
+             0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+            |     Type      |     Length    |           Reserved            |
+            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+            |                           Lifetime                            |
+            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+            |                                                               |
+            :            Addresses of IPv6 Recursive DNS Servers            :
+            |                                                               |
+            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
+         */
+
+        final HashSet<Inet6Address> filteredDnses = new HashSet<>();
+        for (Inet6Address dns : dnses) {
+            if ((new LinkAddress(dns, RFC7421_PREFIX_LENGTH)).isGlobalPreferred()) {
+                filteredDnses.add(dns);
+            }
+        }
+        if (filteredDnses.isEmpty()) return;
+
+        final byte ND_OPTION_RDNSS = 25;
+        final byte RDNSS_NUM_8OCTETS = asByte(dnses.size() * 2 + 1);
+        ra.put(ND_OPTION_RDNSS)
+          .put(RDNSS_NUM_8OCTETS)
+          .putShort(asShort(0))
+          .putInt(lifetime);
+
+        for (Inet6Address dns : filteredDnses) {
+            // NOTE: If the full of list DNS servers doesn't fit in the packet,
+            // this code will cause a buffer overflow and the RA won't include
+            // this instance of the option at all.
+            //
+            // TODO: Consider looking at ra.remaining() to determine how many
+            // DNS servers will fit, and adding only those.
+            ra.put(dns.getAddress());
+        }
+    }
+
+    private boolean createSocket() {
+        final int SEND_TIMEOUT_MS = 300;
+
+        final int oldTag = TrafficStats.getAndSetThreadStatsTag(
+                TrafficStatsConstants.TAG_SYSTEM_NEIGHBOR);
+        try {
+            mSocket = Os.socket(AF_INET6, SOCK_RAW, IPPROTO_ICMPV6);
+            // Setting SNDTIMEO is purely for defensive purposes.
+            Os.setsockoptTimeval(
+                    mSocket, SOL_SOCKET, SO_SNDTIMEO, StructTimeval.fromMillis(SEND_TIMEOUT_MS));
+            Os.setsockoptIfreq(mSocket, SOL_SOCKET, SO_BINDTODEVICE, mInterface.name);
+            NetworkUtils.protectFromVpn(mSocket);
+            NetworkUtils.setupRaSocket(mSocket, mInterface.index);
+        } catch (ErrnoException | IOException e) {
+            Log.e(TAG, "Failed to create RA daemon socket: " + e);
+            return false;
+        } finally {
+            TrafficStats.setThreadStatsTag(oldTag);
+        }
+
+        return true;
+    }
+
+    private void closeSocket() {
+        if (mSocket != null) {
+            try {
+                IoBridge.closeAndSignalBlockedThreads(mSocket);
+            } catch (IOException ignored) {}
+        }
+        mSocket = null;
+    }
+
+    private boolean isSocketValid() {
+        final FileDescriptor s = mSocket;
+        return (s != null) && s.valid();
+    }
+
+    private boolean isSuitableDestination(InetSocketAddress dest) {
+        if (mAllNodes.equals(dest)) {
+            return true;
+        }
+
+        final InetAddress destip = dest.getAddress();
+        return (destip instanceof Inet6Address) &&
+                destip.isLinkLocalAddress() &&
+               (((Inet6Address) destip).getScopeId() == mInterface.index);
+    }
+
+    private void maybeSendRA(InetSocketAddress dest) {
+        if (dest == null || !isSuitableDestination(dest)) {
+            dest = mAllNodes;
+        }
+
+        try {
+            synchronized (mLock) {
+                if (mRaLength < MIN_RA_HEADER_SIZE) {
+                    // No actual RA to send.
+                    return;
+                }
+                Os.sendto(mSocket, mRA, 0, mRaLength, 0, dest);
+            }
+            Log.d(TAG, "RA sendto " + dest.getAddress().getHostAddress());
+        } catch (ErrnoException | SocketException e) {
+            if (isSocketValid()) {
+                Log.e(TAG, "sendto error: " + e);
+            }
+        }
+    }
+
+    private final class UnicastResponder extends Thread {
+        private final InetSocketAddress solicitor = new InetSocketAddress();
+        // The recycled buffer for receiving Router Solicitations from clients.
+        // If the RS is larger than IPV6_MIN_MTU the packets are truncated.
+        // This is fine since currently only byte 0 is examined anyway.
+        private final byte mSolication[] = new byte[IPV6_MIN_MTU];
+
+        @Override
+        public void run() {
+            while (isSocketValid()) {
+                try {
+                    // Blocking receive.
+                    final int rval = Os.recvfrom(
+                            mSocket, mSolication, 0, mSolication.length, 0, solicitor);
+                    // Do the least possible amount of validation.
+                    if (rval < 1 || mSolication[0] != ICMPV6_ND_ROUTER_SOLICIT) {
+                        continue;
+                    }
+                } catch (ErrnoException | SocketException e) {
+                    if (isSocketValid()) {
+                        Log.e(TAG, "recvfrom error: " + e);
+                    }
+                    continue;
+                }
+
+                maybeSendRA(solicitor);
+            }
+        }
+    }
+
+    // TODO: Consider moving this to run on a provided Looper as a Handler,
+    // with WakeupMessage-style messages providing the timer driven input.
+    private final class MulticastTransmitter extends Thread {
+        private final Random mRandom = new Random();
+        private final AtomicInteger mUrgentAnnouncements = new AtomicInteger(0);
+
+        @Override
+        public void run() {
+            while (isSocketValid()) {
+                try {
+                    Thread.sleep(getNextMulticastTransmitDelayMs());
+                } catch (InterruptedException ignored) {
+                    // Stop sleeping, immediately send an RA, and continue.
+                }
+
+                maybeSendRA(mAllNodes);
+                synchronized (mLock) {
+                    if (mDeprecatedInfoTracker.decrementCounters()) {
+                        // At least one deprecated PIO has been removed;
+                        // reassemble the RA.
+                        assembleRaLocked();
+                    }
+                }
+            }
+        }
+
+        public void hup() {
+            // Set to one fewer that the desired number, because as soon as
+            // the thread interrupt is processed we immediately send an RA
+            // and mUrgentAnnouncements is not examined until the subsequent
+            // sleep interval computation (i.e. this way we send 3 and not 4).
+            mUrgentAnnouncements.set(MAX_URGENT_RTR_ADVERTISEMENTS - 1);
+            interrupt();
+        }
+
+        private int getNextMulticastTransmitDelaySec() {
+            boolean deprecationInProgress = false;
+            synchronized (mLock) {
+                if (mRaLength < MIN_RA_HEADER_SIZE) {
+                    // No actual RA to send; just sleep for 1 day.
+                    return DAY_IN_SECONDS;
+                }
+                deprecationInProgress = !mDeprecatedInfoTracker.isEmpty();
+            }
+
+            final int urgentPending = mUrgentAnnouncements.getAndDecrement();
+            if ((urgentPending > 0) || deprecationInProgress) {
+                return MIN_DELAY_BETWEEN_RAS_SEC;
+            }
+
+            return MIN_RTR_ADV_INTERVAL_SEC + mRandom.nextInt(
+                    MAX_RTR_ADV_INTERVAL_SEC - MIN_RTR_ADV_INTERVAL_SEC);
+        }
+
+        private long getNextMulticastTransmitDelayMs() {
+            return 1000 * (long) getNextMulticastTransmitDelaySec();
+        }
+    }
+}
diff --git a/android/net/ipmemorystore/NetworkAttributes.java b/android/net/ipmemorystore/NetworkAttributes.java
new file mode 100644
index 0000000..818515a
--- /dev/null
+++ b/android/net/ipmemorystore/NetworkAttributes.java
@@ -0,0 +1,371 @@
+/*
+ * Copyright (C) 2018 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.ipmemorystore;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.StringJoiner;
+
+/**
+ * A POD object to represent attributes of a single L2 network entry.
+ * @hide
+ */
+public class NetworkAttributes {
+    private static final boolean DBG = true;
+
+    // Weight cutoff for grouping. To group, a similarity score is computed with the following
+    // algorithm : if both fields are non-null and equals() then add their assigned weight, else if
+    // both are null then add a portion of their assigned weight (see NULL_MATCH_WEIGHT),
+    // otherwise add nothing.
+    // As a guideline, this should be something like 60~75% of the total weights in this class. The
+    // design states "in essence a reader should imagine that if two important columns don't match,
+    // or one important and several unimportant columns don't match then the two records are
+    // considered a different group".
+    private static final float TOTAL_WEIGHT_CUTOFF = 520.0f;
+    // The portion of the weight that is earned when scoring group-sameness by having both columns
+    // being null. This is because some networks rightfully don't have some attributes (e.g. a
+    // V6-only network won't have an assigned V4 address) and both being null should count for
+    // something, but attributes may also be null just because data is unavailable.
+    private static final float NULL_MATCH_WEIGHT = 0.25f;
+
+    // The v4 address that was assigned to this device the last time it joined this network.
+    // This typically comes from DHCP but could be something else like static configuration.
+    // This does not apply to IPv6.
+    // TODO : add a list of v6 prefixes for the v6 case.
+    @Nullable
+    public final Inet4Address assignedV4Address;
+    private static final float WEIGHT_ASSIGNEDV4ADDR = 300.0f;
+
+    // The lease expiry timestamp of v4 address allocated from DHCP server, in milliseconds.
+    @Nullable
+    public final Long assignedV4AddressExpiry;
+    // lease expiry doesn't imply any correlation between "the same lease expiry value" and "the
+    // same L3 network".
+    private static final float WEIGHT_ASSIGNEDV4ADDREXPIRY = 0.0f;
+
+    // Optionally supplied by the client if it has an opinion on L3 network. For example, this
+    // could be a hash of the SSID + security type on WiFi.
+    @Nullable
+    public final String groupHint;
+    private static final float WEIGHT_GROUPHINT = 300.0f;
+
+    // The list of DNS server addresses.
+    @Nullable
+    public final List<InetAddress> dnsAddresses;
+    private static final float WEIGHT_DNSADDRESSES = 200.0f;
+
+    // The mtu on this network.
+    @Nullable
+    public final Integer mtu;
+    private static final float WEIGHT_MTU = 50.0f;
+
+    // The sum of all weights in this class. Tests ensure that this stays equal to the total of
+    // all weights.
+    /** @hide */
+    @VisibleForTesting
+    public static final float TOTAL_WEIGHT = WEIGHT_ASSIGNEDV4ADDR
+            + WEIGHT_ASSIGNEDV4ADDREXPIRY
+            + WEIGHT_GROUPHINT
+            + WEIGHT_DNSADDRESSES
+            + WEIGHT_MTU;
+
+    /** @hide */
+    @VisibleForTesting
+    public NetworkAttributes(
+            @Nullable final Inet4Address assignedV4Address,
+            @Nullable final Long assignedV4AddressExpiry,
+            @Nullable final String groupHint,
+            @Nullable final List<InetAddress> dnsAddresses,
+            @Nullable final Integer mtu) {
+        if (mtu != null && mtu < 0) throw new IllegalArgumentException("MTU can't be negative");
+        if (assignedV4AddressExpiry != null && assignedV4AddressExpiry <= 0) {
+            throw new IllegalArgumentException("lease expiry can't be negative or zero");
+        }
+        this.assignedV4Address = assignedV4Address;
+        this.assignedV4AddressExpiry = assignedV4AddressExpiry;
+        this.groupHint = groupHint;
+        this.dnsAddresses = null == dnsAddresses ? null :
+                Collections.unmodifiableList(new ArrayList<>(dnsAddresses));
+        this.mtu = mtu;
+    }
+
+    @VisibleForTesting
+    public NetworkAttributes(@NonNull final NetworkAttributesParcelable parcelable) {
+        // The call to the other constructor must be the first statement of this constructor,
+        // so everything has to be inline
+        this((Inet4Address) getByAddressOrNull(parcelable.assignedV4Address),
+                parcelable.assignedV4AddressExpiry > 0
+                        ? parcelable.assignedV4AddressExpiry : null,
+                parcelable.groupHint,
+                blobArrayToInetAddressList(parcelable.dnsAddresses),
+                parcelable.mtu >= 0 ? parcelable.mtu : null);
+    }
+
+    @Nullable
+    private static InetAddress getByAddressOrNull(@Nullable final byte[] address) {
+        if (null == address) return null;
+        try {
+            return InetAddress.getByAddress(address);
+        } catch (UnknownHostException e) {
+            return null;
+        }
+    }
+
+    @Nullable
+    private static List<InetAddress> blobArrayToInetAddressList(@Nullable final Blob[] blobs) {
+        if (null == blobs) return null;
+        final ArrayList<InetAddress> list = new ArrayList<>(blobs.length);
+        for (final Blob b : blobs) {
+            final InetAddress addr = getByAddressOrNull(b.data);
+            if (null != addr) list.add(addr);
+        }
+        return list;
+    }
+
+    @Nullable
+    private static Blob[] inetAddressListToBlobArray(@Nullable final List<InetAddress> addresses) {
+        if (null == addresses) return null;
+        final ArrayList<Blob> blobs = new ArrayList<>();
+        for (int i = 0; i < addresses.size(); ++i) {
+            final InetAddress addr = addresses.get(i);
+            if (null == addr) continue;
+            final Blob b = new Blob();
+            b.data = addr.getAddress();
+            blobs.add(b);
+        }
+        return blobs.toArray(new Blob[0]);
+    }
+
+    /** Converts this NetworkAttributes to a parcelable object */
+    @NonNull
+    public NetworkAttributesParcelable toParcelable() {
+        final NetworkAttributesParcelable parcelable = new NetworkAttributesParcelable();
+        parcelable.assignedV4Address =
+                (null == assignedV4Address) ? null : assignedV4Address.getAddress();
+        parcelable.assignedV4AddressExpiry =
+                (null == assignedV4AddressExpiry) ? 0 : assignedV4AddressExpiry;
+        parcelable.groupHint = groupHint;
+        parcelable.dnsAddresses = inetAddressListToBlobArray(dnsAddresses);
+        parcelable.mtu = (null == mtu) ? -1 : mtu;
+        return parcelable;
+    }
+
+    private float samenessContribution(final float weight,
+            @Nullable final Object o1, @Nullable final Object o2) {
+        if (null == o1) {
+            return (null == o2) ? weight * NULL_MATCH_WEIGHT : 0f;
+        }
+        return Objects.equals(o1, o2) ? weight : 0f;
+    }
+
+    /** @hide */
+    public float getNetworkGroupSamenessConfidence(@NonNull final NetworkAttributes o) {
+        final float samenessScore =
+                samenessContribution(WEIGHT_ASSIGNEDV4ADDR, assignedV4Address, o.assignedV4Address)
+                + samenessContribution(WEIGHT_ASSIGNEDV4ADDREXPIRY, assignedV4AddressExpiry,
+                      o.assignedV4AddressExpiry)
+                + samenessContribution(WEIGHT_GROUPHINT, groupHint, o.groupHint)
+                + samenessContribution(WEIGHT_DNSADDRESSES, dnsAddresses, o.dnsAddresses)
+                + samenessContribution(WEIGHT_MTU, mtu, o.mtu);
+        // The minimum is 0, the max is TOTAL_WEIGHT and should be represented by 1.0, and
+        // TOTAL_WEIGHT_CUTOFF should represent 0.5, but there is no requirement that
+        // TOTAL_WEIGHT_CUTOFF would be half of TOTAL_WEIGHT (indeed, it should not be).
+        // So scale scores under the cutoff between 0 and 0.5, and the scores over the cutoff
+        // between 0.5 and 1.0.
+        if (samenessScore < TOTAL_WEIGHT_CUTOFF) {
+            return samenessScore / (TOTAL_WEIGHT_CUTOFF * 2);
+        } else {
+            return (samenessScore - TOTAL_WEIGHT_CUTOFF) / (TOTAL_WEIGHT - TOTAL_WEIGHT_CUTOFF) / 2
+                    + 0.5f;
+        }
+    }
+
+    /** @hide */
+    public static class Builder {
+        @Nullable
+        private Inet4Address mAssignedAddress;
+        @Nullable
+        private Long mAssignedAddressExpiry;
+        @Nullable
+        private String mGroupHint;
+        @Nullable
+        private List<InetAddress> mDnsAddresses;
+        @Nullable
+        private Integer mMtu;
+
+        /**
+         * Set the assigned address.
+         * @param assignedV4Address The assigned address.
+         * @return This builder.
+         */
+        public Builder setAssignedV4Address(@Nullable final Inet4Address assignedV4Address) {
+            mAssignedAddress = assignedV4Address;
+            return this;
+        }
+
+        /**
+         * Set the lease expiry timestamp of assigned v4 address. Long.MAX_VALUE is used
+         * to represent "infinite lease".
+         *
+         * @param assignedV4AddressExpiry The lease expiry timestamp of assigned v4 address.
+         * @return This builder.
+         */
+        public Builder setAssignedV4AddressExpiry(
+                @Nullable final Long assignedV4AddressExpiry) {
+            if (null != assignedV4AddressExpiry && assignedV4AddressExpiry <= 0) {
+                throw new IllegalArgumentException("lease expiry can't be negative or zero");
+            }
+            mAssignedAddressExpiry = assignedV4AddressExpiry;
+            return this;
+        }
+
+        /**
+         * Set the group hint.
+         * @param groupHint The group hint.
+         * @return This builder.
+         */
+        public Builder setGroupHint(@Nullable final String groupHint) {
+            mGroupHint = groupHint;
+            return this;
+        }
+
+        /**
+         * Set the DNS addresses.
+         * @param dnsAddresses The DNS addresses.
+         * @return This builder.
+         */
+        public Builder setDnsAddresses(@Nullable final List<InetAddress> dnsAddresses) {
+            if (DBG && null != dnsAddresses) {
+                // Parceling code crashes if one of the addresses is null, therefore validate
+                // them when running in debug.
+                for (final InetAddress address : dnsAddresses) {
+                    if (null == address) throw new IllegalArgumentException("Null DNS address");
+                }
+            }
+            this.mDnsAddresses = dnsAddresses;
+            return this;
+        }
+
+        /**
+         * Set the MTU.
+         * @param mtu The MTU.
+         * @return This builder.
+         */
+        public Builder setMtu(@Nullable final Integer mtu) {
+            if (null != mtu && mtu < 0) throw new IllegalArgumentException("MTU can't be negative");
+            mMtu = mtu;
+            return this;
+        }
+
+        /**
+         * Return the built NetworkAttributes object.
+         * @return The built NetworkAttributes object.
+         */
+        public NetworkAttributes build() {
+            return new NetworkAttributes(mAssignedAddress, mAssignedAddressExpiry,
+                  mGroupHint, mDnsAddresses, mMtu);
+        }
+    }
+
+    /** @hide */
+    public boolean isEmpty() {
+        return (null == assignedV4Address) && (null == assignedV4AddressExpiry)
+                && (null == groupHint) && (null == dnsAddresses) && (null == mtu);
+    }
+
+    @Override
+    public boolean equals(@Nullable final Object o) {
+        if (!(o instanceof NetworkAttributes)) return false;
+        final NetworkAttributes other = (NetworkAttributes) o;
+        return Objects.equals(assignedV4Address, other.assignedV4Address)
+                && Objects.equals(assignedV4AddressExpiry, other.assignedV4AddressExpiry)
+                && Objects.equals(groupHint, other.groupHint)
+                && Objects.equals(dnsAddresses, other.dnsAddresses)
+                && Objects.equals(mtu, other.mtu);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(assignedV4Address, assignedV4AddressExpiry,
+                groupHint, dnsAddresses, mtu);
+    }
+
+    /** Pretty print */
+    @Override
+    public String toString() {
+        final StringJoiner resultJoiner = new StringJoiner(" ", "{", "}");
+        final ArrayList<String> nullFields = new ArrayList<>();
+
+        if (null != assignedV4Address) {
+            resultJoiner.add("assignedV4Addr :");
+            resultJoiner.add(assignedV4Address.toString());
+        } else {
+            nullFields.add("assignedV4Addr");
+        }
+
+        if (null != assignedV4AddressExpiry) {
+            resultJoiner.add("assignedV4AddressExpiry :");
+            resultJoiner.add(assignedV4AddressExpiry.toString());
+        } else {
+            nullFields.add("assignedV4AddressExpiry");
+        }
+
+        if (null != groupHint) {
+            resultJoiner.add("groupHint :");
+            resultJoiner.add(groupHint);
+        } else {
+            nullFields.add("groupHint");
+        }
+
+        if (null != dnsAddresses) {
+            resultJoiner.add("dnsAddr : [");
+            for (final InetAddress addr : dnsAddresses) {
+                resultJoiner.add(addr.getHostAddress());
+            }
+            resultJoiner.add("]");
+        } else {
+            nullFields.add("dnsAddr");
+        }
+
+        if (null != mtu) {
+            resultJoiner.add("mtu :");
+            resultJoiner.add(mtu.toString());
+        } else {
+            nullFields.add("mtu");
+        }
+
+        if (!nullFields.isEmpty()) {
+            resultJoiner.add("; Null fields : [");
+            for (final String field : nullFields) {
+                resultJoiner.add(field);
+            }
+            resultJoiner.add("]");
+        }
+
+        return resultJoiner.toString();
+    }
+}
diff --git a/android/net/ipmemorystore/OnBlobRetrievedListener.java b/android/net/ipmemorystore/OnBlobRetrievedListener.java
new file mode 100644
index 0000000..a17483a
--- /dev/null
+++ b/android/net/ipmemorystore/OnBlobRetrievedListener.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2019 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.ipmemorystore;
+
+import android.annotation.NonNull;
+
+/**
+ * A listener for the IpMemoryStore to return a blob.
+ * @hide
+ */
+public interface OnBlobRetrievedListener {
+    /**
+     * The memory store has come up with the answer to a query that was sent.
+     */
+    void onBlobRetrieved(Status status, String l2Key, String name, Blob blob);
+
+    /** Converts this OnBlobRetrievedListener to a parcelable object */
+    @NonNull
+    static IOnBlobRetrievedListener toAIDL(@NonNull final OnBlobRetrievedListener listener) {
+        return new IOnBlobRetrievedListener.Stub() {
+            @Override
+            public void onBlobRetrieved(final StatusParcelable statusParcelable, final String l2Key,
+                    final String name, final Blob blob) {
+                // NonNull, but still don't crash the system server if null
+                if (null != listener) {
+                    listener.onBlobRetrieved(new Status(statusParcelable), l2Key, name, blob);
+                }
+            }
+
+            @Override
+            public int getInterfaceVersion() {
+                return this.VERSION;
+            }
+        };
+    }
+}
diff --git a/android/net/ipmemorystore/OnL2KeyResponseListener.java b/android/net/ipmemorystore/OnL2KeyResponseListener.java
new file mode 100644
index 0000000..e608aec
--- /dev/null
+++ b/android/net/ipmemorystore/OnL2KeyResponseListener.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2019 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.ipmemorystore;
+
+import android.annotation.NonNull;
+
+/**
+ * A listener for the IpMemoryStore to return a L2 key.
+ * @hide
+ */
+public interface OnL2KeyResponseListener {
+    /**
+     * The operation has completed with the specified status.
+     */
+    void onL2KeyResponse(Status status, String l2Key);
+
+    /** Converts this OnL2KeyResponseListener to a parcelable object */
+    @NonNull
+    static IOnL2KeyResponseListener toAIDL(@NonNull final OnL2KeyResponseListener listener) {
+        return new IOnL2KeyResponseListener.Stub() {
+            @Override
+            public void onL2KeyResponse(final StatusParcelable statusParcelable,
+                    final String l2Key) {
+                // NonNull, but still don't crash the system server if null
+                if (null != listener) {
+                    listener.onL2KeyResponse(new Status(statusParcelable), l2Key);
+                }
+            }
+
+            @Override
+            public int getInterfaceVersion() {
+                return this.VERSION;
+            }
+        };
+    }
+}
diff --git a/android/net/ipmemorystore/OnNetworkAttributesRetrievedListener.java b/android/net/ipmemorystore/OnNetworkAttributesRetrievedListener.java
new file mode 100644
index 0000000..395ad98
--- /dev/null
+++ b/android/net/ipmemorystore/OnNetworkAttributesRetrievedListener.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2019 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.ipmemorystore;
+
+import android.annotation.NonNull;
+
+/**
+ * A listener for the IpMemoryStore to return network attributes.
+ * @hide
+ */
+public interface OnNetworkAttributesRetrievedListener {
+    /**
+     * The memory store has come up with the answer to a query that was sent.
+     */
+    void onNetworkAttributesRetrieved(Status status, String l2Key, NetworkAttributes attributes);
+
+    /** Converts this OnNetworkAttributesRetrievedListener to a parcelable object */
+    @NonNull
+    static IOnNetworkAttributesRetrievedListener toAIDL(
+            @NonNull final OnNetworkAttributesRetrievedListener listener) {
+        return new IOnNetworkAttributesRetrievedListener.Stub() {
+            @Override
+            public void onNetworkAttributesRetrieved(final StatusParcelable statusParcelable,
+                    final String l2Key,
+                    final NetworkAttributesParcelable networkAttributesParcelable) {
+                // NonNull, but still don't crash the system server if null
+                if (null != listener) {
+                    listener.onNetworkAttributesRetrieved(
+                            new Status(statusParcelable), l2Key, null == networkAttributesParcelable
+                                ? null : new NetworkAttributes(networkAttributesParcelable));
+                }
+            }
+
+            @Override
+            public int getInterfaceVersion() {
+                return this.VERSION;
+            }
+        };
+    }
+}
diff --git a/android/net/ipmemorystore/OnSameL3NetworkResponseListener.java b/android/net/ipmemorystore/OnSameL3NetworkResponseListener.java
new file mode 100644
index 0000000..67f8da8
--- /dev/null
+++ b/android/net/ipmemorystore/OnSameL3NetworkResponseListener.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2019 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.ipmemorystore;
+
+import android.annotation.NonNull;
+
+/**
+ * A listener for the IpMemoryStore to return a response about network sameness.
+ * @hide
+ */
+public interface OnSameL3NetworkResponseListener {
+    /**
+     * The memory store has come up with the answer to a query that was sent.
+     */
+    void onSameL3NetworkResponse(Status status, SameL3NetworkResponse response);
+
+    /** Converts this OnSameL3NetworkResponseListener to a parcelable object */
+    @NonNull
+    static IOnSameL3NetworkResponseListener toAIDL(
+            @NonNull final OnSameL3NetworkResponseListener listener) {
+        return new IOnSameL3NetworkResponseListener.Stub() {
+            @Override
+            public void onSameL3NetworkResponse(final StatusParcelable statusParcelable,
+                    final SameL3NetworkResponseParcelable sameL3NetworkResponseParcelable) {
+                // NonNull, but still don't crash the system server if null
+                if (null != listener) {
+                    listener.onSameL3NetworkResponse(
+                            new Status(statusParcelable),
+                            new SameL3NetworkResponse(sameL3NetworkResponseParcelable));
+                }
+            }
+
+            @Override
+            public int getInterfaceVersion() {
+                return this.VERSION;
+            }
+        };
+    }
+}
diff --git a/android/net/ipmemorystore/OnStatusListener.java b/android/net/ipmemorystore/OnStatusListener.java
new file mode 100644
index 0000000..4262efd
--- /dev/null
+++ b/android/net/ipmemorystore/OnStatusListener.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2019 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.ipmemorystore;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+/**
+ * A listener for the IpMemoryStore to return a status to a client.
+ * @hide
+ */
+public interface OnStatusListener {
+    /**
+     * The operation has completed with the specified status.
+     */
+    void onComplete(Status status);
+
+    /** Converts this OnStatusListener to a parcelable object */
+    @NonNull
+    static IOnStatusListener toAIDL(@Nullable final OnStatusListener listener) {
+        return new IOnStatusListener.Stub() {
+            @Override
+            public void onComplete(final StatusParcelable statusParcelable) {
+                if (null != listener) {
+                    listener.onComplete(new Status(statusParcelable));
+                }
+            }
+
+            @Override
+            public int getInterfaceVersion() {
+                return this.VERSION;
+            }
+        };
+    }
+}
diff --git a/android/net/ipmemorystore/SameL3NetworkResponse.java b/android/net/ipmemorystore/SameL3NetworkResponse.java
new file mode 100644
index 0000000..291aca8
--- /dev/null
+++ b/android/net/ipmemorystore/SameL3NetworkResponse.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2018 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.ipmemorystore;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * An object representing the answer to a query whether two given L2 networks represent the
+ * same L3 network. Parcels as a SameL3NetworkResponseParceled object.
+ * @hide
+ */
+public class SameL3NetworkResponse {
+    @IntDef(prefix = "NETWORK_",
+            value = {NETWORK_SAME, NETWORK_DIFFERENT, NETWORK_NEVER_CONNECTED})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface NetworkSameness {}
+
+    /**
+     * Both L2 networks represent the same L3 network.
+     */
+    public static final int NETWORK_SAME = 1;
+
+    /**
+     * The two L2 networks represent a different L3 network.
+     */
+    public static final int NETWORK_DIFFERENT = 2;
+
+    /**
+     * The device has never connected to at least one of these two L2 networks, or data
+     * has been wiped. Therefore the device has never seen the L3 network behind at least
+     * one of these two L2 networks, and can't evaluate whether it's the same as the other.
+     */
+    public static final int NETWORK_NEVER_CONNECTED = 3;
+
+    /**
+     * The first L2 key specified in the query.
+     */
+    @NonNull
+    public final String l2Key1;
+
+    /**
+     * The second L2 key specified in the query.
+     */
+    @NonNull
+    public final String l2Key2;
+
+    /**
+     * A confidence value indicating whether the two L2 networks represent the same L3 network.
+     *
+     * If both L2 networks were known, this value will be between 0.0 and 1.0, with 0.0
+     * representing complete confidence that the given L2 networks represent a different
+     * L3 network, and 1.0 representing complete confidence that the given L2 networks
+     * represent the same L3 network.
+     * If at least one of the L2 networks was not known, this value will be outside of the
+     * 0.0~1.0 range.
+     *
+     * Most apps should not be interested in this, and are encouraged to use the collapsing
+     * {@link #getNetworkSameness()} function below.
+     */
+    public final float confidence;
+
+    /**
+     * @return whether the two L2 networks represent the same L3 network. Either
+     *     {@code NETWORK_SAME}, {@code NETWORK_DIFFERENT} or {@code NETWORK_NEVER_CONNECTED}.
+     */
+    @NetworkSameness
+    public final int getNetworkSameness() {
+        if (confidence > 1.0 || confidence < 0.0) return NETWORK_NEVER_CONNECTED;
+        return confidence > 0.5 ? NETWORK_SAME : NETWORK_DIFFERENT;
+    }
+
+    /** @hide */
+    public SameL3NetworkResponse(@NonNull final String l2Key1, @NonNull final String l2Key2,
+            final float confidence) {
+        this.l2Key1 = l2Key1;
+        this.l2Key2 = l2Key2;
+        this.confidence = confidence;
+    }
+
+    /** Builds a SameL3NetworkResponse from a parcelable object */
+    @VisibleForTesting
+    public SameL3NetworkResponse(@NonNull final SameL3NetworkResponseParcelable parceled) {
+        this(parceled.l2Key1, parceled.l2Key2, parceled.confidence);
+    }
+
+    /** Converts this SameL3NetworkResponse to a parcelable object */
+    @NonNull
+    public SameL3NetworkResponseParcelable toParcelable() {
+        final SameL3NetworkResponseParcelable parcelable = new SameL3NetworkResponseParcelable();
+        parcelable.l2Key1 = l2Key1;
+        parcelable.l2Key2 = l2Key2;
+        parcelable.confidence = confidence;
+        return parcelable;
+    }
+
+    // Note key1 and key2 have to match each other for this to return true. If
+    // key1 matches o.key2 and the other way around this returns false.
+    @Override
+    public boolean equals(@Nullable final Object o) {
+        if (!(o instanceof SameL3NetworkResponse)) return false;
+        final SameL3NetworkResponse other = (SameL3NetworkResponse) o;
+        return l2Key1.equals(other.l2Key1) && l2Key2.equals(other.l2Key2)
+                && confidence == other.confidence;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(l2Key1, l2Key2, confidence);
+    }
+
+    @Override
+    /** Pretty print */
+    public String toString() {
+        switch (getNetworkSameness()) {
+            case NETWORK_SAME:
+                return "\"" + l2Key1 + "\" same L3 network as \"" + l2Key2 + "\"";
+            case NETWORK_DIFFERENT:
+                return "\"" + l2Key1 + "\" different L3 network from \"" + l2Key2 + "\"";
+            case NETWORK_NEVER_CONNECTED:
+                return "\"" + l2Key1 + "\" can't be tested against \"" + l2Key2 + "\"";
+            default:
+                return "Buggy sameness value ? \"" + l2Key1 + "\", \"" + l2Key2 + "\"";
+        }
+    }
+}
diff --git a/android/net/ipmemorystore/Status.java b/android/net/ipmemorystore/Status.java
new file mode 100644
index 0000000..13242c0
--- /dev/null
+++ b/android/net/ipmemorystore/Status.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright (C) 2018 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.ipmemorystore;
+
+import android.annotation.NonNull;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * A parcelable status representing the result of an operation.
+ * Parcels as StatusParceled.
+ * @hide
+ */
+public class Status {
+    public static final int SUCCESS = 0;
+
+    public static final int ERROR_GENERIC = -1;
+    public static final int ERROR_ILLEGAL_ARGUMENT = -2;
+    public static final int ERROR_DATABASE_CANNOT_BE_OPENED = -3;
+    public static final int ERROR_STORAGE = -4;
+    public static final int ERROR_UNKNOWN = -5;
+
+    public final int resultCode;
+
+    public Status(final int resultCode) {
+        this.resultCode = resultCode;
+    }
+
+    @VisibleForTesting
+    public Status(@NonNull final StatusParcelable parcelable) {
+        this(parcelable.resultCode);
+    }
+
+    /** Converts this Status to a parcelable object */
+    @NonNull
+    public StatusParcelable toParcelable() {
+        final StatusParcelable parcelable = new StatusParcelable();
+        parcelable.resultCode = resultCode;
+        return parcelable;
+    }
+
+    public boolean isSuccess() {
+        return SUCCESS == resultCode;
+    }
+
+    /** Pretty print */
+    @Override
+    public String toString() {
+        switch (resultCode) {
+            case SUCCESS: return "SUCCESS";
+            case ERROR_GENERIC: return "GENERIC ERROR";
+            case ERROR_ILLEGAL_ARGUMENT: return "ILLEGAL ARGUMENT";
+            case ERROR_DATABASE_CANNOT_BE_OPENED: return "DATABASE CANNOT BE OPENED";
+            // "DB storage error" is not very helpful but SQLite does not provide specific error
+            // codes upon store failure. Thus this indicates SQLite returned some error upon store
+            case ERROR_STORAGE: return "DATABASE STORAGE ERROR";
+            default: return "Unknown value ?!";
+        }
+    }
+}
diff --git a/android/net/lowpan/InterfaceDisabledException.java b/android/net/lowpan/InterfaceDisabledException.java
new file mode 100644
index 0000000..e917d45
--- /dev/null
+++ b/android/net/lowpan/InterfaceDisabledException.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.lowpan;
+
+/**
+ * Exception indicating this operation requires the interface to be enabled.
+ *
+ * @see LowpanInterface
+ * @hide
+ */
+// @SystemApi
+public class InterfaceDisabledException extends LowpanException {
+
+    public InterfaceDisabledException() {}
+
+    public InterfaceDisabledException(String message) {
+        super(message);
+    }
+
+    public InterfaceDisabledException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    protected InterfaceDisabledException(Exception cause) {
+        super(cause);
+    }
+}
diff --git a/android/net/lowpan/JoinFailedAtAuthException.java b/android/net/lowpan/JoinFailedAtAuthException.java
new file mode 100644
index 0000000..7aceb71
--- /dev/null
+++ b/android/net/lowpan/JoinFailedAtAuthException.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.lowpan;
+
+/**
+ * Exception indicating the join operation was unable to find the given network.
+ *
+ * @see LowpanInterface
+ * @hide
+ */
+// @SystemApi
+public class JoinFailedAtAuthException extends JoinFailedException {
+
+    public JoinFailedAtAuthException() {}
+
+    public JoinFailedAtAuthException(String message) {
+        super(message);
+    }
+
+    public JoinFailedAtAuthException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public JoinFailedAtAuthException(Exception cause) {
+        super(cause);
+    }
+}
diff --git a/android/net/lowpan/JoinFailedAtScanException.java b/android/net/lowpan/JoinFailedAtScanException.java
new file mode 100644
index 0000000..a4346f9
--- /dev/null
+++ b/android/net/lowpan/JoinFailedAtScanException.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.lowpan;
+
+/**
+ * Exception indicating the join operation was unable to find the given network.
+ *
+ * @see LowpanInterface
+ * @hide
+ */
+// @SystemApi
+public class JoinFailedAtScanException extends JoinFailedException {
+
+    public JoinFailedAtScanException() {}
+
+    public JoinFailedAtScanException(String message) {
+        super(message);
+    }
+
+    public JoinFailedAtScanException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public JoinFailedAtScanException(Exception cause) {
+        super(cause);
+    }
+}
diff --git a/android/net/lowpan/JoinFailedException.java b/android/net/lowpan/JoinFailedException.java
new file mode 100644
index 0000000..e51d382
--- /dev/null
+++ b/android/net/lowpan/JoinFailedException.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.lowpan;
+
+/**
+ * Exception indicating the join operation has failed.
+ *
+ * @see LowpanInterface
+ * @hide
+ */
+// @SystemApi
+public class JoinFailedException extends LowpanException {
+
+    public JoinFailedException() {}
+
+    public JoinFailedException(String message) {
+        super(message);
+    }
+
+    public JoinFailedException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    protected JoinFailedException(Exception cause) {
+        super(cause);
+    }
+}
diff --git a/android/net/lowpan/LowpanBeaconInfo.java b/android/net/lowpan/LowpanBeaconInfo.java
new file mode 100644
index 0000000..5d4a3a0
--- /dev/null
+++ b/android/net/lowpan/LowpanBeaconInfo.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.lowpan;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import com.android.internal.util.HexDump;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Objects;
+import java.util.TreeSet;
+
+/**
+ * Describes a LoWPAN Beacon
+ *
+ * @hide
+ */
+// @SystemApi
+public class LowpanBeaconInfo implements Parcelable {
+    public static final int UNKNOWN_RSSI = Integer.MAX_VALUE;
+    public static final int UNKNOWN_LQI = 0;
+
+    private LowpanIdentity mIdentity;
+    private int mRssi = UNKNOWN_RSSI;
+    private int mLqi = UNKNOWN_LQI;
+    private byte[] mBeaconAddress = null;
+    private final TreeSet<Integer> mFlags = new TreeSet<>();
+
+    public static final int FLAG_CAN_ASSIST = 1;
+
+    /** @hide */
+    public static class Builder {
+        final LowpanIdentity.Builder mIdentityBuilder = new LowpanIdentity.Builder();
+        final LowpanBeaconInfo mBeaconInfo = new LowpanBeaconInfo();
+
+        public Builder setLowpanIdentity(LowpanIdentity x) {
+            mIdentityBuilder.setLowpanIdentity(x);
+            return this;
+        }
+
+        public Builder setName(String x) {
+            mIdentityBuilder.setName(x);
+            return this;
+        }
+
+        public Builder setXpanid(byte x[]) {
+            mIdentityBuilder.setXpanid(x);
+            return this;
+        }
+
+        public Builder setPanid(int x) {
+            mIdentityBuilder.setPanid(x);
+            return this;
+        }
+
+        public Builder setChannel(int x) {
+            mIdentityBuilder.setChannel(x);
+            return this;
+        }
+
+        public Builder setType(String x) {
+            mIdentityBuilder.setType(x);
+            return this;
+        }
+
+        public Builder setRssi(int x) {
+            mBeaconInfo.mRssi = x;
+            return this;
+        }
+
+        public Builder setLqi(int x) {
+            mBeaconInfo.mLqi = x;
+            return this;
+        }
+
+        public Builder setBeaconAddress(byte x[]) {
+            mBeaconInfo.mBeaconAddress = (x != null ? x.clone() : null);
+            return this;
+        }
+
+        public Builder setFlag(int x) {
+            mBeaconInfo.mFlags.add(x);
+            return this;
+        }
+
+        public Builder setFlags(Collection<Integer> x) {
+            mBeaconInfo.mFlags.addAll(x);
+            return this;
+        }
+
+        public LowpanBeaconInfo build() {
+            mBeaconInfo.mIdentity = mIdentityBuilder.build();
+            if (mBeaconInfo.mBeaconAddress == null) {
+                mBeaconInfo.mBeaconAddress = new byte[0];
+            }
+            return mBeaconInfo;
+        }
+    }
+
+    private LowpanBeaconInfo() {}
+
+    public LowpanIdentity getLowpanIdentity() {
+        return mIdentity;
+    }
+
+    public int getRssi() {
+        return mRssi;
+    }
+
+    public int getLqi() {
+        return mLqi;
+    }
+
+    public byte[] getBeaconAddress() {
+        return mBeaconAddress.clone();
+    }
+
+    public Collection<Integer> getFlags() {
+        return (Collection<Integer>) mFlags.clone();
+    }
+
+    public boolean isFlagSet(int flag) {
+        return mFlags.contains(flag);
+    }
+
+    @Override
+    public String toString() {
+        StringBuffer sb = new StringBuffer();
+
+        sb.append(mIdentity.toString());
+
+        if (mRssi != UNKNOWN_RSSI) {
+            sb.append(", RSSI:").append(mRssi).append("dBm");
+        }
+
+        if (mLqi != UNKNOWN_LQI) {
+            sb.append(", LQI:").append(mLqi);
+        }
+
+        if (mBeaconAddress.length > 0) {
+            sb.append(", BeaconAddress:").append(HexDump.toHexString(mBeaconAddress));
+        }
+
+        for (Integer flag : mFlags) {
+            switch (flag.intValue()) {
+                case FLAG_CAN_ASSIST:
+                    sb.append(", CAN_ASSIST");
+                    break;
+                default:
+                    sb.append(", FLAG_").append(Integer.toHexString(flag));
+                    break;
+            }
+        }
+
+        return sb.toString();
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mIdentity, mRssi, mLqi, Arrays.hashCode(mBeaconAddress), mFlags);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof LowpanBeaconInfo)) {
+            return false;
+        }
+        LowpanBeaconInfo rhs = (LowpanBeaconInfo) obj;
+        return mIdentity.equals(rhs.mIdentity)
+                && Arrays.equals(mBeaconAddress, rhs.mBeaconAddress)
+                && mRssi == rhs.mRssi
+                && mLqi == rhs.mLqi
+                && mFlags.equals(rhs.mFlags);
+    }
+
+    /** Implement the Parcelable interface. */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Implement the Parcelable interface. */
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        mIdentity.writeToParcel(dest, flags);
+        dest.writeInt(mRssi);
+        dest.writeInt(mLqi);
+        dest.writeByteArray(mBeaconAddress);
+
+        dest.writeInt(mFlags.size());
+        for (Integer val : mFlags) {
+            dest.writeInt(val);
+        }
+    }
+
+    /** Implement the Parcelable interface. */
+    public static final @android.annotation.NonNull Creator<LowpanBeaconInfo> CREATOR =
+            new Creator<LowpanBeaconInfo>() {
+                public LowpanBeaconInfo createFromParcel(Parcel in) {
+                    Builder builder = new Builder();
+
+                    builder.setLowpanIdentity(LowpanIdentity.CREATOR.createFromParcel(in));
+
+                    builder.setRssi(in.readInt());
+                    builder.setLqi(in.readInt());
+
+                    builder.setBeaconAddress(in.createByteArray());
+
+                    for (int i = in.readInt(); i > 0; i--) {
+                        builder.setFlag(in.readInt());
+                    }
+
+                    return builder.build();
+                }
+
+                public LowpanBeaconInfo[] newArray(int size) {
+                    return new LowpanBeaconInfo[size];
+                }
+            };
+}
diff --git a/android/net/lowpan/LowpanChannelInfo.java b/android/net/lowpan/LowpanChannelInfo.java
new file mode 100644
index 0000000..12c98b6
--- /dev/null
+++ b/android/net/lowpan/LowpanChannelInfo.java
@@ -0,0 +1,216 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.lowpan;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import java.util.Objects;
+
+/**
+ * Provides detailed information about a given channel.
+ *
+ * @hide
+ */
+// @SystemApi
+public class LowpanChannelInfo implements Parcelable {
+
+    public static final int UNKNOWN_POWER = Integer.MAX_VALUE;
+    public static final float UNKNOWN_FREQUENCY = 0.0f;
+    public static final float UNKNOWN_BANDWIDTH = 0.0f;
+
+    private int mIndex = 0;
+    private String mName = null;
+    private float mSpectrumCenterFrequency = UNKNOWN_FREQUENCY;
+    private float mSpectrumBandwidth = UNKNOWN_BANDWIDTH;
+    private int mMaxTransmitPower = UNKNOWN_POWER;
+    private boolean mIsMaskedByRegulatoryDomain = false;
+
+    /** @hide */
+    public static LowpanChannelInfo getChannelInfoForIeee802154Page0(int index) {
+        LowpanChannelInfo info = new LowpanChannelInfo();
+
+        if (index < 0) {
+            info = null;
+
+        } else if (index == 0) {
+            info.mSpectrumCenterFrequency = 868300000.0f;
+            info.mSpectrumBandwidth = 600000.0f;
+
+        } else if (index < 11) {
+            info.mSpectrumCenterFrequency = 906000000.0f - (2000000.0f * 1) + 2000000.0f * (index);
+            info.mSpectrumBandwidth = 0; // Unknown
+
+        } else if (index < 26) {
+            info.mSpectrumCenterFrequency =
+                    2405000000.0f - (5000000.0f * 11) + 5000000.0f * (index);
+            info.mSpectrumBandwidth = 2000000.0f;
+
+        } else {
+            info = null;
+        }
+
+        info.mName = Integer.toString(index);
+
+        return info;
+    }
+
+    private LowpanChannelInfo() {}
+
+    private LowpanChannelInfo(int index, String name, float cf, float bw) {
+        mIndex = index;
+        mName = name;
+        mSpectrumCenterFrequency = cf;
+        mSpectrumBandwidth = bw;
+    }
+
+    public String getName() {
+        return mName;
+    }
+
+    public int getIndex() {
+        return mIndex;
+    }
+
+    public int getMaxTransmitPower() {
+        return mMaxTransmitPower;
+    }
+
+    public boolean isMaskedByRegulatoryDomain() {
+        return mIsMaskedByRegulatoryDomain;
+    }
+
+    public float getSpectrumCenterFrequency() {
+        return mSpectrumCenterFrequency;
+    }
+
+    public float getSpectrumBandwidth() {
+        return mSpectrumBandwidth;
+    }
+
+    @Override
+    public String toString() {
+        StringBuffer sb = new StringBuffer();
+
+        sb.append("Channel ").append(mIndex);
+
+        if (mName != null && !mName.equals(Integer.toString(mIndex))) {
+            sb.append(" (").append(mName).append(")");
+        }
+
+        if (mSpectrumCenterFrequency > 0.0f) {
+            if (mSpectrumCenterFrequency > 1000000000.0f) {
+                sb.append(", SpectrumCenterFrequency: ")
+                        .append(mSpectrumCenterFrequency / 1000000000.0f)
+                        .append("GHz");
+            } else if (mSpectrumCenterFrequency > 1000000.0f) {
+                sb.append(", SpectrumCenterFrequency: ")
+                        .append(mSpectrumCenterFrequency / 1000000.0f)
+                        .append("MHz");
+            } else {
+                sb.append(", SpectrumCenterFrequency: ")
+                        .append(mSpectrumCenterFrequency / 1000.0f)
+                        .append("kHz");
+            }
+        }
+
+        if (mSpectrumBandwidth > 0.0f) {
+            if (mSpectrumBandwidth > 1000000000.0f) {
+                sb.append(", SpectrumBandwidth: ")
+                        .append(mSpectrumBandwidth / 1000000000.0f)
+                        .append("GHz");
+            } else if (mSpectrumBandwidth > 1000000.0f) {
+                sb.append(", SpectrumBandwidth: ")
+                        .append(mSpectrumBandwidth / 1000000.0f)
+                        .append("MHz");
+            } else {
+                sb.append(", SpectrumBandwidth: ")
+                        .append(mSpectrumBandwidth / 1000.0f)
+                        .append("kHz");
+            }
+        }
+
+        if (mMaxTransmitPower != UNKNOWN_POWER) {
+            sb.append(", MaxTransmitPower: ").append(mMaxTransmitPower).append("dBm");
+        }
+
+        return sb.toString();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof LowpanChannelInfo)) {
+            return false;
+        }
+        LowpanChannelInfo rhs = (LowpanChannelInfo) obj;
+        return Objects.equals(mName, rhs.mName)
+                && mIndex == rhs.mIndex
+                && mIsMaskedByRegulatoryDomain == rhs.mIsMaskedByRegulatoryDomain
+                && mSpectrumCenterFrequency == rhs.mSpectrumCenterFrequency
+                && mSpectrumBandwidth == rhs.mSpectrumBandwidth
+                && mMaxTransmitPower == rhs.mMaxTransmitPower;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(
+                mName,
+                mIndex,
+                mIsMaskedByRegulatoryDomain,
+                mSpectrumCenterFrequency,
+                mSpectrumBandwidth,
+                mMaxTransmitPower);
+    }
+
+    /** Implement the Parcelable interface. */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Implement the Parcelable interface. */
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(mIndex);
+        dest.writeString(mName);
+        dest.writeFloat(mSpectrumCenterFrequency);
+        dest.writeFloat(mSpectrumBandwidth);
+        dest.writeInt(mMaxTransmitPower);
+        dest.writeBoolean(mIsMaskedByRegulatoryDomain);
+    }
+
+    /** Implement the Parcelable interface. */
+    public static final @android.annotation.NonNull Creator<LowpanChannelInfo> CREATOR =
+            new Creator<LowpanChannelInfo>() {
+
+                public LowpanChannelInfo createFromParcel(Parcel in) {
+                    LowpanChannelInfo info = new LowpanChannelInfo();
+
+                    info.mIndex = in.readInt();
+                    info.mName = in.readString();
+                    info.mSpectrumCenterFrequency = in.readFloat();
+                    info.mSpectrumBandwidth = in.readFloat();
+                    info.mMaxTransmitPower = in.readInt();
+                    info.mIsMaskedByRegulatoryDomain = in.readBoolean();
+
+                    return info;
+                }
+
+                public LowpanChannelInfo[] newArray(int size) {
+                    return new LowpanChannelInfo[size];
+                }
+            };
+}
diff --git a/android/net/lowpan/LowpanCommissioningSession.java b/android/net/lowpan/LowpanCommissioningSession.java
new file mode 100644
index 0000000..8f75e8d
--- /dev/null
+++ b/android/net/lowpan/LowpanCommissioningSession.java
@@ -0,0 +1,223 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.lowpan;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.IpPrefix;
+import android.os.DeadObjectException;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.RemoteException;
+
+/**
+ * Commissioning Session.
+ *
+ * <p>This class enables a device to learn the credential needed to join a network using a technique
+ * called "in-band commissioning".
+ *
+ * @hide
+ */
+// @SystemApi
+public class LowpanCommissioningSession {
+
+    private final ILowpanInterface mBinder;
+    private final LowpanBeaconInfo mBeaconInfo;
+    private final ILowpanInterfaceListener mInternalCallback = new InternalCallback();
+    private final Looper mLooper;
+    private Handler mHandler;
+    private Callback mCallback = null;
+    private volatile boolean mIsClosed = false;
+
+    /**
+     * Callback base class for {@link LowpanCommissioningSession}
+     *
+     * @hide
+     */
+    // @SystemApi
+    public abstract static class Callback {
+        public void onReceiveFromCommissioner(@NonNull byte[] packet) {};
+
+        public void onClosed() {};
+    }
+
+    private class InternalCallback extends ILowpanInterfaceListener.Stub {
+        @Override
+        public void onStateChanged(String value) {
+            if (!mIsClosed) {
+                switch (value) {
+                    case ILowpanInterface.STATE_OFFLINE:
+                    case ILowpanInterface.STATE_FAULT:
+                        synchronized (LowpanCommissioningSession.this) {
+                            lockedCleanup();
+                        }
+                }
+            }
+        }
+
+        @Override
+        public void onReceiveFromCommissioner(byte[] packet) {
+            mHandler.post(
+                    () -> {
+                        synchronized (LowpanCommissioningSession.this) {
+                            if (!mIsClosed && (mCallback != null)) {
+                                mCallback.onReceiveFromCommissioner(packet);
+                            }
+                        }
+                    });
+        }
+
+        // We ignore all other callbacks.
+        @Override
+        public void onEnabledChanged(boolean value) {}
+
+        @Override
+        public void onConnectedChanged(boolean value) {}
+
+        @Override
+        public void onUpChanged(boolean value) {}
+
+        @Override
+        public void onRoleChanged(String value) {}
+
+        @Override
+        public void onLowpanIdentityChanged(LowpanIdentity value) {}
+
+        @Override
+        public void onLinkNetworkAdded(IpPrefix value) {}
+
+        @Override
+        public void onLinkNetworkRemoved(IpPrefix value) {}
+
+        @Override
+        public void onLinkAddressAdded(String value) {}
+
+        @Override
+        public void onLinkAddressRemoved(String value) {}
+    }
+
+    LowpanCommissioningSession(
+            ILowpanInterface binder, LowpanBeaconInfo beaconInfo, Looper looper) {
+        mBinder = binder;
+        mBeaconInfo = beaconInfo;
+        mLooper = looper;
+
+        if (mLooper != null) {
+            mHandler = new Handler(mLooper);
+        } else {
+            mHandler = new Handler();
+        }
+
+        try {
+            mBinder.addListener(mInternalCallback);
+
+        } catch (RemoteException x) {
+            throw x.rethrowAsRuntimeException();
+        }
+    }
+
+    private void lockedCleanup() {
+        // Note: this method is only called from synchronized contexts.
+
+        if (!mIsClosed) {
+            try {
+                mBinder.removeListener(mInternalCallback);
+
+            } catch (DeadObjectException x) {
+                /* We don't care if we receive a DOE at this point.
+                 * DOE is as good as success as far as we are concerned.
+                 */
+
+            } catch (RemoteException x) {
+                throw x.rethrowAsRuntimeException();
+            }
+
+            if (mCallback != null) {
+                mHandler.post(() -> mCallback.onClosed());
+            }
+        }
+
+        mCallback = null;
+        mIsClosed = true;
+    }
+
+    /** TODO: doc */
+    @NonNull
+    public LowpanBeaconInfo getBeaconInfo() {
+        return mBeaconInfo;
+    }
+
+    /** TODO: doc */
+    public void sendToCommissioner(@NonNull byte[] packet) {
+        if (!mIsClosed) {
+            try {
+                mBinder.sendToCommissioner(packet);
+
+            } catch (DeadObjectException x) {
+                /* This method is a best-effort delivery.
+                 * We don't care if we receive a DOE at this point.
+                 */
+
+            } catch (RemoteException x) {
+                throw x.rethrowAsRuntimeException();
+            }
+        }
+    }
+
+    /** TODO: doc */
+    public synchronized void setCallback(@Nullable Callback cb, @Nullable Handler handler) {
+        if (!mIsClosed) {
+            /* This class can be created with or without a default looper.
+             * Also, this method can be called with or without a specific
+             * handler. If a handler is specified, it is to always be used.
+             * Otherwise, if there was a Looper specified when this object
+             * was created, we create a new handle based on that looper.
+             * Otherwise we just create a default handler object. Since we
+             * don't really know how the previous handler was created, we
+             * end up always replacing it here. This isn't a huge problem
+             * because this method should be called infrequently.
+             */
+            if (handler != null) {
+                mHandler = handler;
+            } else if (mLooper != null) {
+                mHandler = new Handler(mLooper);
+            } else {
+                mHandler = new Handler();
+            }
+            mCallback = cb;
+        }
+    }
+
+    /** TODO: doc */
+    public synchronized void close() {
+        if (!mIsClosed) {
+            try {
+                mBinder.closeCommissioningSession();
+
+                lockedCleanup();
+
+            } catch (DeadObjectException x) {
+                /* We don't care if we receive a DOE at this point.
+                 * DOE is as good as success as far as we are concerned.
+                 */
+
+            } catch (RemoteException x) {
+                throw x.rethrowAsRuntimeException();
+            }
+        }
+    }
+}
diff --git a/android/net/lowpan/LowpanCredential.java b/android/net/lowpan/LowpanCredential.java
new file mode 100644
index 0000000..dcbb831
--- /dev/null
+++ b/android/net/lowpan/LowpanCredential.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.lowpan;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import com.android.internal.util.HexDump;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Describes a credential for a LoWPAN network.
+ *
+ * @hide
+ */
+// @SystemApi
+public class LowpanCredential implements Parcelable {
+
+    public static final int UNSPECIFIED_KEY_INDEX = 0;
+
+    private byte[] mMasterKey = null;
+    private int mMasterKeyIndex = UNSPECIFIED_KEY_INDEX;
+
+    LowpanCredential() {}
+
+    private LowpanCredential(byte[] masterKey, int keyIndex) {
+        setMasterKey(masterKey, keyIndex);
+    }
+
+    private LowpanCredential(byte[] masterKey) {
+        setMasterKey(masterKey);
+    }
+
+    public static LowpanCredential createMasterKey(byte[] masterKey) {
+        return new LowpanCredential(masterKey);
+    }
+
+    public static LowpanCredential createMasterKey(byte[] masterKey, int keyIndex) {
+        return new LowpanCredential(masterKey, keyIndex);
+    }
+
+    void setMasterKey(byte[] masterKey) {
+        if (masterKey != null) {
+            masterKey = masterKey.clone();
+        }
+        mMasterKey = masterKey;
+    }
+
+    void setMasterKeyIndex(int keyIndex) {
+        mMasterKeyIndex = keyIndex;
+    }
+
+    void setMasterKey(byte[] masterKey, int keyIndex) {
+        setMasterKey(masterKey);
+        setMasterKeyIndex(keyIndex);
+    }
+
+    public byte[] getMasterKey() {
+        if (mMasterKey != null) {
+            return mMasterKey.clone();
+        }
+        return null;
+    }
+
+    public int getMasterKeyIndex() {
+        return mMasterKeyIndex;
+    }
+
+    public boolean isMasterKey() {
+        return mMasterKey != null;
+    }
+
+    public String toSensitiveString() {
+        StringBuffer sb = new StringBuffer();
+
+        sb.append("<LowpanCredential");
+
+        if (isMasterKey()) {
+            sb.append(" MasterKey:").append(HexDump.toHexString(mMasterKey));
+            if (mMasterKeyIndex != UNSPECIFIED_KEY_INDEX) {
+                sb.append(", Index:").append(mMasterKeyIndex);
+            }
+        } else {
+            sb.append(" empty");
+        }
+
+        sb.append(">");
+
+        return sb.toString();
+    }
+
+    @Override
+    public String toString() {
+        StringBuffer sb = new StringBuffer();
+
+        sb.append("<LowpanCredential");
+
+        if (isMasterKey()) {
+            // We don't print out the contents of the key here,
+            // we only do that in toSensitiveString.
+            sb.append(" MasterKey");
+            if (mMasterKeyIndex != UNSPECIFIED_KEY_INDEX) {
+                sb.append(", Index:").append(mMasterKeyIndex);
+            }
+        } else {
+            sb.append(" empty");
+        }
+
+        sb.append(">");
+
+        return sb.toString();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof LowpanCredential)) {
+            return false;
+        }
+        LowpanCredential rhs = (LowpanCredential) obj;
+        return Arrays.equals(mMasterKey, rhs.mMasterKey) && mMasterKeyIndex == rhs.mMasterKeyIndex;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(Arrays.hashCode(mMasterKey), mMasterKeyIndex);
+    }
+
+    /** Implement the Parcelable interface. */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Implement the Parcelable interface. */
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeByteArray(mMasterKey);
+        dest.writeInt(mMasterKeyIndex);
+    }
+
+    /** Implement the Parcelable interface. */
+    public static final @android.annotation.NonNull Creator<LowpanCredential> CREATOR =
+            new Creator<LowpanCredential>() {
+
+                public LowpanCredential createFromParcel(Parcel in) {
+                    LowpanCredential credential = new LowpanCredential();
+
+                    credential.mMasterKey = in.createByteArray();
+                    credential.mMasterKeyIndex = in.readInt();
+
+                    return credential;
+                }
+
+                public LowpanCredential[] newArray(int size) {
+                    return new LowpanCredential[size];
+                }
+            };
+}
diff --git a/android/net/lowpan/LowpanEnergyScanResult.java b/android/net/lowpan/LowpanEnergyScanResult.java
new file mode 100644
index 0000000..da87752
--- /dev/null
+++ b/android/net/lowpan/LowpanEnergyScanResult.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.lowpan;
+
+/**
+ * Describes the result from one channel of an energy scan.
+ *
+ * @hide
+ */
+// @SystemApi
+public class LowpanEnergyScanResult {
+    public static final int UNKNOWN = Integer.MAX_VALUE;
+
+    private int mChannel = UNKNOWN;
+    private int mMaxRssi = UNKNOWN;
+
+    LowpanEnergyScanResult() {}
+
+    public int getChannel() {
+        return mChannel;
+    }
+
+    public int getMaxRssi() {
+        return mMaxRssi;
+    }
+
+    void setChannel(int x) {
+        mChannel = x;
+    }
+
+    void setMaxRssi(int x) {
+        mMaxRssi = x;
+    }
+
+    @Override
+    public String toString() {
+        return "LowpanEnergyScanResult(channel: " + mChannel + ", maxRssi:" + mMaxRssi + ")";
+    }
+}
diff --git a/android/net/lowpan/LowpanException.java b/android/net/lowpan/LowpanException.java
new file mode 100644
index 0000000..5dfce48
--- /dev/null
+++ b/android/net/lowpan/LowpanException.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.lowpan;
+
+import android.os.ServiceSpecificException;
+import android.util.AndroidException;
+
+/**
+ * <code>LowpanException</code> is thrown if an action to a LoWPAN interface could not be performed
+ * or a LoWPAN interface property could not be fetched or changed.
+ *
+ * @see LowpanInterface
+ * @hide
+ */
+// @SystemApi
+public class LowpanException extends AndroidException {
+    public LowpanException() {}
+
+    public LowpanException(String message) {
+        super(message);
+    }
+
+    public LowpanException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public LowpanException(Exception cause) {
+        super(cause);
+    }
+
+    /* This method returns LowpanException so that the caller
+     * can add "throw" before the invocation of this method.
+     * This might seem superfluous, but it is actually to
+     * help provide a hint to the java compiler that this
+     * function will not return.
+     */
+    static LowpanException rethrowFromServiceSpecificException(ServiceSpecificException e)
+            throws LowpanException {
+        switch (e.errorCode) {
+            case ILowpanInterface.ERROR_DISABLED:
+                throw new InterfaceDisabledException(e);
+
+            case ILowpanInterface.ERROR_WRONG_STATE:
+                throw new WrongStateException(e);
+
+            case ILowpanInterface.ERROR_CANCELED:
+                throw new OperationCanceledException(e);
+
+            case ILowpanInterface.ERROR_JOIN_FAILED_UNKNOWN:
+                throw new JoinFailedException(e);
+
+            case ILowpanInterface.ERROR_JOIN_FAILED_AT_SCAN:
+                throw new JoinFailedAtScanException(e);
+
+            case ILowpanInterface.ERROR_JOIN_FAILED_AT_AUTH:
+                throw new JoinFailedAtAuthException(e);
+
+            case ILowpanInterface.ERROR_FORM_FAILED_AT_SCAN:
+                throw new NetworkAlreadyExistsException(e);
+
+            case ILowpanInterface.ERROR_FEATURE_NOT_SUPPORTED:
+                throw new LowpanException(
+                        e.getMessage() != null ? e.getMessage() : "Feature not supported", e);
+
+            case ILowpanInterface.ERROR_NCP_PROBLEM:
+                throw new LowpanRuntimeException(
+                        e.getMessage() != null ? e.getMessage() : "NCP problem", e);
+
+            case ILowpanInterface.ERROR_INVALID_ARGUMENT:
+                throw new LowpanRuntimeException(
+                        e.getMessage() != null ? e.getMessage() : "Invalid argument", e);
+
+            case ILowpanInterface.ERROR_UNSPECIFIED:
+            default:
+                throw new LowpanRuntimeException(e);
+        }
+    }
+}
diff --git a/android/net/lowpan/LowpanIdentity.java b/android/net/lowpan/LowpanIdentity.java
new file mode 100644
index 0000000..1997bc4
--- /dev/null
+++ b/android/net/lowpan/LowpanIdentity.java
@@ -0,0 +1,255 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.lowpan;
+
+import android.annotation.NonNull;
+import android.icu.text.StringPrep;
+import android.icu.text.StringPrepParseException;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+import com.android.internal.util.HexDump;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Describes an instance of a LoWPAN network.
+ *
+ * @hide
+ */
+// @SystemApi
+public class LowpanIdentity implements Parcelable {
+    private static final String TAG = LowpanIdentity.class.getSimpleName();
+
+    // Constants
+    public static final int UNSPECIFIED_CHANNEL = -1;
+    public static final int UNSPECIFIED_PANID = 0xFFFFFFFF;
+    // Builder
+
+    /** @hide */
+    // @SystemApi
+    public static class Builder {
+        private static final StringPrep stringPrep =
+                StringPrep.getInstance(StringPrep.RFC3920_RESOURCEPREP);
+
+        final LowpanIdentity mIdentity = new LowpanIdentity();
+
+        private static String escape(@NonNull byte[] bytes) {
+            StringBuffer sb = new StringBuffer();
+            for (byte b : bytes) {
+                if (b >= 32 && b <= 126) {
+                    sb.append((char) b);
+                } else {
+                    sb.append(String.format("\\0x%02x", b & 0xFF));
+                }
+            }
+            return sb.toString();
+        }
+
+        public Builder setLowpanIdentity(@NonNull LowpanIdentity x) {
+            Objects.requireNonNull(x);
+            setRawName(x.getRawName());
+            setXpanid(x.getXpanid());
+            setPanid(x.getPanid());
+            setChannel(x.getChannel());
+            setType(x.getType());
+            return this;
+        }
+
+        public Builder setName(@NonNull String name) {
+            Objects.requireNonNull(name);
+            try {
+                mIdentity.mName = stringPrep.prepare(name, StringPrep.DEFAULT);
+                mIdentity.mRawName = mIdentity.mName.getBytes(StandardCharsets.UTF_8);
+                mIdentity.mIsNameValid = true;
+            } catch (StringPrepParseException x) {
+                Log.w(TAG, x.toString());
+                setRawName(name.getBytes(StandardCharsets.UTF_8));
+            }
+            return this;
+        }
+
+        public Builder setRawName(@NonNull byte[] name) {
+            Objects.requireNonNull(name);
+            mIdentity.mRawName = name.clone();
+            mIdentity.mName = new String(name, StandardCharsets.UTF_8);
+            try {
+                String nameCheck = stringPrep.prepare(mIdentity.mName, StringPrep.DEFAULT);
+                mIdentity.mIsNameValid =
+                        Arrays.equals(nameCheck.getBytes(StandardCharsets.UTF_8), name);
+            } catch (StringPrepParseException x) {
+                Log.w(TAG, x.toString());
+                mIdentity.mIsNameValid = false;
+            }
+
+            // Non-normal names must be rendered differently to avoid confusion.
+            if (!mIdentity.mIsNameValid) {
+                mIdentity.mName = "«" + escape(name) + "»";
+            }
+
+            return this;
+        }
+
+        public Builder setXpanid(byte x[]) {
+            mIdentity.mXpanid = (x != null ? x.clone() : null);
+            return this;
+        }
+
+        public Builder setPanid(int x) {
+            mIdentity.mPanid = x;
+            return this;
+        }
+
+        public Builder setType(@NonNull String x) {
+            mIdentity.mType = x;
+            return this;
+        }
+
+        public Builder setChannel(int x) {
+            mIdentity.mChannel = x;
+            return this;
+        }
+
+        public LowpanIdentity build() {
+            return mIdentity;
+        }
+    }
+
+    LowpanIdentity() {}
+
+    // Instance Variables
+
+    private String mName = "";
+    private boolean mIsNameValid = true;
+    private byte[] mRawName = new byte[0];
+    private String mType = "";
+    private byte[] mXpanid = new byte[0];
+    private int mPanid = UNSPECIFIED_PANID;
+    private int mChannel = UNSPECIFIED_CHANNEL;
+
+    // Public Getters
+
+    public String getName() {
+        return mName;
+    }
+
+    public boolean isNameValid() {
+        return mIsNameValid;
+    }
+
+    public byte[] getRawName() {
+        return mRawName.clone();
+    }
+
+    public byte[] getXpanid() {
+        return mXpanid.clone();
+    }
+
+    public int getPanid() {
+        return mPanid;
+    }
+
+    public String getType() {
+        return mType;
+    }
+
+    public int getChannel() {
+        return mChannel;
+    }
+
+    @Override
+    public String toString() {
+        StringBuffer sb = new StringBuffer();
+
+        sb.append("Name:").append(getName());
+
+        if (mType.length() > 0) {
+            sb.append(", Type:").append(mType);
+        }
+
+        if (mXpanid.length > 0) {
+            sb.append(", XPANID:").append(HexDump.toHexString(mXpanid));
+        }
+
+        if (mPanid != UNSPECIFIED_PANID) {
+            sb.append(", PANID:").append(String.format("0x%04X", mPanid));
+        }
+
+        if (mChannel != UNSPECIFIED_CHANNEL) {
+            sb.append(", Channel:").append(mChannel);
+        }
+
+        return sb.toString();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof LowpanIdentity)) {
+            return false;
+        }
+        LowpanIdentity rhs = (LowpanIdentity) obj;
+        return Arrays.equals(mRawName, rhs.mRawName)
+                && Arrays.equals(mXpanid, rhs.mXpanid)
+                && mType.equals(rhs.mType)
+                && mPanid == rhs.mPanid
+                && mChannel == rhs.mChannel;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(
+                Arrays.hashCode(mRawName), mType, Arrays.hashCode(mXpanid), mPanid, mChannel);
+    }
+
+    /** Implement the Parcelable interface. */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Implement the Parcelable interface. */
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeByteArray(mRawName);
+        dest.writeString(mType);
+        dest.writeByteArray(mXpanid);
+        dest.writeInt(mPanid);
+        dest.writeInt(mChannel);
+    }
+
+    /** Implement the Parcelable interface. */
+    public static final @android.annotation.NonNull Creator<LowpanIdentity> CREATOR =
+            new Creator<LowpanIdentity>() {
+
+                public LowpanIdentity createFromParcel(Parcel in) {
+                    Builder builder = new Builder();
+
+                    builder.setRawName(in.createByteArray());
+                    builder.setType(in.readString());
+                    builder.setXpanid(in.createByteArray());
+                    builder.setPanid(in.readInt());
+                    builder.setChannel(in.readInt());
+
+                    return builder.build();
+                }
+
+                public LowpanIdentity[] newArray(int size) {
+                    return new LowpanIdentity[size];
+                }
+            };
+}
diff --git a/android/net/lowpan/LowpanInterface.java b/android/net/lowpan/LowpanInterface.java
new file mode 100644
index 0000000..57e9135
--- /dev/null
+++ b/android/net/lowpan/LowpanInterface.java
@@ -0,0 +1,824 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.lowpan;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.os.DeadObjectException;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import android.util.Log;
+import java.util.HashMap;
+
+/**
+ * Class for managing a specific Low-power Wireless Personal Area Network (LoWPAN) interface.
+ *
+ * @hide
+ */
+// @SystemApi
+public class LowpanInterface {
+    private static final String TAG = LowpanInterface.class.getSimpleName();
+
+    /** Detached role. The interface is not currently attached to a network. */
+    public static final String ROLE_DETACHED = ILowpanInterface.ROLE_DETACHED;
+
+    /** End-device role. End devices do not route traffic for other nodes. */
+    public static final String ROLE_END_DEVICE = ILowpanInterface.ROLE_END_DEVICE;
+
+    /** Router role. Routers help route traffic around the mesh network. */
+    public static final String ROLE_ROUTER = ILowpanInterface.ROLE_ROUTER;
+
+    /**
+     * Sleepy End-Device role.
+     *
+     * <p>End devices with this role are nominally asleep, waking up periodically to check in with
+     * their parent to see if there are packets destined for them. Such devices are capable of
+     * extraordinarilly low power consumption, but packet latency can be on the order of dozens of
+     * seconds(depending on how the node is configured).
+     */
+    public static final String ROLE_SLEEPY_END_DEVICE = ILowpanInterface.ROLE_SLEEPY_END_DEVICE;
+
+    /**
+     * Sleepy-router role.
+     *
+     * <p>Routers with this role are nominally asleep, waking up periodically to check in with other
+     * routers and their children.
+     */
+    public static final String ROLE_SLEEPY_ROUTER = ILowpanInterface.ROLE_SLEEPY_ROUTER;
+
+    /** TODO: doc */
+    public static final String ROLE_LEADER = ILowpanInterface.ROLE_LEADER;
+
+    /** TODO: doc */
+    public static final String ROLE_COORDINATOR = ILowpanInterface.ROLE_COORDINATOR;
+
+    /**
+     * Offline state.
+     *
+     * <p>This is the initial state of the LoWPAN interface when the underlying driver starts. In
+     * this state the NCP is idle and not connected to any network.
+     *
+     * <p>This state can be explicitly entered by calling {@link #reset()}, {@link #leave()}, or
+     * <code>setUp(false)</code>, with the later two only working if we were not previously in the
+     * {@link #STATE_FAULT} state.
+     *
+     * @see #getState()
+     * @see #STATE_FAULT
+     */
+    public static final String STATE_OFFLINE = ILowpanInterface.STATE_OFFLINE;
+
+    /**
+     * Commissioning state.
+     *
+     * <p>The interface enters this state after a call to {@link #startCommissioningSession()}. This
+     * state may only be entered directly from the {@link #STATE_OFFLINE} state.
+     *
+     * @see #startCommissioningSession()
+     * @see #getState()
+     * @hide
+     */
+    public static final String STATE_COMMISSIONING = ILowpanInterface.STATE_COMMISSIONING;
+
+    /**
+     * Attaching state.
+     *
+     * <p>The interface enters this state when it starts the process of trying to find other nodes
+     * so that it can attach to any pre-existing network fragment, or when it is in the process of
+     * calculating the optimal values for unspecified parameters when forming a new network.
+     *
+     * <p>The interface may stay in this state for a prolonged period of time (or may spontaneously
+     * enter this state from {@link #STATE_ATTACHED}) if the underlying network technology is
+     * heirarchical (like ZigBeeIP) or if the device role is that of an "end-device" ({@link
+     * #ROLE_END_DEVICE} or {@link #ROLE_SLEEPY_END_DEVICE}). This is because such roles cannot
+     * create their own network fragments.
+     *
+     * @see #STATE_ATTACHED
+     * @see #getState()
+     */
+    public static final String STATE_ATTACHING = ILowpanInterface.STATE_ATTACHING;
+
+    /**
+     * Attached state.
+     *
+     * <p>The interface enters this state from {@link #STATE_ATTACHING} once it is actively
+     * participating on a network fragment.
+     *
+     * @see #STATE_ATTACHING
+     * @see #getState()
+     */
+    public static final String STATE_ATTACHED = ILowpanInterface.STATE_ATTACHED;
+
+    /**
+     * Fault state.
+     *
+     * <p>The interface will enter this state when the driver has detected some sort of problem from
+     * which it was not immediately able to recover.
+     *
+     * <p>This state can be entered spontaneously from any other state. Calling {@link #reset} will
+     * cause the device to return to the {@link #STATE_OFFLINE} state.
+     *
+     * @see #getState
+     * @see #STATE_OFFLINE
+     */
+    public static final String STATE_FAULT = ILowpanInterface.STATE_FAULT;
+
+    /**
+     * Network type for Thread 1.x networks.
+     *
+     * @see android.net.lowpan.LowpanIdentity#getType
+     * @see #getLowpanIdentity
+     * @hide
+     */
+    public static final String NETWORK_TYPE_THREAD_V1 = ILowpanInterface.NETWORK_TYPE_THREAD_V1;
+
+    public static final String EMPTY_PARTITION_ID = "";
+
+    /**
+     * Callback base class for LowpanInterface
+     *
+     * @hide
+     */
+    // @SystemApi
+    public abstract static class Callback {
+        public void onConnectedChanged(boolean value) {}
+
+        public void onEnabledChanged(boolean value) {}
+
+        public void onUpChanged(boolean value) {}
+
+        public void onRoleChanged(@NonNull String value) {}
+
+        public void onStateChanged(@NonNull String state) {}
+
+        public void onLowpanIdentityChanged(@NonNull LowpanIdentity value) {}
+
+        public void onLinkNetworkAdded(IpPrefix prefix) {}
+
+        public void onLinkNetworkRemoved(IpPrefix prefix) {}
+
+        public void onLinkAddressAdded(LinkAddress address) {}
+
+        public void onLinkAddressRemoved(LinkAddress address) {}
+    }
+
+    private final ILowpanInterface mBinder;
+    private final Looper mLooper;
+    private final HashMap<Integer, ILowpanInterfaceListener> mListenerMap = new HashMap<>();
+
+    /**
+     * Create a new LowpanInterface instance. Applications will almost always want to use {@link
+     * LowpanManager#getInterface LowpanManager.getInterface()} instead of this.
+     *
+     * @param context the application context
+     * @param service the Binder interface
+     * @param looper the Binder interface
+     * @hide
+     */
+    public LowpanInterface(Context context, ILowpanInterface service, Looper looper) {
+        /* We aren't currently using the context, but if we need
+         * it later on we can easily add it to the class.
+         */
+
+        mBinder = service;
+        mLooper = looper;
+    }
+
+    /**
+     * Returns the ILowpanInterface object associated with this interface.
+     *
+     * @hide
+     */
+    public ILowpanInterface getService() {
+        return mBinder;
+    }
+
+    // Public Actions
+
+    /**
+     * Form a new network with the given network information optional credential. Unspecified fields
+     * in the network information will be filled in with reasonable values. If the network
+     * credential is unspecified, one will be generated automatically.
+     *
+     * <p>This method will block until either the network was successfully formed or an error
+     * prevents the network form being formed.
+     *
+     * <p>Upon success, the interface will be up and attached to the newly formed network.
+     *
+     * @see #join(LowpanProvision)
+     */
+    public void form(@NonNull LowpanProvision provision) throws LowpanException {
+        try {
+            mBinder.form(provision);
+
+        } catch (RemoteException x) {
+            throw x.rethrowAsRuntimeException();
+
+        } catch (ServiceSpecificException x) {
+            throw LowpanException.rethrowFromServiceSpecificException(x);
+        }
+    }
+
+    /**
+     * Attempts to join a new network with the given network information. This method will block
+     * until either the network was successfully joined or an error prevented the network from being
+     * formed. Upon success, the interface will be up and attached to the newly joined network.
+     *
+     * <p>Note that “joining” is distinct from “attaching”: Joining requires at least one other peer
+     * device to be present in order for the operation to complete successfully.
+     */
+    public void join(@NonNull LowpanProvision provision) throws LowpanException {
+        try {
+            mBinder.join(provision);
+
+        } catch (RemoteException x) {
+            throw x.rethrowAsRuntimeException();
+
+        } catch (ServiceSpecificException x) {
+            throw LowpanException.rethrowFromServiceSpecificException(x);
+        }
+    }
+
+    /**
+     * Attaches to the network described by identity and credential. This is similar to {@link
+     * #join}, except that (assuming the identity and credential are valid) it will always succeed
+     * and provision the interface, even if there are no peers nearby.
+     *
+     * <p>This method will block execution until the operation has completed.
+     */
+    public void attach(@NonNull LowpanProvision provision) throws LowpanException {
+        try {
+            mBinder.attach(provision);
+
+        } catch (RemoteException x) {
+            throw x.rethrowAsRuntimeException();
+
+        } catch (ServiceSpecificException x) {
+            throw LowpanException.rethrowFromServiceSpecificException(x);
+        }
+    }
+
+    /**
+     * Bring down the network interface and forget all non-volatile details about the current
+     * network.
+     *
+     * <p>This method will block execution until the operation has completed.
+     */
+    public void leave() throws LowpanException {
+        try {
+            mBinder.leave();
+
+        } catch (RemoteException x) {
+            throw x.rethrowAsRuntimeException();
+
+        } catch (ServiceSpecificException x) {
+            throw LowpanException.rethrowFromServiceSpecificException(x);
+        }
+    }
+
+    /**
+     * Start a new commissioning session. Will fail if the interface is attached to a network or if
+     * the interface is disabled.
+     */
+    public @NonNull LowpanCommissioningSession startCommissioningSession(
+            @NonNull LowpanBeaconInfo beaconInfo) throws LowpanException {
+        try {
+            mBinder.startCommissioningSession(beaconInfo);
+
+            return new LowpanCommissioningSession(mBinder, beaconInfo, mLooper);
+
+        } catch (RemoteException x) {
+            throw x.rethrowAsRuntimeException();
+
+        } catch (ServiceSpecificException x) {
+            throw LowpanException.rethrowFromServiceSpecificException(x);
+        }
+    }
+
+    /**
+     * Reset this network interface as if it has been power cycled. Will bring the network interface
+     * down if it was previously up. Will not erase any non-volatile settings.
+     *
+     * <p>This method will block execution until the operation has completed.
+     *
+     * @hide
+     */
+    public void reset() throws LowpanException {
+        try {
+            mBinder.reset();
+
+        } catch (RemoteException x) {
+            throw x.rethrowAsRuntimeException();
+
+        } catch (ServiceSpecificException x) {
+            throw LowpanException.rethrowFromServiceSpecificException(x);
+        }
+    }
+
+    // Public Getters and Setters
+
+    /** Returns the name of this network interface. */
+    @NonNull
+    public String getName() {
+        try {
+            return mBinder.getName();
+
+        } catch (DeadObjectException x) {
+            return "";
+
+        } catch (RemoteException x) {
+            throw x.rethrowAsRuntimeException();
+        }
+    }
+
+    /**
+     * Indicates if the interface is enabled or disabled.
+     *
+     * @see #setEnabled
+     * @see android.net.lowpan.LowpanException#LOWPAN_DISABLED
+     */
+    public boolean isEnabled() {
+        try {
+            return mBinder.isEnabled();
+
+        } catch (DeadObjectException x) {
+            return false;
+
+        } catch (RemoteException x) {
+            throw x.rethrowAsRuntimeException();
+        }
+    }
+
+    /**
+     * Enables or disables the LoWPAN interface. When disabled, the interface is put into a
+     * low-power state and all commands that require the NCP to be queried will fail with {@link
+     * android.net.lowpan.LowpanException#LOWPAN_DISABLED}.
+     *
+     * @see #isEnabled
+     * @see android.net.lowpan.LowpanException#LOWPAN_DISABLED
+     * @hide
+     */
+    public void setEnabled(boolean enabled) throws LowpanException {
+        try {
+            mBinder.setEnabled(enabled);
+
+        } catch (RemoteException x) {
+            throw x.rethrowAsRuntimeException();
+
+        } catch (ServiceSpecificException x) {
+            throw LowpanException.rethrowFromServiceSpecificException(x);
+        }
+    }
+
+    /**
+     * Indicates if the network interface is up or down.
+     *
+     * @hide
+     */
+    public boolean isUp() {
+        try {
+            return mBinder.isUp();
+
+        } catch (DeadObjectException x) {
+            return false;
+
+        } catch (RemoteException x) {
+            throw x.rethrowAsRuntimeException();
+        }
+    }
+
+    /**
+     * Indicates if there is at least one peer in range.
+     *
+     * @return <code>true</code> if we have at least one other peer in range, <code>false</code>
+     *     otherwise.
+     */
+    public boolean isConnected() {
+        try {
+            return mBinder.isConnected();
+
+        } catch (DeadObjectException x) {
+            return false;
+
+        } catch (RemoteException x) {
+            throw x.rethrowAsRuntimeException();
+        }
+    }
+
+    /**
+     * Indicates if this interface is currently commissioned onto an existing network. If the
+     * interface is commissioned, the interface may be brought up using setUp().
+     */
+    public boolean isCommissioned() {
+        try {
+            return mBinder.isCommissioned();
+
+        } catch (DeadObjectException x) {
+            return false;
+
+        } catch (RemoteException x) {
+            throw x.rethrowAsRuntimeException();
+        }
+    }
+
+    /**
+     * Get interface state
+     *
+     * <h3>State Diagram</h3>
+     *
+     * <img src="LowpanInterface-1.png" />
+     *
+     * @return The current state of the interface.
+     * @see #STATE_OFFLINE
+     * @see #STATE_COMMISSIONING
+     * @see #STATE_ATTACHING
+     * @see #STATE_ATTACHED
+     * @see #STATE_FAULT
+     */
+    public String getState() {
+        try {
+            return mBinder.getState();
+
+        } catch (DeadObjectException x) {
+            return STATE_FAULT;
+
+        } catch (RemoteException x) {
+            throw x.rethrowAsRuntimeException();
+        }
+    }
+
+    /** Get network partition/fragment identifier. */
+    public String getPartitionId() {
+        try {
+            return mBinder.getPartitionId();
+
+        } catch (DeadObjectException x) {
+            return EMPTY_PARTITION_ID;
+
+        } catch (RemoteException x) {
+            throw x.rethrowAsRuntimeException();
+        }
+    }
+
+    /** TODO: doc */
+    public LowpanIdentity getLowpanIdentity() {
+        try {
+            return mBinder.getLowpanIdentity();
+
+        } catch (DeadObjectException x) {
+            return new LowpanIdentity();
+
+        } catch (RemoteException x) {
+            throw x.rethrowAsRuntimeException();
+        }
+    }
+
+    /** TODO: doc */
+    @NonNull
+    public String getRole() {
+        try {
+            return mBinder.getRole();
+
+        } catch (DeadObjectException x) {
+            return ROLE_DETACHED;
+
+        } catch (RemoteException x) {
+            throw x.rethrowAsRuntimeException();
+        }
+    }
+
+    /** TODO: doc */
+    @Nullable
+    public LowpanCredential getLowpanCredential() {
+        try {
+            return mBinder.getLowpanCredential();
+
+        } catch (RemoteException x) {
+            throw x.rethrowAsRuntimeException();
+        }
+    }
+
+    public @NonNull String[] getSupportedNetworkTypes() throws LowpanException {
+        try {
+            return mBinder.getSupportedNetworkTypes();
+
+        } catch (RemoteException x) {
+            throw x.rethrowAsRuntimeException();
+
+        } catch (ServiceSpecificException x) {
+            throw LowpanException.rethrowFromServiceSpecificException(x);
+        }
+    }
+
+    public @NonNull LowpanChannelInfo[] getSupportedChannels() throws LowpanException {
+        try {
+            return mBinder.getSupportedChannels();
+
+        } catch (RemoteException x) {
+            throw x.rethrowAsRuntimeException();
+
+        } catch (ServiceSpecificException x) {
+            throw LowpanException.rethrowFromServiceSpecificException(x);
+        }
+    }
+
+    // Listener Support
+
+    /**
+     * Registers a subclass of {@link LowpanInterface.Callback} to receive events.
+     *
+     * @param cb Subclass of {@link LowpanInterface.Callback} which will receive events.
+     * @param handler If not <code>null</code>, events will be dispatched via the given handler
+     *     object. If <code>null</code>, the thread upon which events will be dispatched is
+     *     unspecified.
+     * @see #registerCallback(Callback)
+     * @see #unregisterCallback(Callback)
+     */
+    public void registerCallback(@NonNull Callback cb, @Nullable Handler handler) {
+        ILowpanInterfaceListener.Stub listenerBinder =
+                new ILowpanInterfaceListener.Stub() {
+                    private Handler mHandler;
+
+                    {
+                        if (handler != null) {
+                            mHandler = handler;
+                        } else if (mLooper != null) {
+                            mHandler = new Handler(mLooper);
+                        } else {
+                            mHandler = new Handler();
+                        }
+                    }
+
+                    @Override
+                    public void onEnabledChanged(boolean value) {
+                        mHandler.post(() -> cb.onEnabledChanged(value));
+                    }
+
+                    @Override
+                    public void onConnectedChanged(boolean value) {
+                        mHandler.post(() -> cb.onConnectedChanged(value));
+                    }
+
+                    @Override
+                    public void onUpChanged(boolean value) {
+                        mHandler.post(() -> cb.onUpChanged(value));
+                    }
+
+                    @Override
+                    public void onRoleChanged(String value) {
+                        mHandler.post(() -> cb.onRoleChanged(value));
+                    }
+
+                    @Override
+                    public void onStateChanged(String value) {
+                        mHandler.post(() -> cb.onStateChanged(value));
+                    }
+
+                    @Override
+                    public void onLowpanIdentityChanged(LowpanIdentity value) {
+                        mHandler.post(() -> cb.onLowpanIdentityChanged(value));
+                    }
+
+                    @Override
+                    public void onLinkNetworkAdded(IpPrefix value) {
+                        mHandler.post(() -> cb.onLinkNetworkAdded(value));
+                    }
+
+                    @Override
+                    public void onLinkNetworkRemoved(IpPrefix value) {
+                        mHandler.post(() -> cb.onLinkNetworkRemoved(value));
+                    }
+
+                    @Override
+                    public void onLinkAddressAdded(String value) {
+                        LinkAddress la;
+                        try {
+                            la = new LinkAddress(value);
+                        } catch (IllegalArgumentException x) {
+                            Log.e(
+                                    TAG,
+                                    "onLinkAddressAdded: Bad LinkAddress \"" + value + "\", " + x);
+                            return;
+                        }
+                        mHandler.post(() -> cb.onLinkAddressAdded(la));
+                    }
+
+                    @Override
+                    public void onLinkAddressRemoved(String value) {
+                        LinkAddress la;
+                        try {
+                            la = new LinkAddress(value);
+                        } catch (IllegalArgumentException x) {
+                            Log.e(
+                                    TAG,
+                                    "onLinkAddressRemoved: Bad LinkAddress \""
+                                            + value
+                                            + "\", "
+                                            + x);
+                            return;
+                        }
+                        mHandler.post(() -> cb.onLinkAddressRemoved(la));
+                    }
+
+                    @Override
+                    public void onReceiveFromCommissioner(byte[] packet) {
+                        // This is only used by the LowpanCommissioningSession.
+                    }
+                };
+        try {
+            mBinder.addListener(listenerBinder);
+        } catch (RemoteException x) {
+            throw x.rethrowAsRuntimeException();
+        }
+
+        synchronized (mListenerMap) {
+            mListenerMap.put(System.identityHashCode(cb), listenerBinder);
+        }
+    }
+
+    /**
+     * Registers a subclass of {@link LowpanInterface.Callback} to receive events.
+     *
+     * <p>The thread upon which events will be dispatched is unspecified.
+     *
+     * @param cb Subclass of {@link LowpanInterface.Callback} which will receive events.
+     * @see #registerCallback(Callback, Handler)
+     * @see #unregisterCallback(Callback)
+     */
+    public void registerCallback(Callback cb) {
+        registerCallback(cb, null);
+    }
+
+    /**
+     * Unregisters a previously registered callback class.
+     *
+     * @param cb Subclass of {@link LowpanInterface.Callback} which was previously registered to
+     *     receive events.
+     * @see #registerCallback(Callback, Handler)
+     * @see #registerCallback(Callback)
+     */
+    public void unregisterCallback(Callback cb) {
+        int hashCode = System.identityHashCode(cb);
+        synchronized (mListenerMap) {
+            ILowpanInterfaceListener listenerBinder = mListenerMap.get(hashCode);
+
+            if (listenerBinder != null) {
+                mListenerMap.remove(hashCode);
+
+                try {
+                    mBinder.removeListener(listenerBinder);
+                } catch (DeadObjectException x) {
+                    // We ignore a dead object exception because that
+                    // pretty clearly means our callback isn't registered.
+                } catch (RemoteException x) {
+                    throw x.rethrowAsRuntimeException();
+                }
+            }
+        }
+    }
+
+    // Active and Passive Scanning
+
+    /**
+     * Creates a new {@link android.net.lowpan.LowpanScanner} object for this interface.
+     *
+     * <p>This method allocates a new unique object for each call.
+     *
+     * @see android.net.lowpan.LowpanScanner
+     */
+    public @NonNull LowpanScanner createScanner() {
+        return new LowpanScanner(mBinder);
+    }
+
+    // Route Management
+
+    /**
+     * Makes a copy of the internal list of LinkAddresses.
+     *
+     * @hide
+     */
+    public LinkAddress[] getLinkAddresses() throws LowpanException {
+        try {
+            String[] linkAddressStrings = mBinder.getLinkAddresses();
+            LinkAddress[] ret = new LinkAddress[linkAddressStrings.length];
+            int i = 0;
+            for (String str : linkAddressStrings) {
+                ret[i++] = new LinkAddress(str);
+            }
+            return ret;
+
+        } catch (RemoteException x) {
+            throw x.rethrowAsRuntimeException();
+
+        } catch (ServiceSpecificException x) {
+            throw LowpanException.rethrowFromServiceSpecificException(x);
+        }
+    }
+
+    /**
+     * Makes a copy of the internal list of networks reachable on via this link.
+     *
+     * @hide
+     */
+    public IpPrefix[] getLinkNetworks() throws LowpanException {
+        try {
+            return mBinder.getLinkNetworks();
+
+        } catch (RemoteException x) {
+            throw x.rethrowAsRuntimeException();
+
+        } catch (ServiceSpecificException x) {
+            throw LowpanException.rethrowFromServiceSpecificException(x);
+        }
+    }
+
+    /**
+     * Advertise the given IP prefix as an on-mesh prefix.
+     *
+     * @hide
+     */
+    public void addOnMeshPrefix(IpPrefix prefix, int flags) throws LowpanException {
+        try {
+            mBinder.addOnMeshPrefix(prefix, flags);
+
+        } catch (RemoteException x) {
+            throw x.rethrowAsRuntimeException();
+
+        } catch (ServiceSpecificException x) {
+            throw LowpanException.rethrowFromServiceSpecificException(x);
+        }
+    }
+
+    /**
+     * Remove an IP prefix previously advertised by this device from the list of advertised on-mesh
+     * prefixes.
+     *
+     * @hide
+     */
+    public void removeOnMeshPrefix(IpPrefix prefix) {
+        try {
+            mBinder.removeOnMeshPrefix(prefix);
+
+        } catch (RemoteException x) {
+            throw x.rethrowAsRuntimeException();
+
+        } catch (ServiceSpecificException x) {
+            // Catch and ignore all service exceptions
+            Log.e(TAG, x.toString());
+        }
+    }
+
+    /**
+     * Advertise this device to other devices on the mesh network as having a specific route to the
+     * given network. This device will then receive forwarded traffic for that network.
+     *
+     * @hide
+     */
+    public void addExternalRoute(IpPrefix prefix, int flags) throws LowpanException {
+        try {
+            mBinder.addExternalRoute(prefix, flags);
+
+        } catch (RemoteException x) {
+            throw x.rethrowAsRuntimeException();
+
+        } catch (ServiceSpecificException x) {
+            throw LowpanException.rethrowFromServiceSpecificException(x);
+        }
+    }
+
+    /**
+     * Revoke a previously advertised specific route to the given network.
+     *
+     * @hide
+     */
+    public void removeExternalRoute(IpPrefix prefix) {
+        try {
+            mBinder.removeExternalRoute(prefix);
+
+        } catch (RemoteException x) {
+            throw x.rethrowAsRuntimeException();
+
+        } catch (ServiceSpecificException x) {
+            // Catch and ignore all service exceptions
+            Log.e(TAG, x.toString());
+        }
+    }
+}
diff --git a/android/net/lowpan/LowpanManager.java b/android/net/lowpan/LowpanManager.java
new file mode 100644
index 0000000..76876ce
--- /dev/null
+++ b/android/net/lowpan/LowpanManager.java
@@ -0,0 +1,327 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.lowpan;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import java.lang.ref.WeakReference;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.WeakHashMap;
+
+/**
+ * Manager object for looking up LoWPAN interfaces.
+ *
+ * @hide
+ */
+// @SystemApi
+public class LowpanManager {
+    private static final String TAG = LowpanManager.class.getSimpleName();
+
+    /** @hide */
+    // @SystemApi
+    public abstract static class Callback {
+        public void onInterfaceAdded(LowpanInterface lowpanInterface) {}
+
+        public void onInterfaceRemoved(LowpanInterface lowpanInterface) {}
+    }
+
+    private final Map<Integer, ILowpanManagerListener> mListenerMap = new HashMap<>();
+    private final Map<String, LowpanInterface> mInterfaceCache = new HashMap<>();
+
+    /* This is a WeakHashMap because we don't want to hold onto
+     * a strong reference to ILowpanInterface, so that it can be
+     * garbage collected if it isn't being used anymore. Since
+     * the value class holds onto this specific ILowpanInterface,
+     * we also need to have a weak reference to the value.
+     * This design pattern allows us to skip removal of items
+     * from this Map without leaking memory.
+     */
+    private final Map<IBinder, WeakReference<LowpanInterface>> mBinderCache =
+            new WeakHashMap<>();
+
+    private final ILowpanManager mService;
+    private final Context mContext;
+    private final Looper mLooper;
+
+    // Static Methods
+
+    public static LowpanManager from(Context context) {
+        return (LowpanManager) context.getSystemService(Context.LOWPAN_SERVICE);
+    }
+
+    /** @hide */
+    public static LowpanManager getManager() {
+        IBinder binder = ServiceManager.getService(Context.LOWPAN_SERVICE);
+
+        if (binder != null) {
+            ILowpanManager service = ILowpanManager.Stub.asInterface(binder);
+            return new LowpanManager(service);
+        }
+
+        return null;
+    }
+
+    // Constructors
+
+    LowpanManager(ILowpanManager service) {
+        mService = service;
+        mContext = null;
+        mLooper = null;
+    }
+
+    /**
+     * Create a new LowpanManager instance. Applications will almost always want to use {@link
+     * android.content.Context#getSystemService Context.getSystemService()} to retrieve the standard
+     * {@link android.content.Context#LOWPAN_SERVICE Context.LOWPAN_SERVICE}.
+     *
+     * @param context the application context
+     * @param service the Binder interface
+     * @param looper the default Looper to run callbacks on
+     * @hide - hide this because it takes in a parameter of type ILowpanManager, which is a system
+     *     private class.
+     */
+    public LowpanManager(Context context, ILowpanManager service, Looper looper) {
+        mContext = context;
+        mService = service;
+        mLooper = looper;
+    }
+
+    /** @hide */
+    @Nullable
+    public LowpanInterface getInterfaceNoCreate(@NonNull ILowpanInterface ifaceService) {
+        LowpanInterface iface = null;
+
+        synchronized (mBinderCache) {
+            if (mBinderCache.containsKey(ifaceService.asBinder())) {
+                iface = mBinderCache.get(ifaceService.asBinder()).get();
+            }
+        }
+
+        return iface;
+    }
+
+    /** @hide */
+    @Nullable
+    public LowpanInterface getInterface(@NonNull ILowpanInterface ifaceService) {
+        LowpanInterface iface = null;
+
+        try {
+            synchronized (mBinderCache) {
+                if (mBinderCache.containsKey(ifaceService.asBinder())) {
+                    iface = mBinderCache.get(ifaceService.asBinder()).get();
+                }
+
+                if (iface == null) {
+                    String ifaceName = ifaceService.getName();
+
+                    iface = new LowpanInterface(mContext, ifaceService, mLooper);
+
+                    synchronized (mInterfaceCache) {
+                        mInterfaceCache.put(iface.getName(), iface);
+                    }
+
+                    mBinderCache.put(ifaceService.asBinder(), new WeakReference(iface));
+
+                    /* Make sure we remove the object from the
+                     * interface cache if the associated service
+                     * dies.
+                     */
+                    ifaceService
+                            .asBinder()
+                            .linkToDeath(
+                                    new IBinder.DeathRecipient() {
+                                        @Override
+                                        public void binderDied() {
+                                            synchronized (mInterfaceCache) {
+                                                LowpanInterface iface =
+                                                        mInterfaceCache.get(ifaceName);
+
+                                                if ((iface != null)
+                                                        && (iface.getService() == ifaceService)) {
+                                                    mInterfaceCache.remove(ifaceName);
+                                                }
+                                            }
+                                        }
+                                    },
+                                    0);
+                }
+            }
+        } catch (RemoteException x) {
+            throw x.rethrowAsRuntimeException();
+        }
+
+        return iface;
+    }
+
+    /**
+     * Returns a reference to the requested LowpanInterface object. If the given interface doesn't
+     * exist, or it is not a LoWPAN interface, returns null.
+     */
+    @Nullable
+    public LowpanInterface getInterface(@NonNull String name) {
+        LowpanInterface iface = null;
+
+        try {
+            /* This synchronized block covers both branches of the enclosed
+             * if() statement in order to avoid a race condition. Two threads
+             * calling getInterface() with the same name would race to create
+             * the associated LowpanInterface object, creating two of them.
+             * Having the whole block be synchronized avoids that race.
+             */
+            synchronized (mInterfaceCache) {
+                if (mInterfaceCache.containsKey(name)) {
+                    iface = mInterfaceCache.get(name);
+
+                } else {
+                    ILowpanInterface ifaceService = mService.getInterface(name);
+
+                    if (ifaceService != null) {
+                        iface = getInterface(ifaceService);
+                    }
+                }
+            }
+        } catch (RemoteException x) {
+            throw x.rethrowFromSystemServer();
+        }
+
+        return iface;
+    }
+
+    /**
+     * Returns a reference to the first registered LowpanInterface object. If there are no LoWPAN
+     * interfaces registered, returns null.
+     */
+    @Nullable
+    public LowpanInterface getInterface() {
+        String[] ifaceList = getInterfaceList();
+        if (ifaceList.length > 0) {
+            return getInterface(ifaceList[0]);
+        }
+        return null;
+    }
+
+    /**
+     * Returns a string array containing the names of LoWPAN interfaces. This list may contain fewer
+     * interfaces if the calling process does not have permissions to see individual interfaces.
+     */
+    @NonNull
+    public String[] getInterfaceList() {
+        try {
+            return mService.getInterfaceList();
+        } catch (RemoteException x) {
+            throw x.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Registers a callback object to receive notifications when LoWPAN interfaces are added or
+     * removed.
+     *
+     * @hide
+     */
+    public void registerCallback(@NonNull Callback cb, @Nullable Handler handler)
+            throws LowpanException {
+        ILowpanManagerListener.Stub listenerBinder =
+                new ILowpanManagerListener.Stub() {
+                    private Handler mHandler;
+
+                    {
+                        if (handler != null) {
+                            mHandler = handler;
+                        } else if (mLooper != null) {
+                            mHandler = new Handler(mLooper);
+                        } else {
+                            mHandler = new Handler();
+                        }
+                    }
+
+                    @Override
+                    public void onInterfaceAdded(ILowpanInterface ifaceService) {
+                        Runnable runnable =
+                                () -> {
+                                    LowpanInterface iface = getInterface(ifaceService);
+
+                                    if (iface != null) {
+                                        cb.onInterfaceAdded(iface);
+                                    }
+                                };
+
+                        mHandler.post(runnable);
+                    }
+
+                    @Override
+                    public void onInterfaceRemoved(ILowpanInterface ifaceService) {
+                        Runnable runnable =
+                                () -> {
+                                    LowpanInterface iface = getInterfaceNoCreate(ifaceService);
+
+                                    if (iface != null) {
+                                        cb.onInterfaceRemoved(iface);
+                                    }
+                                };
+
+                        mHandler.post(runnable);
+                    }
+                };
+        try {
+            mService.addListener(listenerBinder);
+        } catch (RemoteException x) {
+            throw x.rethrowFromSystemServer();
+        }
+
+        synchronized (mListenerMap) {
+            mListenerMap.put(Integer.valueOf(System.identityHashCode(cb)), listenerBinder);
+        }
+    }
+
+    /** @hide */
+    public void registerCallback(@NonNull Callback cb) throws LowpanException {
+        registerCallback(cb, null);
+    }
+
+    /**
+     * Unregisters a previously registered {@link LowpanManager.Callback} object.
+     *
+     * @hide
+     */
+    public void unregisterCallback(@NonNull Callback cb) {
+        Integer hashCode = Integer.valueOf(System.identityHashCode(cb));
+        ILowpanManagerListener listenerBinder = null;
+
+        synchronized (mListenerMap) {
+            listenerBinder = mListenerMap.get(hashCode);
+            mListenerMap.remove(hashCode);
+        }
+
+        if (listenerBinder != null) {
+            try {
+                mService.removeListener(listenerBinder);
+            } catch (RemoteException x) {
+                throw x.rethrowFromSystemServer();
+            }
+        } else {
+            throw new RuntimeException("Attempt to unregister an unknown callback");
+        }
+    }
+}
diff --git a/android/net/lowpan/LowpanProperties.java b/android/net/lowpan/LowpanProperties.java
new file mode 100644
index 0000000..cc45ff8
--- /dev/null
+++ b/android/net/lowpan/LowpanProperties.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.lowpan;
+
+/** {@hide} */
+public final class LowpanProperties {
+
+    public static final LowpanProperty<int[]> KEY_CHANNEL_MASK =
+            new LowpanStandardProperty("android.net.lowpan.property.CHANNEL_MASK", int[].class);
+
+    public static final LowpanProperty<Integer> KEY_MAX_TX_POWER =
+            new LowpanStandardProperty("android.net.lowpan.property.MAX_TX_POWER", Integer.class);
+
+    /** @hide */
+    private LowpanProperties() {}
+
+    /** @hide */
+    static final class LowpanStandardProperty<T> extends LowpanProperty<T> {
+        private final String mName;
+        private final Class<T> mType;
+
+        LowpanStandardProperty(String name, Class<T> type) {
+            mName = name;
+            mType = type;
+        }
+
+        @Override
+        public String getName() {
+            return mName;
+        }
+
+        @Override
+        public Class<T> getType() {
+            return mType;
+        }
+
+        @Override
+        public String toString() {
+            return getName();
+        }
+    }
+}
diff --git a/android/net/lowpan/LowpanProperty.java b/android/net/lowpan/LowpanProperty.java
new file mode 100644
index 0000000..7f26986
--- /dev/null
+++ b/android/net/lowpan/LowpanProperty.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.lowpan;
+
+import java.util.Map;
+
+/** {@hide} */
+public abstract class LowpanProperty<T> {
+    public abstract String getName();
+
+    public abstract Class<T> getType();
+
+    public void putInMap(Map map, T value) {
+        map.put(getName(), value);
+    }
+
+    public T getFromMap(Map map) {
+        return (T) map.get(getName());
+    }
+}
diff --git a/android/net/lowpan/LowpanProvision.java b/android/net/lowpan/LowpanProvision.java
new file mode 100644
index 0000000..68c8709
--- /dev/null
+++ b/android/net/lowpan/LowpanProvision.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.lowpan;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import java.util.Objects;
+
+/**
+ * Describes the information needed to describe a network
+ *
+ * @hide
+ */
+// @SystemApi
+public class LowpanProvision implements Parcelable {
+
+    // Builder
+
+    /** @hide */
+    // @SystemApi
+    public static class Builder {
+        private final LowpanProvision provision = new LowpanProvision();
+
+        public Builder setLowpanIdentity(@NonNull LowpanIdentity identity) {
+            provision.mIdentity = identity;
+            return this;
+        }
+
+        public Builder setLowpanCredential(@NonNull LowpanCredential credential) {
+            provision.mCredential = credential;
+            return this;
+        }
+
+        public LowpanProvision build() {
+            return provision;
+        }
+    }
+
+    private LowpanProvision() {}
+
+    // Instance Variables
+
+    private LowpanIdentity mIdentity = new LowpanIdentity();
+    private LowpanCredential mCredential = null;
+
+    // Public Getters and Setters
+
+    @NonNull
+    public LowpanIdentity getLowpanIdentity() {
+        return mIdentity;
+    }
+
+    @Nullable
+    public LowpanCredential getLowpanCredential() {
+        return mCredential;
+    }
+
+    @Override
+    public String toString() {
+        StringBuffer sb = new StringBuffer();
+
+        sb.append("LowpanProvision { identity => ").append(mIdentity.toString());
+
+        if (mCredential != null) {
+            sb.append(", credential => ").append(mCredential.toString());
+        }
+
+        sb.append("}");
+
+        return sb.toString();
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mIdentity, mCredential);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof LowpanProvision)) {
+            return false;
+        }
+        LowpanProvision rhs = (LowpanProvision) obj;
+
+        if (!mIdentity.equals(rhs.mIdentity)) {
+            return false;
+        }
+
+        if (!Objects.equals(mCredential, rhs.mCredential)) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /** Implement the Parcelable interface. */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Implement the Parcelable interface. */
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        mIdentity.writeToParcel(dest, flags);
+        if (mCredential == null) {
+            dest.writeBoolean(false);
+        } else {
+            dest.writeBoolean(true);
+            mCredential.writeToParcel(dest, flags);
+        }
+    }
+
+    /** Implement the Parcelable interface. */
+    public static final @android.annotation.NonNull Creator<LowpanProvision> CREATOR =
+            new Creator<LowpanProvision>() {
+                public LowpanProvision createFromParcel(Parcel in) {
+                    Builder builder = new Builder();
+
+                    builder.setLowpanIdentity(LowpanIdentity.CREATOR.createFromParcel(in));
+
+                    if (in.readBoolean()) {
+                        builder.setLowpanCredential(LowpanCredential.CREATOR.createFromParcel(in));
+                    }
+
+                    return builder.build();
+                }
+
+                public LowpanProvision[] newArray(int size) {
+                    return new LowpanProvision[size];
+                }
+            };
+};
diff --git a/android/net/lowpan/LowpanRuntimeException.java b/android/net/lowpan/LowpanRuntimeException.java
new file mode 100644
index 0000000..71a5a13
--- /dev/null
+++ b/android/net/lowpan/LowpanRuntimeException.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.lowpan;
+
+import android.util.AndroidRuntimeException;
+
+/**
+ * Generic runtime exception for LoWPAN operations.
+ *
+ * @hide
+ */
+// @SystemApi
+public class LowpanRuntimeException extends AndroidRuntimeException {
+
+    public LowpanRuntimeException() {}
+
+    public LowpanRuntimeException(String message) {
+        super(message);
+    }
+
+    public LowpanRuntimeException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public LowpanRuntimeException(Exception cause) {
+        super(cause);
+    }
+}
diff --git a/android/net/lowpan/LowpanScanner.java b/android/net/lowpan/LowpanScanner.java
new file mode 100644
index 0000000..59156c4
--- /dev/null
+++ b/android/net/lowpan/LowpanScanner.java
@@ -0,0 +1,326 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.lowpan;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Handler;
+import android.os.RemoteException;
+import android.os.ServiceSpecificException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * LoWPAN Scanner
+ *
+ * <p>This class allows performing network (active) scans and energy (passive) scans.
+ *
+ * @see LowpanInterface
+ * @hide
+ */
+// @SystemApi
+public class LowpanScanner {
+    private static final String TAG = LowpanScanner.class.getSimpleName();
+
+    // Public Classes
+
+    /**
+     * Callback base class for LowpanScanner
+     *
+     * @hide
+     */
+    // @SystemApi
+    public abstract static class Callback {
+        public void onNetScanBeacon(LowpanBeaconInfo beacon) {}
+
+        public void onEnergyScanResult(LowpanEnergyScanResult result) {}
+
+        public void onScanFinished() {}
+    }
+
+    // Instance Variables
+
+    private ILowpanInterface mBinder;
+    private Callback mCallback = null;
+    private Handler mHandler = null;
+    private ArrayList<Integer> mChannelMask = null;
+    private int mTxPower = Integer.MAX_VALUE;
+
+    // Constructors/Accessors and Exception Glue
+
+    LowpanScanner(@NonNull ILowpanInterface binder) {
+        mBinder = binder;
+    }
+
+    /** Sets an instance of {@link LowpanScanner.Callback} to receive events. */
+    public synchronized void setCallback(@Nullable Callback cb, @Nullable Handler handler) {
+        mCallback = cb;
+        mHandler = handler;
+    }
+
+    /** Sets an instance of {@link LowpanScanner.Callback} to receive events. */
+    public void setCallback(@Nullable Callback cb) {
+        setCallback(cb, null);
+    }
+
+    /**
+     * Sets the channel mask to use when scanning.
+     *
+     * @param mask The channel mask to use when scanning. If <code>null</code>, any previously set
+     *     channel mask will be cleared and all channels not masked by the current regulatory zone
+     *     will be scanned.
+     */
+    public void setChannelMask(@Nullable Collection<Integer> mask) {
+        if (mask == null) {
+            mChannelMask = null;
+        } else {
+            if (mChannelMask == null) {
+                mChannelMask = new ArrayList<>();
+            } else {
+                mChannelMask.clear();
+            }
+            mChannelMask.addAll(mask);
+        }
+    }
+
+    /**
+     * Gets the current channel mask.
+     *
+     * @return the current channel mask, or <code>null</code> if no channel mask is currently set.
+     */
+    public @Nullable Collection<Integer> getChannelMask() {
+        return (Collection<Integer>) mChannelMask.clone();
+    }
+
+    /**
+     * Adds a channel to the channel mask used for scanning.
+     *
+     * <p>If a channel mask was previously <code>null</code>, a new one is created containing only
+     * this channel. May be called multiple times to add additional channels ot the channel mask.
+     *
+     * @see #setChannelMask
+     * @see #getChannelMask
+     * @see #getTxPower
+     */
+    public void addChannel(int channel) {
+        if (mChannelMask == null) {
+            mChannelMask = new ArrayList<>();
+        }
+        mChannelMask.add(Integer.valueOf(channel));
+    }
+
+    /**
+     * Sets the maximum transmit power to be used for active scanning.
+     *
+     * <p>The actual transmit power used is the lesser of this value and the currently configured
+     * maximum transmit power for the interface.
+     *
+     * @see #getTxPower
+     */
+    public void setTxPower(int txPower) {
+        mTxPower = txPower;
+    }
+
+    /**
+     * Gets the maximum transmit power used for active scanning.
+     *
+     * @see #setTxPower
+     */
+    public int getTxPower() {
+        return mTxPower;
+    }
+
+    private Map<String, Object> createScanOptionMap() {
+        Map<String, Object> map = new HashMap();
+
+        if (mChannelMask != null) {
+            LowpanProperties.KEY_CHANNEL_MASK.putInMap(
+                    map, mChannelMask.stream().mapToInt(i -> i).toArray());
+        }
+
+        if (mTxPower != Integer.MAX_VALUE) {
+            LowpanProperties.KEY_MAX_TX_POWER.putInMap(map, Integer.valueOf(mTxPower));
+        }
+
+        return map;
+    }
+
+    /**
+     * Start a network scan.
+     *
+     * <p>This method will return once the scan has started.
+     *
+     * @see #stopNetScan
+     */
+    public void startNetScan() throws LowpanException {
+        Map<String, Object> map = createScanOptionMap();
+
+        ILowpanNetScanCallback binderListener =
+                new ILowpanNetScanCallback.Stub() {
+                    public void onNetScanBeacon(LowpanBeaconInfo beaconInfo) {
+                        Callback callback;
+                        Handler handler;
+
+                        synchronized (LowpanScanner.this) {
+                            callback = mCallback;
+                            handler = mHandler;
+                        }
+
+                        if (callback == null) {
+                            return;
+                        }
+
+                        Runnable runnable = () -> callback.onNetScanBeacon(beaconInfo);
+
+                        if (handler != null) {
+                            handler.post(runnable);
+                        } else {
+                            runnable.run();
+                        }
+                    }
+
+                    public void onNetScanFinished() {
+                        Callback callback;
+                        Handler handler;
+
+                        synchronized (LowpanScanner.this) {
+                            callback = mCallback;
+                            handler = mHandler;
+                        }
+
+                        if (callback == null) {
+                            return;
+                        }
+
+                        Runnable runnable = () -> callback.onScanFinished();
+
+                        if (handler != null) {
+                            handler.post(runnable);
+                        } else {
+                            runnable.run();
+                        }
+                    }
+                };
+
+        try {
+            mBinder.startNetScan(map, binderListener);
+
+        } catch (RemoteException x) {
+            throw x.rethrowAsRuntimeException();
+
+        } catch (ServiceSpecificException x) {
+            throw LowpanException.rethrowFromServiceSpecificException(x);
+        }
+    }
+
+    /**
+     * Stop a network scan currently in progress.
+     *
+     * @see #startNetScan
+     */
+    public void stopNetScan() {
+        try {
+            mBinder.stopNetScan();
+
+        } catch (RemoteException x) {
+            throw x.rethrowAsRuntimeException();
+        }
+    }
+
+    /**
+     * Start an energy scan.
+     *
+     * <p>This method will return once the scan has started.
+     *
+     * @see #stopEnergyScan
+     */
+    public void startEnergyScan() throws LowpanException {
+        Map<String, Object> map = createScanOptionMap();
+
+        ILowpanEnergyScanCallback binderListener =
+                new ILowpanEnergyScanCallback.Stub() {
+                    public void onEnergyScanResult(int channel, int rssi) {
+                        Callback callback = mCallback;
+                        Handler handler = mHandler;
+
+                        if (callback == null) {
+                            return;
+                        }
+
+                        Runnable runnable =
+                                () -> {
+                                    if (callback != null) {
+                                        LowpanEnergyScanResult result =
+                                                new LowpanEnergyScanResult();
+                                        result.setChannel(channel);
+                                        result.setMaxRssi(rssi);
+                                        callback.onEnergyScanResult(result);
+                                    }
+                                };
+
+                        if (handler != null) {
+                            handler.post(runnable);
+                        } else {
+                            runnable.run();
+                        }
+                    }
+
+                    public void onEnergyScanFinished() {
+                        Callback callback = mCallback;
+                        Handler handler = mHandler;
+
+                        if (callback == null) {
+                            return;
+                        }
+
+                        Runnable runnable = () -> callback.onScanFinished();
+
+                        if (handler != null) {
+                            handler.post(runnable);
+                        } else {
+                            runnable.run();
+                        }
+                    }
+                };
+
+        try {
+            mBinder.startEnergyScan(map, binderListener);
+
+        } catch (RemoteException x) {
+            throw x.rethrowAsRuntimeException();
+
+        } catch (ServiceSpecificException x) {
+            throw LowpanException.rethrowFromServiceSpecificException(x);
+        }
+    }
+
+    /**
+     * Stop an energy scan currently in progress.
+     *
+     * @see #startEnergyScan
+     */
+    public void stopEnergyScan() {
+        try {
+            mBinder.stopEnergyScan();
+
+        } catch (RemoteException x) {
+            throw x.rethrowAsRuntimeException();
+        }
+    }
+}
diff --git a/android/net/lowpan/NetworkAlreadyExistsException.java b/android/net/lowpan/NetworkAlreadyExistsException.java
new file mode 100644
index 0000000..90ef498
--- /dev/null
+++ b/android/net/lowpan/NetworkAlreadyExistsException.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.lowpan;
+
+/**
+ * Exception indicating the form operation found a network nearby with the same identity.
+ *
+ * @see LowpanInterface
+ * @hide
+ */
+// @SystemApi
+public class NetworkAlreadyExistsException extends LowpanException {
+
+    public NetworkAlreadyExistsException() {}
+
+    public NetworkAlreadyExistsException(String message) {
+        super(message, null);
+    }
+
+    public NetworkAlreadyExistsException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public NetworkAlreadyExistsException(Exception cause) {
+        super(cause);
+    }
+}
diff --git a/android/net/lowpan/OperationCanceledException.java b/android/net/lowpan/OperationCanceledException.java
new file mode 100644
index 0000000..fcafe3a
--- /dev/null
+++ b/android/net/lowpan/OperationCanceledException.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.lowpan;
+
+/**
+ * Exception indicating this operation was canceled by the driver before it could finish.
+ *
+ * @see LowpanInterface
+ * @hide
+ */
+// @SystemApi
+public class OperationCanceledException extends LowpanException {
+
+    public OperationCanceledException() {}
+
+    public OperationCanceledException(String message) {
+        super(message);
+    }
+
+    public OperationCanceledException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    protected OperationCanceledException(Exception cause) {
+        super(cause);
+    }
+}
diff --git a/android/net/lowpan/WrongStateException.java b/android/net/lowpan/WrongStateException.java
new file mode 100644
index 0000000..3565419
--- /dev/null
+++ b/android/net/lowpan/WrongStateException.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.lowpan;
+
+/**
+ * Exception indicating the interface is the wrong state for an operation.
+ *
+ * @see LowpanInterface
+ * @hide
+ */
+// @SystemApi
+public class WrongStateException extends LowpanException {
+
+    public WrongStateException() {}
+
+    public WrongStateException(String message) {
+        super(message);
+    }
+
+    public WrongStateException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    protected WrongStateException(Exception cause) {
+        super(cause);
+    }
+}
diff --git a/android/net/metrics/ApfProgramEvent.java b/android/net/metrics/ApfProgramEvent.java
new file mode 100644
index 0000000..e9c209c
--- /dev/null
+++ b/android/net/metrics/ApfProgramEvent.java
@@ -0,0 +1,247 @@
+/*
+ * 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.metrics;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.annotation.UnsupportedAppUsage;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.SparseArray;
+
+import com.android.internal.util.MessageUtils;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.BitSet;
+import java.util.List;
+
+/**
+ * An event logged when there is a change or event that requires updating the
+ * the APF program in place with a new APF program.
+ * {@hide}
+ */
+@TestApi
+@SystemApi
+public final class ApfProgramEvent implements IpConnectivityLog.Event {
+
+    // Bitflag constants describing what an Apf program filters.
+    // Bits are indexeds from LSB to MSB, starting at index 0.
+    /** @hide */
+    public static final int FLAG_MULTICAST_FILTER_ON = 0;
+    /** @hide */
+    public static final int FLAG_HAS_IPV4_ADDRESS    = 1;
+
+    /** {@hide} */
+    @IntDef(flag = true, value = {FLAG_MULTICAST_FILTER_ON, FLAG_HAS_IPV4_ADDRESS})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Flags {}
+
+    /** @hide */
+    @UnsupportedAppUsage
+    public final long lifetime;       // Maximum computed lifetime of the program in seconds
+    /** @hide */
+    @UnsupportedAppUsage
+    public final long actualLifetime; // Effective program lifetime in seconds
+    /** @hide */
+    @UnsupportedAppUsage
+    public final int filteredRas;     // Number of RAs filtered by the APF program
+    /** @hide */
+    @UnsupportedAppUsage
+    public final int currentRas;      // Total number of current RAs at generation time
+    /** @hide */
+    @UnsupportedAppUsage
+    public final int programLength;   // Length of the APF program in bytes
+    /** @hide */
+    @UnsupportedAppUsage
+    public final int flags;           // Bitfield compound of FLAG_* constants
+
+    private ApfProgramEvent(long lifetime, long actualLifetime, int filteredRas, int currentRas,
+            int programLength, int flags) {
+        this.lifetime = lifetime;
+        this.actualLifetime = actualLifetime;
+        this.filteredRas = filteredRas;
+        this.currentRas = currentRas;
+        this.programLength = programLength;
+        this.flags = flags;
+    }
+
+    private ApfProgramEvent(Parcel in) {
+        this.lifetime = in.readLong();
+        this.actualLifetime = in.readLong();
+        this.filteredRas = in.readInt();
+        this.currentRas = in.readInt();
+        this.programLength = in.readInt();
+        this.flags = in.readInt();
+    }
+
+    /**
+     * Utility to create an instance of {@link ApfProgramEvent}.
+     */
+    public static final class Builder {
+        private long mLifetime;
+        private long mActualLifetime;
+        private int mFilteredRas;
+        private int mCurrentRas;
+        private int mProgramLength;
+        private int mFlags;
+
+        /**
+         * Set the maximum computed lifetime of the program in seconds.
+         */
+        @NonNull
+        public Builder setLifetime(long lifetime) {
+            mLifetime = lifetime;
+            return this;
+        }
+
+        /**
+         * Set the effective program lifetime in seconds.
+         */
+        @NonNull
+        public Builder setActualLifetime(long lifetime) {
+            mActualLifetime = lifetime;
+            return this;
+        }
+
+        /**
+         * Set the number of RAs filtered by the APF program.
+         */
+        @NonNull
+        public Builder setFilteredRas(int filteredRas) {
+            mFilteredRas = filteredRas;
+            return this;
+        }
+
+        /**
+         * Set the total number of current RAs at generation time.
+         */
+        @NonNull
+        public Builder setCurrentRas(int currentRas) {
+            mCurrentRas = currentRas;
+            return this;
+        }
+
+        /**
+         * Set the length of the APF program in bytes.
+         */
+        @NonNull
+        public Builder setProgramLength(int programLength) {
+            mProgramLength = programLength;
+            return this;
+        }
+
+        /**
+         * Set the flags describing what an Apf program filters.
+         */
+        @NonNull
+        public Builder setFlags(boolean hasIPv4, boolean multicastFilterOn) {
+            mFlags = flagsFor(hasIPv4, multicastFilterOn);
+            return this;
+        }
+
+        /**
+         * Build a new {@link ApfProgramEvent}.
+         */
+        @NonNull
+        public ApfProgramEvent build() {
+            return new ApfProgramEvent(mLifetime, mActualLifetime, mFilteredRas, mCurrentRas,
+                    mProgramLength, mFlags);
+        }
+    }
+
+    /** @hide */
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeLong(lifetime);
+        out.writeLong(actualLifetime);
+        out.writeInt(filteredRas);
+        out.writeInt(currentRas);
+        out.writeInt(programLength);
+        out.writeInt(this.flags);
+    }
+
+    /** @hide */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public String toString() {
+        String lifetimeString = (lifetime < Long.MAX_VALUE) ? lifetime + "s" : "forever";
+        return String.format("ApfProgramEvent(%d/%d RAs %dB %ds/%s %s)", filteredRas, currentRas,
+                programLength, actualLifetime, lifetimeString, namesOf(flags));
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null || !(obj.getClass().equals(ApfProgramEvent.class))) return false;
+        final ApfProgramEvent other = (ApfProgramEvent) obj;
+        return lifetime == other.lifetime
+                && actualLifetime == other.actualLifetime
+                && filteredRas == other.filteredRas
+                && currentRas == other.currentRas
+                && programLength == other.programLength
+                && flags == other.flags;
+    }
+
+    /** @hide */
+    public static final @android.annotation.NonNull Parcelable.Creator<ApfProgramEvent> CREATOR
+            = new Parcelable.Creator<ApfProgramEvent>() {
+        public ApfProgramEvent createFromParcel(Parcel in) {
+            return new ApfProgramEvent(in);
+        }
+
+        public ApfProgramEvent[] newArray(int size) {
+            return new ApfProgramEvent[size];
+        }
+    };
+
+    /** @hide */
+    @UnsupportedAppUsage
+    public static @Flags int flagsFor(boolean hasIPv4, boolean multicastFilterOn) {
+        int bitfield = 0;
+        if (hasIPv4) {
+            bitfield |= (1 << FLAG_HAS_IPV4_ADDRESS);
+        }
+        if (multicastFilterOn) {
+            bitfield |= (1 << FLAG_MULTICAST_FILTER_ON);
+        }
+        return bitfield;
+    }
+
+    private static String namesOf(@Flags int bitfield) {
+        List<String> names = new ArrayList<>(Integer.bitCount(bitfield));
+        BitSet set = BitSet.valueOf(new long[]{bitfield & Integer.MAX_VALUE});
+        // Only iterate over flag bits which are set.
+        for (int bit = set.nextSetBit(0); bit >= 0; bit = set.nextSetBit(bit+1)) {
+            names.add(Decoder.constants.get(bit));
+        }
+        return TextUtils.join("|", names);
+    }
+
+    final static class Decoder {
+        static final SparseArray<String> constants =
+                MessageUtils.findMessageNames(
+                       new Class[]{ApfProgramEvent.class}, new String[]{"FLAG_"});
+    }
+}
diff --git a/android/net/metrics/ApfStats.java b/android/net/metrics/ApfStats.java
new file mode 100644
index 0000000..b963777
--- /dev/null
+++ b/android/net/metrics/ApfStats.java
@@ -0,0 +1,304 @@
+/*
+ * 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.metrics;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.annotation.UnsupportedAppUsage;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * An event logged for an interface with APF capabilities when its IpClient state machine exits.
+ * {@hide}
+ */
+@SystemApi
+@TestApi
+public final class ApfStats implements IpConnectivityLog.Event {
+
+    /**
+     * time interval in milliseconds these stastistics covers.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public final long durationMs;
+    /**
+     * number of received RAs.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public final int receivedRas;
+    /**
+     * number of received RAs matching a known RA.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public final int matchingRas;
+    /**
+     * number of received RAs ignored due to the MAX_RAS limit.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public final int droppedRas;
+    /**
+     * number of received RAs with a minimum lifetime of 0.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public final int zeroLifetimeRas;
+    /**
+     * number of received RAs that could not be parsed.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public final int parseErrors;
+    /**
+     * number of APF program updates from receiving RAs.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public final int programUpdates;
+    /**
+     * total number of APF program updates.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public final int programUpdatesAll;
+    /**
+     * number of APF program updates from allowing multicast traffic.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public final int programUpdatesAllowingMulticast;
+    /**
+     * maximum APF program size advertised by hardware.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public final int maxProgramSize;
+
+    private ApfStats(Parcel in) {
+        this.durationMs = in.readLong();
+        this.receivedRas = in.readInt();
+        this.matchingRas = in.readInt();
+        this.droppedRas = in.readInt();
+        this.zeroLifetimeRas = in.readInt();
+        this.parseErrors = in.readInt();
+        this.programUpdates = in.readInt();
+        this.programUpdatesAll = in.readInt();
+        this.programUpdatesAllowingMulticast = in.readInt();
+        this.maxProgramSize = in.readInt();
+    }
+
+    private ApfStats(long durationMs, int receivedRas, int matchingRas, int droppedRas,
+            int zeroLifetimeRas, int parseErrors, int programUpdates, int programUpdatesAll,
+            int programUpdatesAllowingMulticast, int maxProgramSize) {
+        this.durationMs = durationMs;
+        this.receivedRas = receivedRas;
+        this.matchingRas = matchingRas;
+        this.droppedRas = droppedRas;
+        this.zeroLifetimeRas = zeroLifetimeRas;
+        this.parseErrors = parseErrors;
+        this.programUpdates = programUpdates;
+        this.programUpdatesAll = programUpdatesAll;
+        this.programUpdatesAllowingMulticast = programUpdatesAllowingMulticast;
+        this.maxProgramSize = maxProgramSize;
+    }
+
+    /**
+     * Utility to create an instance of {@link ApfStats}.
+     * @hide
+     */
+    @SystemApi
+    @TestApi
+    public static final class Builder {
+        private long mDurationMs;
+        private int mReceivedRas;
+        private int mMatchingRas;
+        private int mDroppedRas;
+        private int mZeroLifetimeRas;
+        private int mParseErrors;
+        private int mProgramUpdates;
+        private int mProgramUpdatesAll;
+        private int mProgramUpdatesAllowingMulticast;
+        private int mMaxProgramSize;
+
+        /**
+         * Set the time interval in milliseconds these statistics covers.
+         */
+        @NonNull
+        public Builder setDurationMs(long durationMs) {
+            mDurationMs = durationMs;
+            return this;
+        }
+
+        /**
+         * Set the number of received RAs.
+         */
+        @NonNull
+        public Builder setReceivedRas(int receivedRas) {
+            mReceivedRas = receivedRas;
+            return this;
+        }
+
+        /**
+         * Set the number of received RAs matching a known RA.
+         */
+        @NonNull
+        public Builder setMatchingRas(int matchingRas) {
+            mMatchingRas = matchingRas;
+            return this;
+        }
+
+        /**
+         * Set the number of received RAs ignored due to the MAX_RAS limit.
+         */
+        @NonNull
+        public Builder setDroppedRas(int droppedRas) {
+            mDroppedRas = droppedRas;
+            return this;
+        }
+
+        /**
+         * Set the number of received RAs with a minimum lifetime of 0.
+         */
+        @NonNull
+        public Builder setZeroLifetimeRas(int zeroLifetimeRas) {
+            mZeroLifetimeRas = zeroLifetimeRas;
+            return this;
+        }
+
+        /**
+         * Set the number of received RAs that could not be parsed.
+         */
+        @NonNull
+        public Builder setParseErrors(int parseErrors) {
+            mParseErrors = parseErrors;
+            return this;
+        }
+
+        /**
+         * Set the number of APF program updates from receiving RAs.
+         */
+        @NonNull
+        public Builder setProgramUpdates(int programUpdates) {
+            mProgramUpdates = programUpdates;
+            return this;
+        }
+
+        /**
+         * Set the total number of APF program updates.
+         */
+        @NonNull
+        public Builder setProgramUpdatesAll(int programUpdatesAll) {
+            mProgramUpdatesAll = programUpdatesAll;
+            return this;
+        }
+
+        /**
+         * Set the number of APF program updates from allowing multicast traffic.
+         */
+        @NonNull
+        public Builder setProgramUpdatesAllowingMulticast(int programUpdatesAllowingMulticast) {
+            mProgramUpdatesAllowingMulticast = programUpdatesAllowingMulticast;
+            return this;
+        }
+
+        /**
+         * Set the maximum APF program size advertised by hardware.
+         */
+        @NonNull
+        public Builder setMaxProgramSize(int maxProgramSize) {
+            mMaxProgramSize = maxProgramSize;
+            return this;
+        }
+
+        /**
+         * Create a new {@link ApfStats}.
+         */
+        @NonNull
+        public ApfStats build() {
+            return new ApfStats(mDurationMs, mReceivedRas, mMatchingRas, mDroppedRas,
+                    mZeroLifetimeRas, mParseErrors, mProgramUpdates, mProgramUpdatesAll,
+                    mProgramUpdatesAllowingMulticast, mMaxProgramSize);
+        }
+    }
+
+    /** @hide */
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeLong(durationMs);
+        out.writeInt(receivedRas);
+        out.writeInt(matchingRas);
+        out.writeInt(droppedRas);
+        out.writeInt(zeroLifetimeRas);
+        out.writeInt(parseErrors);
+        out.writeInt(programUpdates);
+        out.writeInt(programUpdatesAll);
+        out.writeInt(programUpdatesAllowingMulticast);
+        out.writeInt(maxProgramSize);
+    }
+
+    /** @hide */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public String toString() {
+        return new StringBuilder("ApfStats(")
+                .append(String.format("%dms ", durationMs))
+                .append(String.format("%dB RA: {", maxProgramSize))
+                .append(String.format("%d received, ", receivedRas))
+                .append(String.format("%d matching, ", matchingRas))
+                .append(String.format("%d dropped, ", droppedRas))
+                .append(String.format("%d zero lifetime, ", zeroLifetimeRas))
+                .append(String.format("%d parse errors}, ", parseErrors))
+                .append(String.format("updates: {all: %d, RAs: %d, allow multicast: %d})",
+                        programUpdatesAll, programUpdates, programUpdatesAllowingMulticast))
+                .toString();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null || !(obj.getClass().equals(ApfStats.class))) return false;
+        final ApfStats other = (ApfStats) obj;
+        return durationMs == other.durationMs
+                && receivedRas == other.receivedRas
+                && matchingRas == other.matchingRas
+                && droppedRas == other.droppedRas
+                && zeroLifetimeRas == other.zeroLifetimeRas
+                && parseErrors == other.parseErrors
+                && programUpdates == other.programUpdates
+                && programUpdatesAll == other.programUpdatesAll
+                && programUpdatesAllowingMulticast == other.programUpdatesAllowingMulticast
+                && maxProgramSize == other.maxProgramSize;
+    }
+
+    /** @hide */
+    public static final @android.annotation.NonNull Parcelable.Creator<ApfStats> CREATOR = new Parcelable.Creator<ApfStats>() {
+        public ApfStats createFromParcel(Parcel in) {
+            return new ApfStats(in);
+        }
+
+        public ApfStats[] newArray(int size) {
+            return new ApfStats[size];
+        }
+    };
+}
diff --git a/android/net/metrics/ConnectStats.java b/android/net/metrics/ConnectStats.java
new file mode 100644
index 0000000..b320b75
--- /dev/null
+++ b/android/net/metrics/ConnectStats.java
@@ -0,0 +1,138 @@
+/*
+ * 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.metrics;
+
+import android.net.NetworkCapabilities;
+import android.system.OsConstants;
+import android.util.IntArray;
+import android.util.SparseIntArray;
+
+import com.android.internal.util.BitUtils;
+import com.android.internal.util.TokenBucket;
+
+/**
+ * A class that aggregates connect() statistics.
+ * {@hide}
+ */
+public class ConnectStats {
+    private final static int EALREADY     = OsConstants.EALREADY;
+    private final static int EINPROGRESS  = OsConstants.EINPROGRESS;
+
+    /** Network id of the network associated with the event, or 0 if unspecified. */
+    public final int netId;
+    /** Transports of the network associated with the event, as defined in NetworkCapabilities. */
+    public final long transports;
+    /** How many events resulted in a given errno. */
+    public final SparseIntArray errnos = new SparseIntArray();
+    /** Latencies of successful blocking connects. TODO: add non-blocking connects latencies. */
+    public final IntArray latencies = new IntArray();
+    /** TokenBucket for rate limiting latency recording. */
+    public final TokenBucket mLatencyTb;
+    /** Maximum number of latency values recorded. */
+    public final int mMaxLatencyRecords;
+    /** Total count of events */
+    public int eventCount = 0;
+    /** Total count of successful connects. */
+    public int connectCount = 0;
+    /** Total count of successful connects done in blocking mode. */
+    public int connectBlockingCount = 0;
+    /** Total count of successful connects with IPv6 socket address. */
+    public int ipv6ConnectCount = 0;
+
+    public ConnectStats(int netId, long transports, TokenBucket tb, int maxLatencyRecords) {
+        this.netId = netId;
+        this.transports = transports;
+        mLatencyTb = tb;
+        mMaxLatencyRecords = maxLatencyRecords;
+    }
+
+    boolean addEvent(int errno, int latencyMs, String ipAddr) {
+        eventCount++;
+        if (isSuccess(errno)) {
+            countConnect(errno, ipAddr);
+            countLatency(errno, latencyMs);
+            return true;
+        } else {
+            countError(errno);
+            return false;
+        }
+    }
+
+    private void countConnect(int errno, String ipAddr) {
+        connectCount++;
+        if (!isNonBlocking(errno)) {
+            connectBlockingCount++;
+        }
+        if (isIPv6(ipAddr)) {
+            ipv6ConnectCount++;
+        }
+    }
+
+    private void countLatency(int errno, int ms) {
+        if (isNonBlocking(errno)) {
+            // Ignore connect() on non-blocking sockets
+            return;
+        }
+        if (!mLatencyTb.get()) {
+            // Rate limited
+            return;
+        }
+        if (latencies.size() >= mMaxLatencyRecords) {
+            // Hard limit the total number of latency measurements.
+            return;
+        }
+        latencies.add(ms);
+    }
+
+    private void countError(int errno) {
+        final int newcount = errnos.get(errno, 0) + 1;
+        errnos.put(errno, newcount);
+    }
+
+    private static boolean isSuccess(int errno) {
+        return (errno == 0) || isNonBlocking(errno);
+    }
+
+    static boolean isNonBlocking(int errno) {
+        // On non-blocking TCP sockets, connect() immediately returns EINPROGRESS.
+        // On non-blocking TCP sockets that are connecting, connect() immediately returns EALREADY.
+        return (errno == EINPROGRESS) || (errno == EALREADY);
+    }
+
+    private static boolean isIPv6(String ipAddr) {
+        return ipAddr.contains(":");
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder builder =
+                new StringBuilder("ConnectStats(").append("netId=").append(netId).append(", ");
+        for (int t : BitUtils.unpackBits(transports)) {
+            builder.append(NetworkCapabilities.transportNameOf(t)).append(", ");
+        }
+        builder.append(String.format("%d events, ", eventCount));
+        builder.append(String.format("%d success, ", connectCount));
+        builder.append(String.format("%d blocking, ", connectBlockingCount));
+        builder.append(String.format("%d IPv6 dst", ipv6ConnectCount));
+        for (int i = 0; i < errnos.size(); i++) {
+            String errno = OsConstants.errnoName(errnos.keyAt(i));
+            int count = errnos.valueAt(i);
+            builder.append(String.format(", %s: %d", errno, count));
+        }
+        return builder.append(")").toString();
+    }
+}
diff --git a/android/net/metrics/DefaultNetworkEvent.java b/android/net/metrics/DefaultNetworkEvent.java
new file mode 100644
index 0000000..6f383b4
--- /dev/null
+++ b/android/net/metrics/DefaultNetworkEvent.java
@@ -0,0 +1,93 @@
+/*
+ * 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.metrics;
+
+import static android.net.ConnectivityManager.NETID_UNSET;
+
+import android.net.NetworkCapabilities;
+
+import com.android.internal.util.BitUtils;
+
+import java.util.StringJoiner;
+
+/**
+ * An event recorded by ConnectivityService when there is a change in the default network.
+ * {@hide}
+ */
+public class DefaultNetworkEvent {
+
+    // The creation time in milliseconds of this DefaultNetworkEvent.
+    public final long creationTimeMs;
+    // The network ID of the network or NETID_UNSET if none.
+    public int netId = NETID_UNSET;
+    // The list of transport types, as defined in NetworkCapabilities.java.
+    public int transports;
+    // The list of transport types of the last previous default network.
+    public int previousTransports;
+    // Whether the network has IPv4/IPv6 connectivity.
+    public boolean ipv4;
+    public boolean ipv6;
+    // The initial network score when this network became the default network.
+    public int initialScore;
+    // The initial network score when this network stopped being the default network.
+    public int finalScore;
+    // The total duration in milliseconds this network was the default network.
+    public long durationMs;
+    // The total duration in milliseconds this network was the default network and was validated.
+    public long validatedMs;
+
+    public DefaultNetworkEvent(long timeMs) {
+        creationTimeMs = timeMs;
+    }
+
+    /** Update the durationMs of this DefaultNetworkEvent for the given current time. */
+    public void updateDuration(long timeMs) {
+        durationMs = timeMs - creationTimeMs;
+    }
+
+    @Override
+    public String toString() {
+        StringJoiner j = new StringJoiner(", ", "DefaultNetworkEvent(", ")");
+        j.add("netId=" + netId);
+        for (int t : BitUtils.unpackBits(transports)) {
+            j.add(NetworkCapabilities.transportNameOf(t));
+        }
+        j.add("ip=" + ipSupport());
+        if (initialScore > 0) {
+            j.add("initial_score=" + initialScore);
+        }
+        if (finalScore > 0) {
+            j.add("final_score=" + finalScore);
+        }
+        j.add(String.format("duration=%.0fs", durationMs / 1000.0));
+        j.add(String.format("validation=%04.1f%%", (validatedMs * 100.0) / durationMs));
+        return j.toString();
+    }
+
+    private String ipSupport() {
+        if (ipv4 && ipv6) {
+            return "IPv4v6";
+        }
+        if (ipv6) {
+            return "IPv6";
+        }
+        if (ipv4) {
+            return "IPv4";
+        }
+        return "NONE";
+    }
+}
diff --git a/android/net/metrics/DhcpClientEvent.java b/android/net/metrics/DhcpClientEvent.java
new file mode 100644
index 0000000..2fed736
--- /dev/null
+++ b/android/net/metrics/DhcpClientEvent.java
@@ -0,0 +1,124 @@
+/*
+ * 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.metrics;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.annotation.UnsupportedAppUsage;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+/**
+ * An event recorded when a DhcpClient state machine transitions to a new state.
+ * {@hide}
+ */
+@SystemApi
+@TestApi
+public final class DhcpClientEvent implements IpConnectivityLog.Event {
+
+    // Names for recording DhcpClient pseudo-state transitions.
+
+    /** @hide */
+    public final String msg;
+    /** @hide */
+    public final int durationMs;
+
+    @UnsupportedAppUsage
+    private DhcpClientEvent(String msg, int durationMs) {
+        this.msg = msg;
+        this.durationMs = durationMs;
+    }
+
+    private DhcpClientEvent(Parcel in) {
+        this.msg = in.readString();
+        this.durationMs = in.readInt();
+    }
+
+    /**
+     * Utility to create an instance of {@link ApfProgramEvent}.
+     */
+    public static final class Builder {
+        private String mMsg;
+        private int mDurationMs;
+
+        /**
+         * Set the message of the event.
+         */
+        @NonNull
+        public Builder setMsg(String msg) {
+            mMsg = msg;
+            return this;
+        }
+
+        /**
+         * Set the duration of the event in milliseconds.
+         */
+        @NonNull
+        public Builder setDurationMs(int durationMs) {
+            mDurationMs = durationMs;
+            return this;
+        }
+
+        /**
+         * Create a new {@link DhcpClientEvent}.
+         */
+        @NonNull
+        public DhcpClientEvent build() {
+            return new DhcpClientEvent(mMsg, mDurationMs);
+        }
+    }
+
+    /** @hide */
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeString(msg);
+        out.writeInt(durationMs);
+    }
+
+    /** @hide */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("DhcpClientEvent(%s, %dms)", msg, durationMs);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null || !(obj.getClass().equals(DhcpClientEvent.class))) return false;
+        final DhcpClientEvent other = (DhcpClientEvent) obj;
+        return TextUtils.equals(msg, other.msg)
+                && durationMs == other.durationMs;
+    }
+
+    /** @hide */
+    public static final @android.annotation.NonNull Parcelable.Creator<DhcpClientEvent> CREATOR
+        = new Parcelable.Creator<DhcpClientEvent>() {
+        public DhcpClientEvent createFromParcel(Parcel in) {
+            return new DhcpClientEvent(in);
+        }
+
+        public DhcpClientEvent[] newArray(int size) {
+            return new DhcpClientEvent[size];
+        }
+    };
+}
diff --git a/android/net/metrics/DhcpErrorEvent.java b/android/net/metrics/DhcpErrorEvent.java
new file mode 100644
index 0000000..8760004
--- /dev/null
+++ b/android/net/metrics/DhcpErrorEvent.java
@@ -0,0 +1,122 @@
+/*
+ * 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.metrics;
+
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.SparseArray;
+
+import com.android.internal.util.MessageUtils;
+
+/**
+ * Event class used to record error events when parsing DHCP response packets.
+ * {@hide}
+ */
+@SystemApi
+@TestApi
+public final class DhcpErrorEvent implements IpConnectivityLog.Event {
+    public static final int L2_ERROR   = 1;
+    public static final int L3_ERROR   = 2;
+    public static final int L4_ERROR   = 3;
+    public static final int DHCP_ERROR = 4;
+    public static final int MISC_ERROR = 5;
+
+    // error code byte format (MSB to LSB):
+    // byte 0: error type
+    // byte 1: error subtype
+    // byte 2: unused
+    // byte 3: optional code
+    /** @hide */
+    public final int errorCode;
+
+    private static final int L2_ERROR_TYPE = L2_ERROR << 8;
+    private static final int L3_ERROR_TYPE = L3_ERROR << 8;
+    private static final int L4_ERROR_TYPE = L4_ERROR << 8;
+    private static final int DHCP_ERROR_TYPE = DHCP_ERROR << 8;
+    private static final int MISC_ERROR_TYPE = MISC_ERROR << 8;
+
+    public static final int L2_TOO_SHORT               = (L2_ERROR_TYPE | 0x1) << 16;
+    public static final int L2_WRONG_ETH_TYPE          = (L2_ERROR_TYPE | 0x2) << 16;
+
+    public static final int L3_TOO_SHORT               = (L3_ERROR_TYPE | 0x1) << 16;
+    public static final int L3_NOT_IPV4                = (L3_ERROR_TYPE | 0x2) << 16;
+    public static final int L3_INVALID_IP              = (L3_ERROR_TYPE | 0x3) << 16;
+
+    public static final int L4_NOT_UDP                 = (L4_ERROR_TYPE | 0x1) << 16;
+    public static final int L4_WRONG_PORT              = (L4_ERROR_TYPE | 0x2) << 16;
+
+    public static final int BOOTP_TOO_SHORT            = (DHCP_ERROR_TYPE | 0x1) << 16;
+    public static final int DHCP_BAD_MAGIC_COOKIE      = (DHCP_ERROR_TYPE | 0x2) << 16;
+    public static final int DHCP_INVALID_OPTION_LENGTH = (DHCP_ERROR_TYPE | 0x3) << 16;
+    public static final int DHCP_NO_MSG_TYPE           = (DHCP_ERROR_TYPE | 0x4) << 16;
+    public static final int DHCP_UNKNOWN_MSG_TYPE      = (DHCP_ERROR_TYPE | 0x5) << 16;
+    public static final int DHCP_NO_COOKIE             = (DHCP_ERROR_TYPE | 0x6) << 16;
+
+    public static final int BUFFER_UNDERFLOW           = (MISC_ERROR_TYPE | 0x1) << 16;
+    public static final int RECEIVE_ERROR              = (MISC_ERROR_TYPE | 0x2) << 16;
+    public static final int PARSING_ERROR              = (MISC_ERROR_TYPE | 0x3) << 16;
+
+    public DhcpErrorEvent(int errorCode) {
+        this.errorCode = errorCode;
+    }
+
+    private DhcpErrorEvent(Parcel in) {
+        this.errorCode = in.readInt();
+    }
+
+    /** @hide */
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeInt(errorCode);
+    }
+
+    /** @hide */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /** @hide */
+    public static final @android.annotation.NonNull Parcelable.Creator<DhcpErrorEvent> CREATOR
+        = new Parcelable.Creator<DhcpErrorEvent>() {
+        public DhcpErrorEvent createFromParcel(Parcel in) {
+            return new DhcpErrorEvent(in);
+        }
+
+        public DhcpErrorEvent[] newArray(int size) {
+            return new DhcpErrorEvent[size];
+        }
+    };
+
+    public static int errorCodeWithOption(int errorCode, int option) {
+        return (0xFFFF0000 & errorCode) | (0xFF & option);
+    }
+
+    @Override
+    public String toString() {
+        return String.format("DhcpErrorEvent(%s)", Decoder.constants.get(errorCode));
+    }
+
+    final static class Decoder {
+        static final SparseArray<String> constants = MessageUtils.findMessageNames(
+                new Class[]{DhcpErrorEvent.class},
+                new String[]{"L2_", "L3_", "L4_", "BOOTP_", "DHCP_", "BUFFER_", "RECEIVE_",
+                "PARSING_"});
+    }
+}
diff --git a/android/net/metrics/DnsEvent.java b/android/net/metrics/DnsEvent.java
new file mode 100644
index 0000000..5aa705b
--- /dev/null
+++ b/android/net/metrics/DnsEvent.java
@@ -0,0 +1,97 @@
+/*
+ * 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.metrics;
+
+import android.net.NetworkCapabilities;
+
+import com.android.internal.util.BitUtils;
+
+import java.util.Arrays;
+
+/**
+ * A batch of DNS events recorded by NetdEventListenerService for a specific network.
+ * {@hide}
+ */
+final public class DnsEvent {
+
+    private static final int SIZE_LIMIT = 20000;
+
+    // Network id of the network associated with the event, or 0 if unspecified.
+    public final int netId;
+    // Transports of the network associated with the event, as defined in NetworkCapabilities.
+    // It is the caller responsability to ensure the value of transports does not change between
+    // calls to addResult.
+    public final long transports;
+    // The number of DNS queries recorded. Queries are stored in the structure-of-array style where
+    // the eventTypes, returnCodes, and latenciesMs arrays have the same length and the i-th event
+    // is spread across the three array at position i.
+    public int eventCount;
+    // The number of successful DNS queries recorded.
+    public int successCount;
+    // The types of DNS queries as defined in INetdEventListener.
+    public byte[] eventTypes;
+    // Current getaddrinfo codes go from 1 to EAI_MAX = 15. gethostbyname returns errno, but there
+    // are fewer than 255 errno values. So we store the result code in a byte as well.
+    public byte[] returnCodes;
+    // Latencies in milliseconds of queries, stored as ints.
+    public int[] latenciesMs;
+
+    public DnsEvent(int netId, long transports, int initialCapacity) {
+        this.netId = netId;
+        this.transports = transports;
+        eventTypes = new byte[initialCapacity];
+        returnCodes = new byte[initialCapacity];
+        latenciesMs = new int[initialCapacity];
+    }
+
+    boolean addResult(byte eventType, byte returnCode, int latencyMs) {
+        boolean isSuccess = (returnCode == 0);
+        if (eventCount >= SIZE_LIMIT) {
+            // TODO: implement better rate limiting that does not biases metrics.
+            return isSuccess;
+        }
+        if (eventCount == eventTypes.length) {
+            resize((int) (1.4 * eventCount));
+        }
+        eventTypes[eventCount] = eventType;
+        returnCodes[eventCount] = returnCode;
+        latenciesMs[eventCount] = latencyMs;
+        eventCount++;
+        if (isSuccess) {
+            successCount++;
+        }
+        return isSuccess;
+    }
+
+    public void resize(int newLength) {
+        eventTypes = Arrays.copyOf(eventTypes, newLength);
+        returnCodes = Arrays.copyOf(returnCodes, newLength);
+        latenciesMs = Arrays.copyOf(latenciesMs, newLength);
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder builder =
+                new StringBuilder("DnsEvent(").append("netId=").append(netId).append(", ");
+        for (int t : BitUtils.unpackBits(transports)) {
+            builder.append(NetworkCapabilities.transportNameOf(t)).append(", ");
+        }
+        builder.append(String.format("%d events, ", eventCount));
+        builder.append(String.format("%d success)", successCount));
+        return builder.toString();
+    }
+}
diff --git a/android/net/metrics/IpConnectivityLog.java b/android/net/metrics/IpConnectivityLog.java
new file mode 100644
index 0000000..680c015
--- /dev/null
+++ b/android/net/metrics/IpConnectivityLog.java
@@ -0,0 +1,172 @@
+/*
+ * 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.metrics;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.net.ConnectivityMetricsEvent;
+import android.net.IIpConnectivityMetrics;
+import android.net.Network;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.BitUtils;
+
+/**
+ * Class for logging IpConnectvity events with IpConnectivityMetrics
+ * {@hide}
+ */
+@SystemApi
+@TestApi
+public class IpConnectivityLog {
+    private static final String TAG = IpConnectivityLog.class.getSimpleName();
+    private static final boolean DBG = false;
+
+    /** @hide */
+    public static final String SERVICE_NAME = "connmetrics";
+    @NonNull
+    private IIpConnectivityMetrics mService;
+
+    /**
+     * An event to be logged.
+     */
+    public interface Event extends Parcelable {}
+
+    /** @hide */
+    @SystemApi
+    @TestApi
+    public IpConnectivityLog() {
+    }
+
+    /** @hide */
+    @VisibleForTesting
+    public IpConnectivityLog(@NonNull IIpConnectivityMetrics service) {
+        mService = service;
+    }
+
+    private boolean checkLoggerService() {
+        if (mService != null) {
+            return true;
+        }
+        final IIpConnectivityMetrics service =
+                IIpConnectivityMetrics.Stub.asInterface(ServiceManager.getService(SERVICE_NAME));
+        if (service == null) {
+            return false;
+        }
+        // Two threads racing here will write the same pointer because getService
+        // is idempotent once MetricsLoggerService is initialized.
+        mService = service;
+        return true;
+    }
+
+    /**
+     * Log a ConnectivityMetricsEvent.
+     * @param ev the event to log. If the event timestamp is 0,
+     * the timestamp is set to the current time in milliseconds.
+     * @return true if the event was successfully logged.
+     * @hide
+     */
+    public boolean log(@NonNull ConnectivityMetricsEvent ev) {
+        if (!checkLoggerService()) {
+            if (DBG) {
+                Log.d(TAG, SERVICE_NAME + " service was not ready");
+            }
+            return false;
+        }
+        if (ev.timestamp == 0) {
+            ev.timestamp = System.currentTimeMillis();
+        }
+        try {
+            int left = mService.logEvent(ev);
+            return left >= 0;
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error logging event", e);
+            return false;
+        }
+    }
+
+    /**
+     * Log an IpConnectivity event.
+     * @param timestamp is the epoch timestamp of the event in ms.
+     * If the timestamp is 0, the timestamp is set to the current time in milliseconds.
+     * @param data is a Parcelable instance representing the event.
+     * @return true if the event was successfully logged.
+     */
+    public boolean log(long timestamp, @NonNull Event data) {
+        ConnectivityMetricsEvent ev = makeEv(data);
+        ev.timestamp = timestamp;
+        return log(ev);
+    }
+
+    /**
+     * Log an IpConnectivity event.
+     * @param ifname the network interface associated with the event.
+     * @param data is a Parcelable instance representing the event.
+     * @return true if the event was successfully logged.
+     */
+    public boolean log(@NonNull String ifname, @NonNull Event data) {
+        ConnectivityMetricsEvent ev = makeEv(data);
+        ev.ifname = ifname;
+        return log(ev);
+    }
+
+    /**
+     * Log an IpConnectivity event.
+     * @param network the network associated with the event.
+     * @param transports the current transports of the network associated with the event, as defined
+     * in NetworkCapabilities.
+     * @param data is a Parcelable instance representing the event.
+     * @return true if the event was successfully logged.
+     */
+    public boolean log(@NonNull Network network, @NonNull int[] transports, @NonNull Event data) {
+        return log(network.netId, transports, data);
+    }
+
+    /**
+     * Log an IpConnectivity event.
+     * @param netid the id of the network associated with the event.
+     * @param transports the current transports of the network associated with the event, as defined
+     * in NetworkCapabilities.
+     * @param data is a Parcelable instance representing the event.
+     * @return true if the event was successfully logged.
+     */
+    public boolean log(int netid, @NonNull int[] transports, @NonNull Event data) {
+        ConnectivityMetricsEvent ev = makeEv(data);
+        ev.netId = netid;
+        ev.transports = BitUtils.packBits(transports);
+        return log(ev);
+    }
+
+    /**
+     * Log an IpConnectivity event.
+     * @param data is a Parcelable instance representing the event.
+     * @return true if the event was successfully logged.
+     */
+    public boolean log(@NonNull Event data) {
+        return log(makeEv(data));
+    }
+
+    private static ConnectivityMetricsEvent makeEv(Event data) {
+        ConnectivityMetricsEvent ev = new ConnectivityMetricsEvent();
+        ev.data = data;
+        return ev;
+    }
+}
diff --git a/android/net/metrics/IpManagerEvent.java b/android/net/metrics/IpManagerEvent.java
new file mode 100644
index 0000000..ba05c59
--- /dev/null
+++ b/android/net/metrics/IpManagerEvent.java
@@ -0,0 +1,117 @@
+/*
+ * 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.metrics;
+
+import android.annotation.IntDef;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.SparseArray;
+
+import com.android.internal.util.MessageUtils;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * An event recorded by IpClient when IP provisioning completes for a network or
+ * when a network disconnects.
+ * {@hide}
+ */
+@SystemApi
+@TestApi
+public final class IpManagerEvent implements IpConnectivityLog.Event {
+
+    public static final int PROVISIONING_OK                       = 1;
+    public static final int PROVISIONING_FAIL                     = 2;
+    public static final int COMPLETE_LIFECYCLE                    = 3;
+    public static final int ERROR_STARTING_IPV4                   = 4;
+    public static final int ERROR_STARTING_IPV6                   = 5;
+    public static final int ERROR_STARTING_IPREACHABILITYMONITOR  = 6;
+    public static final int ERROR_INVALID_PROVISIONING            = 7;
+    public static final int ERROR_INTERFACE_NOT_FOUND             = 8;
+
+    /** @hide */
+    @IntDef(value = {
+            PROVISIONING_OK, PROVISIONING_FAIL, COMPLETE_LIFECYCLE,
+            ERROR_STARTING_IPV4, ERROR_STARTING_IPV6, ERROR_STARTING_IPREACHABILITYMONITOR,
+            ERROR_INVALID_PROVISIONING, ERROR_INTERFACE_NOT_FOUND,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface EventType {}
+
+    /** @hide */
+    public final @EventType int eventType;
+    /** @hide */
+    public final long durationMs;
+
+    public IpManagerEvent(@EventType int eventType, long duration) {
+        this.eventType = eventType;
+        this.durationMs = duration;
+    }
+
+    private IpManagerEvent(Parcel in) {
+        this.eventType = in.readInt();
+        this.durationMs = in.readLong();
+    }
+
+    /** @hide */
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeInt(eventType);
+        out.writeLong(durationMs);
+    }
+
+    /** @hide */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /** @hide */
+    public static final @android.annotation.NonNull Parcelable.Creator<IpManagerEvent> CREATOR
+        = new Parcelable.Creator<IpManagerEvent>() {
+        public IpManagerEvent createFromParcel(Parcel in) {
+            return new IpManagerEvent(in);
+        }
+
+        public IpManagerEvent[] newArray(int size) {
+            return new IpManagerEvent[size];
+        }
+    };
+
+    @Override
+    public String toString() {
+        return String.format("IpManagerEvent(%s, %dms)",
+                Decoder.constants.get(eventType), durationMs);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null || !(obj.getClass().equals(IpManagerEvent.class))) return false;
+        final IpManagerEvent other = (IpManagerEvent) obj;
+        return eventType == other.eventType
+                && durationMs == other.durationMs;
+    }
+
+    final static class Decoder {
+        static final SparseArray<String> constants = MessageUtils.findMessageNames(
+                new Class[]{IpManagerEvent.class},
+                new String[]{"PROVISIONING_", "COMPLETE_", "ERROR_"});
+    }
+}
diff --git a/android/net/metrics/IpReachabilityEvent.java b/android/net/metrics/IpReachabilityEvent.java
new file mode 100644
index 0000000..d4ba294
--- /dev/null
+++ b/android/net/metrics/IpReachabilityEvent.java
@@ -0,0 +1,108 @@
+/*
+ * 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.metrics;
+
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.SparseArray;
+
+import com.android.internal.util.MessageUtils;
+
+/**
+ * An event recorded when IpReachabilityMonitor sends a neighbor probe or receives
+ * a neighbor probe result.
+ * {@hide}
+ */
+@SystemApi
+@TestApi
+public final class IpReachabilityEvent implements IpConnectivityLog.Event {
+
+    // Event types.
+    /** A probe forced by IpReachabilityMonitor. */
+    public static final int PROBE                     = 1 << 8;
+    /** Neighbor unreachable after a forced probe. */
+    public static final int NUD_FAILED                = 2 << 8;
+    /** Neighbor unreachable after a forced probe, IP provisioning is also lost. */
+    public static final int PROVISIONING_LOST         = 3 << 8;
+    /** Neighbor unreachable notification from kernel. */
+    public static final int NUD_FAILED_ORGANIC        = 4 << 8;
+    /** Neighbor unreachable notification from kernel, IP provisioning is also lost. */
+    public static final int PROVISIONING_LOST_ORGANIC = 5 << 8;
+
+    // eventType byte format (MSB to LSB):
+    // byte 0: unused
+    // byte 1: unused
+    // byte 2: type of event: PROBE, NUD_FAILED, PROVISIONING_LOST
+    // byte 3: when byte 2 == PROBE, errno code from RTNetlink or IpReachabilityMonitor.
+    /** @hide */
+    public final int eventType;
+
+    public IpReachabilityEvent(int eventType) {
+        this.eventType = eventType;
+    }
+
+    private IpReachabilityEvent(Parcel in) {
+        this.eventType = in.readInt();
+    }
+
+    /** @hide */
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeInt(eventType);
+    }
+
+    /** @hide */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /** @hide */
+    public static final @android.annotation.NonNull Parcelable.Creator<IpReachabilityEvent> CREATOR
+        = new Parcelable.Creator<IpReachabilityEvent>() {
+        public IpReachabilityEvent createFromParcel(Parcel in) {
+            return new IpReachabilityEvent(in);
+        }
+
+        public IpReachabilityEvent[] newArray(int size) {
+            return new IpReachabilityEvent[size];
+        }
+    };
+
+    @Override
+    public String toString() {
+        int hi = eventType & 0xff00;
+        int lo = eventType & 0x00ff;
+        String eventName = Decoder.constants.get(hi);
+        return String.format("IpReachabilityEvent(%s:%02x)", eventName, lo);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null || !(obj.getClass().equals(IpReachabilityEvent.class))) return false;
+        final IpReachabilityEvent other = (IpReachabilityEvent) obj;
+        return eventType == other.eventType;
+    }
+
+    final static class Decoder {
+        static final SparseArray<String> constants =
+                MessageUtils.findMessageNames(new Class[]{IpReachabilityEvent.class},
+                new String[]{"PROBE", "PROVISIONING_", "NUD_"});
+    }
+}
diff --git a/android/net/metrics/NetworkEvent.java b/android/net/metrics/NetworkEvent.java
new file mode 100644
index 0000000..0c57ec6
--- /dev/null
+++ b/android/net/metrics/NetworkEvent.java
@@ -0,0 +1,136 @@
+/*
+ * 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.metrics;
+
+import android.annotation.IntDef;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.SparseArray;
+
+import com.android.internal.util.MessageUtils;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * {@hide}
+ */
+@SystemApi
+@TestApi
+public final class NetworkEvent implements IpConnectivityLog.Event {
+
+    public static final int NETWORK_CONNECTED            = 1;
+    public static final int NETWORK_VALIDATED            = 2;
+    public static final int NETWORK_VALIDATION_FAILED    = 3;
+    public static final int NETWORK_CAPTIVE_PORTAL_FOUND = 4;
+    public static final int NETWORK_LINGER               = 5;
+    public static final int NETWORK_UNLINGER             = 6;
+    public static final int NETWORK_DISCONNECTED         = 7;
+
+    public static final int NETWORK_FIRST_VALIDATION_SUCCESS      = 8;
+    public static final int NETWORK_REVALIDATION_SUCCESS          = 9;
+    public static final int NETWORK_FIRST_VALIDATION_PORTAL_FOUND = 10;
+    public static final int NETWORK_REVALIDATION_PORTAL_FOUND     = 11;
+
+    public static final int NETWORK_CONSECUTIVE_DNS_TIMEOUT_FOUND = 12;
+
+    public static final int NETWORK_PARTIAL_CONNECTIVITY = 13;
+
+    /** @hide */
+    @IntDef(value = {
+            NETWORK_CONNECTED,
+            NETWORK_VALIDATED,
+            NETWORK_VALIDATION_FAILED,
+            NETWORK_CAPTIVE_PORTAL_FOUND,
+            NETWORK_LINGER,
+            NETWORK_UNLINGER,
+            NETWORK_DISCONNECTED,
+            NETWORK_FIRST_VALIDATION_SUCCESS,
+            NETWORK_REVALIDATION_SUCCESS,
+            NETWORK_FIRST_VALIDATION_PORTAL_FOUND,
+            NETWORK_REVALIDATION_PORTAL_FOUND,
+            NETWORK_CONSECUTIVE_DNS_TIMEOUT_FOUND,
+            NETWORK_PARTIAL_CONNECTIVITY,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface EventType {}
+
+    /** @hide */
+    public final @EventType int eventType;
+    /** @hide */
+    public final long durationMs;
+
+    public NetworkEvent(@EventType int eventType, long durationMs) {
+        this.eventType = eventType;
+        this.durationMs = durationMs;
+    }
+
+    public NetworkEvent(@EventType int eventType) {
+        this(eventType, 0);
+    }
+
+    private NetworkEvent(Parcel in) {
+        eventType = in.readInt();
+        durationMs = in.readLong();
+    }
+
+    /** @hide */
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeInt(eventType);
+        out.writeLong(durationMs);
+    }
+
+    /** @hide */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /** @hide */
+    public static final @android.annotation.NonNull Parcelable.Creator<NetworkEvent> CREATOR
+        = new Parcelable.Creator<NetworkEvent>() {
+        public NetworkEvent createFromParcel(Parcel in) {
+            return new NetworkEvent(in);
+        }
+
+        public NetworkEvent[] newArray(int size) {
+            return new NetworkEvent[size];
+        }
+    };
+
+    @Override
+    public String toString() {
+        return String.format("NetworkEvent(%s, %dms)",
+                Decoder.constants.get(eventType), durationMs);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null || !(obj.getClass().equals(NetworkEvent.class))) return false;
+        final NetworkEvent other = (NetworkEvent) obj;
+        return eventType == other.eventType
+                && durationMs == other.durationMs;
+    }
+
+    final static class Decoder {
+        static final SparseArray<String> constants = MessageUtils.findMessageNames(
+                new Class[]{NetworkEvent.class}, new String[]{"NETWORK_"});
+    }
+}
diff --git a/android/net/metrics/NetworkMetrics.java b/android/net/metrics/NetworkMetrics.java
new file mode 100644
index 0000000..66d92c4
--- /dev/null
+++ b/android/net/metrics/NetworkMetrics.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.metrics;
+
+import android.net.NetworkCapabilities;
+
+import com.android.internal.util.BitUtils;
+import com.android.internal.util.TokenBucket;
+
+import java.util.StringJoiner;
+
+/**
+ * A class accumulating network metrics received from Netd regarding dns queries and
+ * connect() calls on a given network.
+ *
+ * This class also accumulates running sums of dns and connect latency stats and
+ * error counts for bug report logging.
+ *
+ * @hide
+ */
+public class NetworkMetrics {
+
+    private static final int INITIAL_DNS_BATCH_SIZE = 100;
+    private static final int CONNECT_LATENCY_MAXIMUM_RECORDS = 20000;
+
+    // The network id of the Android Network.
+    public final int netId;
+    // The transport types bitmap of the Android Network, as defined in NetworkCapabilities.java.
+    public final long transports;
+    // Accumulated metrics for connect events.
+    public final ConnectStats connectMetrics;
+    // Accumulated metrics for dns events.
+    public final DnsEvent dnsMetrics;
+    // Running sums of latencies and error counts for connect and dns events.
+    public final Summary summary;
+    // Running sums of the most recent latencies and error counts for connect and dns events.
+    // Starts null until some events are accumulated.
+    // Allows to collect periodic snapshot of the running summaries for a given network.
+    public Summary pendingSummary;
+
+    public NetworkMetrics(int netId, long transports, TokenBucket tb) {
+        this.netId = netId;
+        this.transports = transports;
+        this.connectMetrics =
+                new ConnectStats(netId, transports, tb, CONNECT_LATENCY_MAXIMUM_RECORDS);
+        this.dnsMetrics = new DnsEvent(netId, transports, INITIAL_DNS_BATCH_SIZE);
+        this.summary = new Summary(netId, transports);
+    }
+
+    /**
+     * Get currently pending Summary statistics, if any, for this NetworkMetrics, merge them
+     * into the long running Summary statistics of this NetworkMetrics, and also clear them.
+     */
+    public Summary getPendingStats() {
+        Summary s = pendingSummary;
+        pendingSummary = null;
+        if (s != null) {
+            summary.merge(s);
+        }
+        return s;
+    }
+
+    /** Accumulate a dns query result reported by netd. */
+    public void addDnsResult(int eventType, int returnCode, int latencyMs) {
+        if (pendingSummary == null) {
+            pendingSummary = new Summary(netId, transports);
+        }
+        boolean isSuccess = dnsMetrics.addResult((byte) eventType, (byte) returnCode, latencyMs);
+        pendingSummary.dnsLatencies.count(latencyMs);
+        pendingSummary.dnsErrorRate.count(isSuccess ? 0 : 1);
+    }
+
+    /** Accumulate a connect query result reported by netd. */
+    public void addConnectResult(int error, int latencyMs, String ipAddr) {
+        if (pendingSummary == null) {
+            pendingSummary = new Summary(netId, transports);
+        }
+        boolean isSuccess = connectMetrics.addEvent(error, latencyMs, ipAddr);
+        pendingSummary.connectErrorRate.count(isSuccess ? 0 : 1);
+        if (ConnectStats.isNonBlocking(error)) {
+            pendingSummary.connectLatencies.count(latencyMs);
+        }
+    }
+
+    /** Accumulate a single netd sock_diag poll result reported by netd. */
+    public void addTcpStatsResult(int sent, int lost, int rttUs, int sentAckDiffMs) {
+        if (pendingSummary == null) {
+            pendingSummary = new Summary(netId, transports);
+        }
+        pendingSummary.tcpLossRate.count(lost, sent);
+        pendingSummary.roundTripTimeUs.count(rttUs);
+        pendingSummary.sentAckTimeDiffenceMs.count(sentAckDiffMs);
+    }
+
+    /** Represents running sums for dns and connect average error counts and average latencies. */
+    public static class Summary {
+
+        public final int netId;
+        public final long transports;
+        // DNS latencies measured in milliseconds.
+        public final Metrics dnsLatencies = new Metrics();
+        // DNS error rate measured in percentage points.
+        public final Metrics dnsErrorRate = new Metrics();
+        // Blocking connect latencies measured in milliseconds.
+        public final Metrics connectLatencies = new Metrics();
+        // Blocking and non blocking connect error rate measured in percentage points.
+        public final Metrics connectErrorRate = new Metrics();
+        // TCP socket packet loss stats collected from Netlink sock_diag.
+        public final Metrics tcpLossRate = new Metrics();
+        // TCP averaged microsecond round-trip-time stats collected from Netlink sock_diag.
+        public final Metrics roundTripTimeUs = new Metrics();
+        // TCP stats collected from Netlink sock_diag that averages millisecond per-socket
+        // differences between last packet sent timestamp and last ack received timestamp.
+        public final Metrics sentAckTimeDiffenceMs = new Metrics();
+
+        public Summary(int netId, long transports) {
+            this.netId = netId;
+            this.transports = transports;
+        }
+
+        void merge(Summary that) {
+            dnsLatencies.merge(that.dnsLatencies);
+            dnsErrorRate.merge(that.dnsErrorRate);
+            connectLatencies.merge(that.connectLatencies);
+            connectErrorRate.merge(that.connectErrorRate);
+            tcpLossRate.merge(that.tcpLossRate);
+        }
+
+        @Override
+        public String toString() {
+            StringJoiner j = new StringJoiner(", ", "{", "}");
+            j.add("netId=" + netId);
+            for (int t : BitUtils.unpackBits(transports)) {
+                j.add(NetworkCapabilities.transportNameOf(t));
+            }
+            j.add(String.format("dns avg=%dms max=%dms err=%.1f%% tot=%d",
+                    (int) dnsLatencies.average(), (int) dnsLatencies.max,
+                    100 * dnsErrorRate.average(), dnsErrorRate.count));
+            j.add(String.format("connect avg=%dms max=%dms err=%.1f%% tot=%d",
+                    (int) connectLatencies.average(), (int) connectLatencies.max,
+                    100 * connectErrorRate.average(), connectErrorRate.count));
+            j.add(String.format("tcp avg_loss=%.1f%% total_sent=%d total_lost=%d",
+                    100 * tcpLossRate.average(), tcpLossRate.count, (int) tcpLossRate.sum));
+            j.add(String.format("tcp rtt=%dms", (int) (roundTripTimeUs.average() / 1000)));
+            j.add(String.format("tcp sent-ack_diff=%dms", (int) sentAckTimeDiffenceMs.average()));
+            return j.toString();
+        }
+    }
+
+    /** Tracks a running sum and returns the average of a metric. */
+    static class Metrics {
+        public double sum;
+        public double max = Double.MIN_VALUE;
+        public int count;
+
+        void merge(Metrics that) {
+            this.count += that.count;
+            this.sum += that.sum;
+            this.max = Math.max(this.max, that.max);
+        }
+
+        void count(double value) {
+            count(value, 1);
+        }
+
+        void count(double value, int subcount) {
+            count += subcount;
+            sum += value;
+            max = Math.max(max, value);
+        }
+
+        double average() {
+            double a = sum / (double) count;
+            if (Double.isNaN(a)) {
+                a = 0;
+            }
+            return a;
+        }
+    }
+}
diff --git a/android/net/metrics/RaEvent.java b/android/net/metrics/RaEvent.java
new file mode 100644
index 0000000..3fd87c2
--- /dev/null
+++ b/android/net/metrics/RaEvent.java
@@ -0,0 +1,177 @@
+/*
+ * 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.metrics;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * An event logged when the APF packet socket receives an RA packet.
+ * {@hide}
+ */
+@SystemApi
+@TestApi
+public final class RaEvent implements IpConnectivityLog.Event {
+
+    private static final long NO_LIFETIME = -1L;
+
+    // Lifetime in seconds of options found in a single RA packet.
+    // When an option is not set, the value of the associated field is -1;
+    /** @hide */
+    public final long routerLifetime;
+    /** @hide */
+    public final long prefixValidLifetime;
+    /** @hide */
+    public final long prefixPreferredLifetime;
+    /** @hide */
+    public final long routeInfoLifetime;
+    /** @hide */
+    public final long rdnssLifetime;
+    /** @hide */
+    public final long dnsslLifetime;
+
+    /** @hide */
+    public RaEvent(long routerLifetime, long prefixValidLifetime, long prefixPreferredLifetime,
+            long routeInfoLifetime, long rdnssLifetime, long dnsslLifetime) {
+        this.routerLifetime = routerLifetime;
+        this.prefixValidLifetime = prefixValidLifetime;
+        this.prefixPreferredLifetime = prefixPreferredLifetime;
+        this.routeInfoLifetime = routeInfoLifetime;
+        this.rdnssLifetime = rdnssLifetime;
+        this.dnsslLifetime = dnsslLifetime;
+    }
+
+    /** @hide */
+    private RaEvent(Parcel in) {
+        routerLifetime          = in.readLong();
+        prefixValidLifetime     = in.readLong();
+        prefixPreferredLifetime = in.readLong();
+        routeInfoLifetime       = in.readLong();
+        rdnssLifetime           = in.readLong();
+        dnsslLifetime           = in.readLong();
+    }
+
+    /** @hide */
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeLong(routerLifetime);
+        out.writeLong(prefixValidLifetime);
+        out.writeLong(prefixPreferredLifetime);
+        out.writeLong(routeInfoLifetime);
+        out.writeLong(rdnssLifetime);
+        out.writeLong(dnsslLifetime);
+    }
+
+    /** @hide */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public String toString() {
+        return new StringBuilder("RaEvent(lifetimes: ")
+                .append(String.format("router=%ds, ", routerLifetime))
+                .append(String.format("prefix_valid=%ds, ", prefixValidLifetime))
+                .append(String.format("prefix_preferred=%ds, ", prefixPreferredLifetime))
+                .append(String.format("route_info=%ds, ", routeInfoLifetime))
+                .append(String.format("rdnss=%ds, ", rdnssLifetime))
+                .append(String.format("dnssl=%ds)", dnsslLifetime))
+                .toString();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null || !(obj.getClass().equals(RaEvent.class))) return false;
+        final RaEvent other = (RaEvent) obj;
+        return routerLifetime == other.routerLifetime
+                && prefixValidLifetime == other.prefixValidLifetime
+                && prefixPreferredLifetime == other.prefixPreferredLifetime
+                && routeInfoLifetime == other.routeInfoLifetime
+                && rdnssLifetime == other.rdnssLifetime
+                && dnsslLifetime == other.dnsslLifetime;
+    }
+
+    /** @hide */
+    public static final @android.annotation.NonNull Parcelable.Creator<RaEvent> CREATOR = new Parcelable.Creator<RaEvent>() {
+        public RaEvent createFromParcel(Parcel in) {
+            return new RaEvent(in);
+        }
+
+        public RaEvent[] newArray(int size) {
+            return new RaEvent[size];
+        }
+    };
+
+    public static final class Builder {
+
+        long routerLifetime          = NO_LIFETIME;
+        long prefixValidLifetime     = NO_LIFETIME;
+        long prefixPreferredLifetime = NO_LIFETIME;
+        long routeInfoLifetime       = NO_LIFETIME;
+        long rdnssLifetime           = NO_LIFETIME;
+        long dnsslLifetime           = NO_LIFETIME;
+
+        public Builder() {
+        }
+
+        public @NonNull RaEvent build() {
+            return new RaEvent(routerLifetime, prefixValidLifetime, prefixPreferredLifetime,
+                    routeInfoLifetime, rdnssLifetime, dnsslLifetime);
+        }
+
+        public @NonNull Builder updateRouterLifetime(long lifetime) {
+            routerLifetime = updateLifetime(routerLifetime, lifetime);
+            return this;
+        }
+
+        public @NonNull Builder updatePrefixValidLifetime(long lifetime) {
+            prefixValidLifetime = updateLifetime(prefixValidLifetime, lifetime);
+            return this;
+        }
+
+        public @NonNull Builder updatePrefixPreferredLifetime(long lifetime) {
+            prefixPreferredLifetime = updateLifetime(prefixPreferredLifetime, lifetime);
+            return this;
+        }
+
+        public @NonNull Builder updateRouteInfoLifetime(long lifetime) {
+            routeInfoLifetime = updateLifetime(routeInfoLifetime, lifetime);
+            return this;
+        }
+
+        public @NonNull Builder updateRdnssLifetime(long lifetime) {
+            rdnssLifetime = updateLifetime(rdnssLifetime, lifetime);
+            return this;
+        }
+
+        public @NonNull Builder updateDnsslLifetime(long lifetime) {
+            dnsslLifetime = updateLifetime(dnsslLifetime, lifetime);
+            return this;
+        }
+
+        private long updateLifetime(long currentLifetime, long newLifetime) {
+            if (currentLifetime == RaEvent.NO_LIFETIME) {
+                return newLifetime;
+            }
+            return Math.min(currentLifetime, newLifetime);
+        }
+    }
+}
diff --git a/android/net/metrics/ValidationProbeEvent.java b/android/net/metrics/ValidationProbeEvent.java
new file mode 100644
index 0000000..1aaa50d
--- /dev/null
+++ b/android/net/metrics/ValidationProbeEvent.java
@@ -0,0 +1,187 @@
+/*
+ * 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.metrics;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.SparseArray;
+
+import com.android.internal.util.MessageUtils;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * An event recorded by NetworkMonitor when sending a probe for finding captive portals.
+ * {@hide}
+ */
+@SystemApi
+@TestApi
+public final class ValidationProbeEvent implements IpConnectivityLog.Event {
+
+    public static final int PROBE_DNS       = 0;
+    public static final int PROBE_HTTP      = 1;
+    public static final int PROBE_HTTPS     = 2;
+    public static final int PROBE_PAC       = 3;
+    public static final int PROBE_FALLBACK  = 4;
+    public static final int PROBE_PRIVDNS   = 5;
+
+    public static final int DNS_FAILURE = 0;
+    public static final int DNS_SUCCESS = 1;
+
+    private static final int FIRST_VALIDATION  = 1 << 8;
+    private static final int REVALIDATION      = 2 << 8;
+
+    /** @hide */
+    @IntDef(value = {DNS_FAILURE, DNS_SUCCESS})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ReturnCode {}
+
+    /** @hide */
+    public final long durationMs;
+    // probeType byte format (MSB to LSB):
+    // byte 0: unused
+    // byte 1: unused
+    // byte 2: 0 = UNKNOWN, 1 = FIRST_VALIDATION, 2 = REVALIDATION
+    // byte 3: PROBE_* constant
+    /** @hide */
+    public final int probeType;
+    /** @hide */
+    public final @ReturnCode int returnCode;
+
+    private ValidationProbeEvent(long durationMs, int probeType, int returnCode) {
+        this.durationMs = durationMs;
+        this.probeType = probeType;
+        this.returnCode = returnCode;
+    }
+
+    private ValidationProbeEvent(Parcel in) {
+        durationMs = in.readLong();
+        probeType = in.readInt();
+        returnCode = in.readInt();
+    }
+
+    /**
+     * Utility to create an instance of {@link ValidationProbeEvent}.
+     */
+    public static final class Builder {
+        private long mDurationMs;
+        private int mProbeType;
+        private int mReturnCode;
+
+        /**
+         * Set the duration of the probe in milliseconds.
+         */
+        @NonNull
+        public Builder setDurationMs(long durationMs) {
+            mDurationMs = durationMs;
+            return this;
+        }
+
+        /**
+         * Set the probe type based on whether it was the first validation.
+         */
+        @NonNull
+        public Builder setProbeType(int probeType, boolean firstValidation) {
+            mProbeType = makeProbeType(probeType, firstValidation);
+            return this;
+        }
+
+        /**
+         * Set the return code of the probe.
+         */
+        @NonNull
+        public Builder setReturnCode(int returnCode) {
+            mReturnCode = returnCode;
+            return this;
+        }
+
+        /**
+         * Create a new {@link ValidationProbeEvent}.
+         */
+        @NonNull
+        public ValidationProbeEvent build() {
+            return new ValidationProbeEvent(mDurationMs, mProbeType, mReturnCode);
+        }
+    }
+
+    /** @hide */
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeLong(durationMs);
+        out.writeInt(probeType);
+        out.writeInt(returnCode);
+    }
+
+    /** @hide */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /** @hide */
+    public static final @android.annotation.NonNull Parcelable.Creator<ValidationProbeEvent> CREATOR
+        = new Parcelable.Creator<ValidationProbeEvent>() {
+        public ValidationProbeEvent createFromParcel(Parcel in) {
+            return new ValidationProbeEvent(in);
+        }
+
+        public ValidationProbeEvent[] newArray(int size) {
+            return new ValidationProbeEvent[size];
+        }
+    };
+
+    private static int makeProbeType(int probeType, boolean firstValidation) {
+        return (probeType & 0xff) | (firstValidation ? FIRST_VALIDATION : REVALIDATION);
+    }
+
+    /**
+     * Get the name of a probe specified by its probe type.
+     */
+    public static @NonNull String getProbeName(int probeType) {
+        return Decoder.constants.get(probeType & 0xff, "PROBE_???");
+    }
+
+    private static @NonNull String getValidationStage(int probeType) {
+        return Decoder.constants.get(probeType & 0xff00, "UNKNOWN");
+    }
+
+    @Override
+    public String toString() {
+        return String.format("ValidationProbeEvent(%s:%d %s, %dms)",
+                getProbeName(probeType), returnCode, getValidationStage(probeType), durationMs);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == null || !(obj.getClass().equals(ValidationProbeEvent.class))) return false;
+        final ValidationProbeEvent other = (ValidationProbeEvent) obj;
+        return durationMs == other.durationMs
+                && probeType == other.probeType
+                && returnCode == other.returnCode;
+    }
+
+    final static class Decoder {
+        static final SparseArray<String> constants = MessageUtils.findMessageNames(
+                new Class[]{ValidationProbeEvent.class},
+                new String[]{"PROBE_", "FIRST_", "REVALIDATION"});
+    }
+}
diff --git a/android/net/metrics/WakeupEvent.java b/android/net/metrics/WakeupEvent.java
new file mode 100644
index 0000000..af9a73c
--- /dev/null
+++ b/android/net/metrics/WakeupEvent.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.metrics;
+
+import android.net.MacAddress;
+
+import java.util.StringJoiner;
+
+/**
+ * An event logged when NFLOG notifies userspace of a wakeup packet for
+ * watched interfaces.
+ * {@hide}
+ */
+public class WakeupEvent {
+    public String iface;
+    public int uid;
+    public int ethertype;
+    public MacAddress dstHwAddr;
+    public String srcIp;
+    public String dstIp;
+    public int ipNextHeader;
+    public int srcPort;
+    public int dstPort;
+    public long timestampMs;
+
+    @Override
+    public String toString() {
+        StringJoiner j = new StringJoiner(", ", "WakeupEvent(", ")");
+        j.add(String.format("%tT.%tL", timestampMs, timestampMs));
+        j.add(iface);
+        j.add("uid: " + Integer.toString(uid));
+        j.add("eth=0x" + Integer.toHexString(ethertype));
+        j.add("dstHw=" + dstHwAddr);
+        if (ipNextHeader > 0) {
+            j.add("ipNxtHdr=" + ipNextHeader);
+            j.add("srcIp=" + srcIp);
+            j.add("dstIp=" + dstIp);
+            if (srcPort > -1) {
+                j.add("srcPort=" + srcPort);
+            }
+            if (dstPort > -1) {
+                j.add("dstPort=" + dstPort);
+            }
+        }
+        return j.toString();
+    }
+}
diff --git a/android/net/metrics/WakeupStats.java b/android/net/metrics/WakeupStats.java
new file mode 100644
index 0000000..bb36536
--- /dev/null
+++ b/android/net/metrics/WakeupStats.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.metrics;
+
+import android.net.MacAddress;
+import android.os.Process;
+import android.os.SystemClock;
+import android.util.SparseIntArray;
+
+import java.util.StringJoiner;
+
+/**
+ * An event logged per interface and that aggregates WakeupEvents for that interface.
+ * {@hide}
+ */
+public class WakeupStats {
+
+    private static final int NO_UID = -1;
+
+    public final long creationTimeMs = SystemClock.elapsedRealtime();
+    public final String iface;
+
+    public long totalWakeups = 0;
+    public long rootWakeups = 0;
+    public long systemWakeups = 0;
+    public long nonApplicationWakeups = 0;
+    public long applicationWakeups = 0;
+    public long noUidWakeups = 0;
+    public long durationSec = 0;
+
+    public long l2UnicastCount = 0;
+    public long l2MulticastCount = 0;
+    public long l2BroadcastCount = 0;
+
+    public final SparseIntArray ethertypes = new SparseIntArray();
+    public final SparseIntArray ipNextHeaders = new SparseIntArray();
+
+    public WakeupStats(String iface) {
+        this.iface = iface;
+    }
+
+    /** Update durationSec with current time. */
+    public void updateDuration() {
+        durationSec = (SystemClock.elapsedRealtime() - creationTimeMs) / 1000;
+    }
+
+    /** Update wakeup counters for the given WakeupEvent. */
+    public void countEvent(WakeupEvent ev) {
+        totalWakeups++;
+        switch (ev.uid) {
+            case Process.ROOT_UID:
+                rootWakeups++;
+                break;
+            case Process.SYSTEM_UID:
+                systemWakeups++;
+                break;
+            case NO_UID:
+                noUidWakeups++;
+                break;
+            default:
+                if (ev.uid >= Process.FIRST_APPLICATION_UID) {
+                    applicationWakeups++;
+                } else {
+                    nonApplicationWakeups++;
+                }
+                break;
+        }
+
+        switch (ev.dstHwAddr.getAddressType()) {
+            case MacAddress.TYPE_UNICAST:
+                l2UnicastCount++;
+                break;
+            case MacAddress.TYPE_MULTICAST:
+                l2MulticastCount++;
+                break;
+            case MacAddress.TYPE_BROADCAST:
+                l2BroadcastCount++;
+                break;
+            default:
+                break;
+        }
+
+        increment(ethertypes, ev.ethertype);
+        if (ev.ipNextHeader >= 0) {
+            increment(ipNextHeaders, ev.ipNextHeader);
+        }
+    }
+
+    @Override
+    public String toString() {
+        updateDuration();
+        StringJoiner j = new StringJoiner(", ", "WakeupStats(", ")");
+        j.add(iface);
+        j.add("" + durationSec + "s");
+        j.add("total: " + totalWakeups);
+        j.add("root: " + rootWakeups);
+        j.add("system: " + systemWakeups);
+        j.add("apps: " + applicationWakeups);
+        j.add("non-apps: " + nonApplicationWakeups);
+        j.add("no uid: " + noUidWakeups);
+        j.add(String.format("l2 unicast/multicast/broadcast: %d/%d/%d",
+                l2UnicastCount, l2MulticastCount, l2BroadcastCount));
+        for (int i = 0; i < ethertypes.size(); i++) {
+            int eth = ethertypes.keyAt(i);
+            int count = ethertypes.valueAt(i);
+            j.add(String.format("ethertype 0x%x: %d", eth, count));
+        }
+        for (int i = 0; i < ipNextHeaders.size(); i++) {
+            int proto = ipNextHeaders.keyAt(i);
+            int count = ipNextHeaders.valueAt(i);
+            j.add(String.format("ipNxtHdr %d: %d", proto, count));
+        }
+        return j.toString();
+    }
+
+    private static void increment(SparseIntArray counters, int key) {
+        int newcount = counters.get(key, 0) + 1;
+        counters.put(key, newcount);
+    }
+}
diff --git a/android/net/netlink/ConntrackMessage.java b/android/net/netlink/ConntrackMessage.java
new file mode 100644
index 0000000..6978739
--- /dev/null
+++ b/android/net/netlink/ConntrackMessage.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.netlink;
+
+import static android.net.netlink.StructNlMsgHdr.NLM_F_ACK;
+import static android.net.netlink.StructNlMsgHdr.NLM_F_REPLACE;
+import static android.net.netlink.StructNlMsgHdr.NLM_F_REQUEST;
+
+import static java.nio.ByteOrder.BIG_ENDIAN;
+
+import android.system.OsConstants;
+
+import java.net.Inet4Address;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+
+/**
+ * A NetlinkMessage subclass for netlink conntrack messages.
+ *
+ * see also: &lt;linux_src&gt;/include/uapi/linux/netfilter/nfnetlink_conntrack.h
+ *
+ * @hide
+ */
+public class ConntrackMessage extends NetlinkMessage {
+    public static final int STRUCT_SIZE = StructNlMsgHdr.STRUCT_SIZE + StructNfGenMsg.STRUCT_SIZE;
+
+    public static final short NFNL_SUBSYS_CTNETLINK = 1;
+    public static final short IPCTNL_MSG_CT_NEW = 0;
+
+    // enum ctattr_type
+    public static final short CTA_TUPLE_ORIG  = 1;
+    public static final short CTA_TUPLE_REPLY = 2;
+    public static final short CTA_TIMEOUT     = 7;
+
+    // enum ctattr_tuple
+    public static final short CTA_TUPLE_IP    = 1;
+    public static final short CTA_TUPLE_PROTO = 2;
+
+    // enum ctattr_ip
+    public static final short CTA_IP_V4_SRC = 1;
+    public static final short CTA_IP_V4_DST = 2;
+
+    // enum ctattr_l4proto
+    public static final short CTA_PROTO_NUM      = 1;
+    public static final short CTA_PROTO_SRC_PORT = 2;
+    public static final short CTA_PROTO_DST_PORT = 3;
+
+    public static byte[] newIPv4TimeoutUpdateRequest(
+            int proto, Inet4Address src, int sport, Inet4Address dst, int dport, int timeoutSec) {
+        // *** STYLE WARNING ***
+        //
+        // Code below this point uses extra block indentation to highlight the
+        // packing of nested tuple netlink attribute types.
+        final StructNlAttr ctaTupleOrig = new StructNlAttr(CTA_TUPLE_ORIG,
+                new StructNlAttr(CTA_TUPLE_IP,
+                        new StructNlAttr(CTA_IP_V4_SRC, src),
+                        new StructNlAttr(CTA_IP_V4_DST, dst)),
+                new StructNlAttr(CTA_TUPLE_PROTO,
+                        new StructNlAttr(CTA_PROTO_NUM, (byte) proto),
+                        new StructNlAttr(CTA_PROTO_SRC_PORT, (short) sport, BIG_ENDIAN),
+                        new StructNlAttr(CTA_PROTO_DST_PORT, (short) dport, BIG_ENDIAN)));
+
+        final StructNlAttr ctaTimeout = new StructNlAttr(CTA_TIMEOUT, timeoutSec, BIG_ENDIAN);
+
+        final int payloadLength = ctaTupleOrig.getAlignedLength() + ctaTimeout.getAlignedLength();
+        final byte[] bytes = new byte[STRUCT_SIZE + payloadLength];
+        final ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
+        byteBuffer.order(ByteOrder.nativeOrder());
+
+        final ConntrackMessage ctmsg = new ConntrackMessage();
+        ctmsg.mHeader.nlmsg_len = bytes.length;
+        ctmsg.mHeader.nlmsg_type = (NFNL_SUBSYS_CTNETLINK << 8) | IPCTNL_MSG_CT_NEW;
+        ctmsg.mHeader.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_REPLACE;
+        ctmsg.mHeader.nlmsg_seq = 1;
+        ctmsg.pack(byteBuffer);
+
+        ctaTupleOrig.pack(byteBuffer);
+        ctaTimeout.pack(byteBuffer);
+
+        return bytes;
+    }
+
+    protected StructNfGenMsg mNfGenMsg;
+
+    private ConntrackMessage() {
+        super(new StructNlMsgHdr());
+        mNfGenMsg = new StructNfGenMsg((byte) OsConstants.AF_INET);
+    }
+
+    public void pack(ByteBuffer byteBuffer) {
+        mHeader.pack(byteBuffer);
+        mNfGenMsg.pack(byteBuffer);
+    }
+}
diff --git a/android/net/netlink/InetDiagMessage.java b/android/net/netlink/InetDiagMessage.java
new file mode 100644
index 0000000..af9e601
--- /dev/null
+++ b/android/net/netlink/InetDiagMessage.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2018 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.netlink;
+
+import static android.os.Process.INVALID_UID;
+import static android.net.netlink.NetlinkConstants.SOCK_DIAG_BY_FAMILY;
+import static android.net.netlink.NetlinkSocket.DEFAULT_RECV_BUFSIZE;
+import static android.net.netlink.StructNlMsgHdr.NLM_F_DUMP;
+import static android.net.netlink.StructNlMsgHdr.NLM_F_REQUEST;
+import static android.system.OsConstants.AF_INET;
+import static android.system.OsConstants.AF_INET6;
+import static android.system.OsConstants.IPPROTO_UDP;
+import static android.system.OsConstants.NETLINK_INET_DIAG;
+
+import android.os.Build;
+import android.os.Process;
+import android.system.ErrnoException;
+import android.util.Log;
+
+import java.io.FileDescriptor;
+import java.io.InterruptedIOException;
+import java.net.DatagramSocket;
+import java.net.DatagramSocket;
+import java.net.InetAddress;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetSocketAddress;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * A NetlinkMessage subclass for netlink inet_diag messages.
+ *
+ * see also: &lt;linux_src&gt;/include/uapi/linux/inet_diag.h
+ *
+ * @hide
+ */
+public class InetDiagMessage extends NetlinkMessage {
+    public static final String TAG = "InetDiagMessage";
+    private static final int TIMEOUT_MS = 500;
+
+    public static byte[] InetDiagReqV2(int protocol, InetSocketAddress local,
+                                       InetSocketAddress remote, int family, short flags) {
+        final byte[] bytes = new byte[StructNlMsgHdr.STRUCT_SIZE + StructInetDiagReqV2.STRUCT_SIZE];
+        final ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
+        byteBuffer.order(ByteOrder.nativeOrder());
+
+        final StructNlMsgHdr nlMsgHdr = new StructNlMsgHdr();
+        nlMsgHdr.nlmsg_len = bytes.length;
+        nlMsgHdr.nlmsg_type = SOCK_DIAG_BY_FAMILY;
+        nlMsgHdr.nlmsg_flags = flags;
+        nlMsgHdr.pack(byteBuffer);
+
+        final StructInetDiagReqV2 inetDiagReqV2 = new StructInetDiagReqV2(protocol, local, remote,
+                family);
+        inetDiagReqV2.pack(byteBuffer);
+        return bytes;
+    }
+
+    public StructInetDiagMsg mStructInetDiagMsg;
+
+    private InetDiagMessage(StructNlMsgHdr header) {
+        super(header);
+        mStructInetDiagMsg = new StructInetDiagMsg();
+    }
+
+    public static InetDiagMessage parse(StructNlMsgHdr header, ByteBuffer byteBuffer) {
+        final InetDiagMessage msg = new InetDiagMessage(header);
+        msg.mStructInetDiagMsg = StructInetDiagMsg.parse(byteBuffer);
+        return msg;
+    }
+
+    private static int lookupUidByFamily(int protocol, InetSocketAddress local,
+                                         InetSocketAddress remote, int family, short flags,
+                                         FileDescriptor fd)
+            throws ErrnoException, InterruptedIOException {
+        byte[] msg = InetDiagReqV2(protocol, local, remote, family, flags);
+        NetlinkSocket.sendMessage(fd, msg, 0, msg.length, TIMEOUT_MS);
+        ByteBuffer response = NetlinkSocket.recvMessage(fd, DEFAULT_RECV_BUFSIZE, TIMEOUT_MS);
+
+        final NetlinkMessage nlMsg = NetlinkMessage.parse(response);
+        final StructNlMsgHdr hdr = nlMsg.getHeader();
+        if (hdr.nlmsg_type == NetlinkConstants.NLMSG_DONE) {
+            return INVALID_UID;
+        }
+        if (nlMsg instanceof InetDiagMessage) {
+            return ((InetDiagMessage) nlMsg).mStructInetDiagMsg.idiag_uid;
+        }
+        return INVALID_UID;
+    }
+
+    private static final int FAMILY[] = {AF_INET6, AF_INET};
+
+    private static int lookupUid(int protocol, InetSocketAddress local,
+                                 InetSocketAddress remote, FileDescriptor fd)
+            throws ErrnoException, InterruptedIOException {
+        int uid;
+
+        for (int family : FAMILY) {
+            /**
+             * For exact match lookup, swap local and remote for UDP lookups due to kernel
+             * bug which will not be fixed. See aosp/755889 and
+             * https://www.mail-archive.com/[email protected]/msg248638.html
+             */
+            if (protocol == IPPROTO_UDP) {
+                uid = lookupUidByFamily(protocol, remote, local, family, NLM_F_REQUEST, fd);
+            } else {
+                uid = lookupUidByFamily(protocol, local, remote, family, NLM_F_REQUEST, fd);
+            }
+            if (uid != INVALID_UID) {
+                return uid;
+            }
+        }
+
+        /**
+         * For UDP it's possible for a socket to send packets to arbitrary destinations, even if the
+         * socket is not connected (and even if the socket is connected to a different destination).
+         * If we want this API to work for such packets, then on miss we need to do a second lookup
+         * with only the local address and port filled in.
+         * Always use flags == NLM_F_REQUEST | NLM_F_DUMP for wildcard.
+         */
+        if (protocol == IPPROTO_UDP) {
+            try {
+                InetSocketAddress wildcard = new InetSocketAddress(
+                        Inet6Address.getByName("::"), 0);
+                uid = lookupUidByFamily(protocol, local, wildcard, AF_INET6,
+                        (short) (NLM_F_REQUEST | NLM_F_DUMP), fd);
+                if (uid != INVALID_UID) {
+                    return uid;
+                }
+                wildcard = new InetSocketAddress(Inet4Address.getByName("0.0.0.0"), 0);
+                uid = lookupUidByFamily(protocol, local, wildcard, AF_INET,
+                        (short) (NLM_F_REQUEST | NLM_F_DUMP), fd);
+                if (uid != INVALID_UID) {
+                    return uid;
+                }
+            } catch (UnknownHostException e) {
+                Log.e(TAG, e.toString());
+            }
+        }
+        return INVALID_UID;
+    }
+
+    /**
+     * Use an inet_diag socket to look up the UID associated with the input local and remote
+     * address/port and protocol of a connection.
+     */
+    public static int getConnectionOwnerUid(int protocol, InetSocketAddress local,
+                                            InetSocketAddress remote) {
+        try {
+            final FileDescriptor fd = NetlinkSocket.forProto(NETLINK_INET_DIAG);
+            NetlinkSocket.connectToKernel(fd);
+
+            return lookupUid(protocol, local, remote, fd);
+
+        } catch (ErrnoException | SocketException | IllegalArgumentException
+                | InterruptedIOException e) {
+            Log.e(TAG, e.toString());
+        }
+        return INVALID_UID;
+    }
+
+    @Override
+    public String toString() {
+        return "InetDiagMessage{ "
+                + "nlmsghdr{" + (mHeader == null ? "" : mHeader.toString()) + "}, "
+                + "inet_diag_msg{"
+                + (mStructInetDiagMsg == null ? "" : mStructInetDiagMsg.toString()) + "} "
+                + "}";
+    }
+}
diff --git a/android/net/netlink/NetlinkConstants.java b/android/net/netlink/NetlinkConstants.java
new file mode 100644
index 0000000..fc1551c
--- /dev/null
+++ b/android/net/netlink/NetlinkConstants.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright (C) 2015 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.netlink;
+
+import android.system.OsConstants;
+import com.android.internal.util.HexDump;
+
+import java.nio.ByteBuffer;
+
+
+/**
+ * Various constants and static helper methods for netlink communications.
+ *
+ * Values taken from:
+ *
+ *     &lt;linux_src&gt;/include/uapi/linux/netlink.h
+ *     &lt;linux_src&gt;/include/uapi/linux/rtnetlink.h
+ *
+ * @hide
+ */
+public class NetlinkConstants {
+    private NetlinkConstants() {}
+
+    public static final int NLA_ALIGNTO = 4;
+
+    public static final int alignedLengthOf(short length) {
+        final int intLength = (int) length & 0xffff;
+        return alignedLengthOf(intLength);
+    }
+
+    public static final int alignedLengthOf(int length) {
+        if (length <= 0) { return 0; }
+        return (((length + NLA_ALIGNTO - 1) / NLA_ALIGNTO) * NLA_ALIGNTO);
+    }
+
+    public static String stringForAddressFamily(int family) {
+        if (family == OsConstants.AF_INET) { return "AF_INET"; }
+        if (family == OsConstants.AF_INET6) { return "AF_INET6"; }
+        if (family == OsConstants.AF_NETLINK) { return "AF_NETLINK"; }
+        return String.valueOf(family);
+    }
+
+    public static String stringForProtocol(int protocol) {
+        if (protocol == OsConstants.IPPROTO_TCP) { return "IPPROTO_TCP"; }
+        if (protocol == OsConstants.IPPROTO_UDP) { return "IPPROTO_UDP"; }
+        return String.valueOf(protocol);
+    }
+
+    public static String hexify(byte[] bytes) {
+        if (bytes == null) { return "(null)"; }
+        return HexDump.toHexString(bytes);
+    }
+
+    public static String hexify(ByteBuffer buffer) {
+        if (buffer == null) { return "(null)"; }
+        return HexDump.toHexString(
+                buffer.array(), buffer.position(), buffer.remaining());
+    }
+
+    // Known values for struct nlmsghdr nlm_type.
+    public static final short NLMSG_NOOP         = 1;   // Nothing
+    public static final short NLMSG_ERROR        = 2;   // Error
+    public static final short NLMSG_DONE         = 3;   // End of a dump
+    public static final short NLMSG_OVERRUN      = 4;   // Data lost
+    public static final short NLMSG_MAX_RESERVED = 15;  // Max reserved value
+
+    public static final short RTM_NEWLINK        = 16;
+    public static final short RTM_DELLINK        = 17;
+    public static final short RTM_GETLINK        = 18;
+    public static final short RTM_SETLINK        = 19;
+    public static final short RTM_NEWADDR        = 20;
+    public static final short RTM_DELADDR        = 21;
+    public static final short RTM_GETADDR        = 22;
+    public static final short RTM_NEWROUTE       = 24;
+    public static final short RTM_DELROUTE       = 25;
+    public static final short RTM_GETROUTE       = 26;
+    public static final short RTM_NEWNEIGH       = 28;
+    public static final short RTM_DELNEIGH       = 29;
+    public static final short RTM_GETNEIGH       = 30;
+    public static final short RTM_NEWRULE        = 32;
+    public static final short RTM_DELRULE        = 33;
+    public static final short RTM_GETRULE        = 34;
+    public static final short RTM_NEWNDUSEROPT   = 68;
+
+    /* see &lt;linux_src&gt;/include/uapi/linux/sock_diag.h */
+    public static final short SOCK_DIAG_BY_FAMILY = 20;
+
+    public static String stringForNlMsgType(short nlm_type) {
+        switch (nlm_type) {
+            case NLMSG_NOOP: return "NLMSG_NOOP";
+            case NLMSG_ERROR: return "NLMSG_ERROR";
+            case NLMSG_DONE: return "NLMSG_DONE";
+            case NLMSG_OVERRUN: return "NLMSG_OVERRUN";
+            case RTM_NEWLINK: return "RTM_NEWLINK";
+            case RTM_DELLINK: return "RTM_DELLINK";
+            case RTM_GETLINK: return "RTM_GETLINK";
+            case RTM_SETLINK: return "RTM_SETLINK";
+            case RTM_NEWADDR: return "RTM_NEWADDR";
+            case RTM_DELADDR: return "RTM_DELADDR";
+            case RTM_GETADDR: return "RTM_GETADDR";
+            case RTM_NEWROUTE: return "RTM_NEWROUTE";
+            case RTM_DELROUTE: return "RTM_DELROUTE";
+            case RTM_GETROUTE: return "RTM_GETROUTE";
+            case RTM_NEWNEIGH: return "RTM_NEWNEIGH";
+            case RTM_DELNEIGH: return "RTM_DELNEIGH";
+            case RTM_GETNEIGH: return "RTM_GETNEIGH";
+            case RTM_NEWRULE: return "RTM_NEWRULE";
+            case RTM_DELRULE: return "RTM_DELRULE";
+            case RTM_GETRULE: return "RTM_GETRULE";
+            case RTM_NEWNDUSEROPT: return "RTM_NEWNDUSEROPT";
+            default:
+                return "unknown RTM type: " + String.valueOf(nlm_type);
+        }
+    }
+}
diff --git a/android/net/netlink/NetlinkErrorMessage.java b/android/net/netlink/NetlinkErrorMessage.java
new file mode 100644
index 0000000..e275574
--- /dev/null
+++ b/android/net/netlink/NetlinkErrorMessage.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright (C) 2015 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.netlink;
+
+import android.net.netlink.StructNlMsgHdr;
+import android.net.netlink.NetlinkMessage;
+
+import java.nio.ByteBuffer;
+
+
+/**
+ * A NetlinkMessage subclass for netlink error messages.
+ *
+ * @hide
+ */
+public class NetlinkErrorMessage extends NetlinkMessage {
+
+    public static NetlinkErrorMessage parse(StructNlMsgHdr header, ByteBuffer byteBuffer) {
+        final NetlinkErrorMessage errorMsg = new NetlinkErrorMessage(header);
+
+        errorMsg.mNlMsgErr = StructNlMsgErr.parse(byteBuffer);
+        if (errorMsg.mNlMsgErr == null) {
+            return null;
+        }
+
+        return errorMsg;
+    }
+
+    private StructNlMsgErr mNlMsgErr;
+
+    NetlinkErrorMessage(StructNlMsgHdr header) {
+        super(header);
+        mNlMsgErr = null;
+    }
+
+    public StructNlMsgErr getNlMsgError() {
+        return mNlMsgErr;
+    }
+
+    @Override
+    public String toString() {
+        return "NetlinkErrorMessage{ "
+                + "nlmsghdr{" + (mHeader == null ? "" : mHeader.toString()) + "}, "
+                + "nlmsgerr{" + (mNlMsgErr == null ? "" : mNlMsgErr.toString()) + "} "
+                + "}";
+    }
+}
diff --git a/android/net/netlink/NetlinkMessage.java b/android/net/netlink/NetlinkMessage.java
new file mode 100644
index 0000000..a325db8
--- /dev/null
+++ b/android/net/netlink/NetlinkMessage.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2015 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.netlink;
+
+import android.net.netlink.NetlinkConstants;
+import android.net.netlink.NetlinkErrorMessage;
+import android.net.netlink.RtNetlinkNeighborMessage;
+import android.net.netlink.StructNlAttr;
+import android.net.netlink.StructNlMsgHdr;
+import android.util.Log;
+
+import java.nio.ByteBuffer;
+
+
+/**
+ * NetlinkMessage base class for other, more specific netlink message types.
+ *
+ * Classes that extend NetlinkMessage should:
+ *     - implement a public static parse(StructNlMsgHdr, ByteBuffer) method
+ *     - returning either null (parse errors) or a new object of the subclass
+ *       type (cast-able to NetlinkMessage)
+ *
+ * NetlinkMessage.parse() should be updated to know which nlmsg_type values
+ * correspond with which message subclasses.
+ *
+ * @hide
+ */
+public class NetlinkMessage {
+    private final static String TAG = "NetlinkMessage";
+
+    public static NetlinkMessage parse(ByteBuffer byteBuffer) {
+        final int startPosition = (byteBuffer != null) ? byteBuffer.position() : -1;
+        final StructNlMsgHdr nlmsghdr = StructNlMsgHdr.parse(byteBuffer);
+        if (nlmsghdr == null) {
+            return null;
+        }
+
+        int payloadLength = NetlinkConstants.alignedLengthOf(nlmsghdr.nlmsg_len);
+        payloadLength -= StructNlMsgHdr.STRUCT_SIZE;
+        if (payloadLength < 0 || payloadLength > byteBuffer.remaining()) {
+            // Malformed message or runt buffer.  Pretend the buffer was consumed.
+            byteBuffer.position(byteBuffer.limit());
+            return null;
+        }
+
+        switch (nlmsghdr.nlmsg_type) {
+            //case NetlinkConstants.NLMSG_NOOP:
+            case NetlinkConstants.NLMSG_ERROR:
+                return (NetlinkMessage) NetlinkErrorMessage.parse(nlmsghdr, byteBuffer);
+            case NetlinkConstants.NLMSG_DONE:
+                byteBuffer.position(byteBuffer.position() + payloadLength);
+                return new NetlinkMessage(nlmsghdr);
+            //case NetlinkConstants.NLMSG_OVERRUN:
+            case NetlinkConstants.RTM_NEWNEIGH:
+            case NetlinkConstants.RTM_DELNEIGH:
+            case NetlinkConstants.RTM_GETNEIGH:
+                return (NetlinkMessage) RtNetlinkNeighborMessage.parse(nlmsghdr, byteBuffer);
+            case NetlinkConstants.SOCK_DIAG_BY_FAMILY:
+                return (NetlinkMessage) InetDiagMessage.parse(nlmsghdr, byteBuffer);
+            default:
+                if (nlmsghdr.nlmsg_type <= NetlinkConstants.NLMSG_MAX_RESERVED) {
+                    // Netlink control message.  Just parse the header for now,
+                    // pretending the whole message was consumed.
+                    byteBuffer.position(byteBuffer.position() + payloadLength);
+                    return new NetlinkMessage(nlmsghdr);
+                }
+                return null;
+        }
+    }
+
+    protected StructNlMsgHdr mHeader;
+
+    public NetlinkMessage(StructNlMsgHdr nlmsghdr) {
+        mHeader = nlmsghdr;
+    }
+
+    public StructNlMsgHdr getHeader() {
+        return mHeader;
+    }
+
+    @Override
+    public String toString() {
+        return "NetlinkMessage{" + (mHeader == null ? "" : mHeader.toString()) + "}";
+    }
+}
diff --git a/android/net/netlink/NetlinkSocket.java b/android/net/netlink/NetlinkSocket.java
new file mode 100644
index 0000000..7311fc5
--- /dev/null
+++ b/android/net/netlink/NetlinkSocket.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2015 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.netlink;
+
+import static android.net.util.SocketUtils.makeNetlinkSocketAddress;
+import static android.system.OsConstants.AF_NETLINK;
+import static android.system.OsConstants.EIO;
+import static android.system.OsConstants.EPROTO;
+import static android.system.OsConstants.ETIMEDOUT;
+import static android.system.OsConstants.SOCK_DGRAM;
+import static android.system.OsConstants.SOL_SOCKET;
+import static android.system.OsConstants.SO_RCVBUF;
+import static android.system.OsConstants.SO_RCVTIMEO;
+import static android.system.OsConstants.SO_SNDTIMEO;
+
+import android.net.util.SocketUtils;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.system.StructTimeval;
+import android.util.Log;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.net.SocketException;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+
+/**
+ * NetlinkSocket
+ *
+ * A small static class to assist with AF_NETLINK socket operations.
+ *
+ * @hide
+ */
+public class NetlinkSocket {
+    private static final String TAG = "NetlinkSocket";
+
+    public static final int DEFAULT_RECV_BUFSIZE = 8 * 1024;
+    public static final int SOCKET_RECV_BUFSIZE = 64 * 1024;
+
+    public static void sendOneShotKernelMessage(int nlProto, byte[] msg) throws ErrnoException {
+        final String errPrefix = "Error in NetlinkSocket.sendOneShotKernelMessage";
+        final long IO_TIMEOUT = 300L;
+
+        final FileDescriptor fd = forProto(nlProto);
+
+        try {
+            connectToKernel(fd);
+            sendMessage(fd, msg, 0, msg.length, IO_TIMEOUT);
+            final ByteBuffer bytes = recvMessage(fd, DEFAULT_RECV_BUFSIZE, IO_TIMEOUT);
+            // recvMessage() guaranteed to not return null if it did not throw.
+            final NetlinkMessage response = NetlinkMessage.parse(bytes);
+            if (response != null && response instanceof NetlinkErrorMessage &&
+                    (((NetlinkErrorMessage) response).getNlMsgError() != null)) {
+                final int errno = ((NetlinkErrorMessage) response).getNlMsgError().error;
+                if (errno != 0) {
+                    // TODO: consider ignoring EINVAL (-22), which appears to be
+                    // normal when probing a neighbor for which the kernel does
+                    // not already have / no longer has a link layer address.
+                    Log.e(TAG, errPrefix + ", errmsg=" + response.toString());
+                    // Note: convert kernel errnos (negative) into userspace errnos (positive).
+                    throw new ErrnoException(response.toString(), Math.abs(errno));
+                }
+            } else {
+                final String errmsg;
+                if (response == null) {
+                    bytes.position(0);
+                    errmsg = "raw bytes: " + NetlinkConstants.hexify(bytes);
+                } else {
+                    errmsg = response.toString();
+                }
+                Log.e(TAG, errPrefix + ", errmsg=" + errmsg);
+                throw new ErrnoException(errmsg, EPROTO);
+            }
+        } catch (InterruptedIOException e) {
+            Log.e(TAG, errPrefix, e);
+            throw new ErrnoException(errPrefix, ETIMEDOUT, e);
+        } catch (SocketException e) {
+            Log.e(TAG, errPrefix, e);
+            throw new ErrnoException(errPrefix, EIO, e);
+        } finally {
+            try {
+                SocketUtils.closeSocket(fd);
+            } catch (IOException e) {
+                // Nothing we can do here
+            }
+        }
+    }
+
+    public static FileDescriptor forProto(int nlProto) throws ErrnoException {
+        final FileDescriptor fd = Os.socket(AF_NETLINK, SOCK_DGRAM, nlProto);
+        Os.setsockoptInt(fd, SOL_SOCKET, SO_RCVBUF, SOCKET_RECV_BUFSIZE);
+        return fd;
+    }
+
+    public static void connectToKernel(FileDescriptor fd) throws ErrnoException, SocketException {
+        Os.connect(fd, makeNetlinkSocketAddress(0, 0));
+    }
+
+    private static void checkTimeout(long timeoutMs) {
+        if (timeoutMs < 0) {
+            throw new IllegalArgumentException("Negative timeouts not permitted");
+        }
+    }
+
+    /**
+     * Wait up to |timeoutMs| (or until underlying socket error) for a
+     * netlink message of at most |bufsize| size.
+     *
+     * Multi-threaded calls with different timeouts will cause unexpected results.
+     */
+    public static ByteBuffer recvMessage(FileDescriptor fd, int bufsize, long timeoutMs)
+            throws ErrnoException, IllegalArgumentException, InterruptedIOException {
+        checkTimeout(timeoutMs);
+
+        Os.setsockoptTimeval(fd, SOL_SOCKET, SO_RCVTIMEO, StructTimeval.fromMillis(timeoutMs));
+
+        ByteBuffer byteBuffer = ByteBuffer.allocate(bufsize);
+        int length = Os.read(fd, byteBuffer);
+        if (length == bufsize) {
+            Log.w(TAG, "maximum read");
+        }
+        byteBuffer.position(0);
+        byteBuffer.limit(length);
+        byteBuffer.order(ByteOrder.nativeOrder());
+        return byteBuffer;
+    }
+
+    /**
+     * Send a message to a peer to which this socket has previously connected,
+     * waiting at most |timeoutMs| milliseconds for the send to complete.
+     *
+     * Multi-threaded calls with different timeouts will cause unexpected results.
+     */
+    public static int sendMessage(
+            FileDescriptor fd, byte[] bytes, int offset, int count, long timeoutMs)
+            throws ErrnoException, IllegalArgumentException, InterruptedIOException {
+        checkTimeout(timeoutMs);
+        Os.setsockoptTimeval(fd, SOL_SOCKET, SO_SNDTIMEO, StructTimeval.fromMillis(timeoutMs));
+        return Os.write(fd, bytes, offset, count);
+    }
+}
diff --git a/android/net/netlink/RtNetlinkNeighborMessage.java b/android/net/netlink/RtNetlinkNeighborMessage.java
new file mode 100644
index 0000000..e784fbb
--- /dev/null
+++ b/android/net/netlink/RtNetlinkNeighborMessage.java
@@ -0,0 +1,251 @@
+/*
+ * Copyright (C) 2015 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.netlink;
+
+import static android.net.netlink.StructNlMsgHdr.NLM_F_ACK;
+import static android.net.netlink.StructNlMsgHdr.NLM_F_DUMP;
+import static android.net.netlink.StructNlMsgHdr.NLM_F_REPLACE;
+import static android.net.netlink.StructNlMsgHdr.NLM_F_REQUEST;
+
+import android.net.netlink.StructNdaCacheInfo;
+import android.net.netlink.StructNdMsg;
+import android.net.netlink.StructNlAttr;
+import android.net.netlink.StructNlMsgHdr;
+import android.net.netlink.NetlinkMessage;
+import android.system.OsConstants;
+import android.util.Log;
+
+import java.net.InetAddress;
+import java.net.Inet6Address;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+
+/**
+ * A NetlinkMessage subclass for rtnetlink neighbor messages.
+ *
+ * see also: &lt;linux_src&gt;/include/uapi/linux/neighbour.h
+ *
+ * @hide
+ */
+public class RtNetlinkNeighborMessage extends NetlinkMessage {
+    public static final short NDA_UNSPEC    = 0;
+    public static final short NDA_DST       = 1;
+    public static final short NDA_LLADDR    = 2;
+    public static final short NDA_CACHEINFO = 3;
+    public static final short NDA_PROBES    = 4;
+    public static final short NDA_VLAN      = 5;
+    public static final short NDA_PORT      = 6;
+    public static final short NDA_VNI       = 7;
+    public static final short NDA_IFINDEX   = 8;
+    public static final short NDA_MASTER    = 9;
+
+    private static StructNlAttr findNextAttrOfType(short attrType, ByteBuffer byteBuffer) {
+        while (byteBuffer != null && byteBuffer.remaining() > 0) {
+            final StructNlAttr nlAttr = StructNlAttr.peek(byteBuffer);
+            if (nlAttr == null) {
+                break;
+            }
+            if (nlAttr.nla_type == attrType) {
+                return StructNlAttr.parse(byteBuffer);
+            }
+            if (byteBuffer.remaining() < nlAttr.getAlignedLength()) {
+                break;
+            }
+            byteBuffer.position(byteBuffer.position() + nlAttr.getAlignedLength());
+        }
+        return null;
+    }
+
+    public static RtNetlinkNeighborMessage parse(StructNlMsgHdr header, ByteBuffer byteBuffer) {
+        final RtNetlinkNeighborMessage neighMsg = new RtNetlinkNeighborMessage(header);
+
+        neighMsg.mNdmsg = StructNdMsg.parse(byteBuffer);
+        if (neighMsg.mNdmsg == null) {
+            return null;
+        }
+
+        // Some of these are message-type dependent, and not always present.
+        final int baseOffset = byteBuffer.position();
+        StructNlAttr nlAttr = findNextAttrOfType(NDA_DST, byteBuffer);
+        if (nlAttr != null) {
+            neighMsg.mDestination = nlAttr.getValueAsInetAddress();
+        }
+
+        byteBuffer.position(baseOffset);
+        nlAttr = findNextAttrOfType(NDA_LLADDR, byteBuffer);
+        if (nlAttr != null) {
+            neighMsg.mLinkLayerAddr = nlAttr.nla_value;
+        }
+
+        byteBuffer.position(baseOffset);
+        nlAttr = findNextAttrOfType(NDA_PROBES, byteBuffer);
+        if (nlAttr != null) {
+            neighMsg.mNumProbes = nlAttr.getValueAsInt(0);
+        }
+
+        byteBuffer.position(baseOffset);
+        nlAttr = findNextAttrOfType(NDA_CACHEINFO, byteBuffer);
+        if (nlAttr != null) {
+            neighMsg.mCacheInfo = StructNdaCacheInfo.parse(nlAttr.getValueAsByteBuffer());
+        }
+
+        final int kMinConsumed = StructNlMsgHdr.STRUCT_SIZE + StructNdMsg.STRUCT_SIZE;
+        final int kAdditionalSpace = NetlinkConstants.alignedLengthOf(
+                neighMsg.mHeader.nlmsg_len - kMinConsumed);
+        if (byteBuffer.remaining() < kAdditionalSpace) {
+            byteBuffer.position(byteBuffer.limit());
+        } else {
+            byteBuffer.position(baseOffset + kAdditionalSpace);
+        }
+
+        return neighMsg;
+    }
+
+    /**
+     * A convenience method to create an RTM_GETNEIGH request message.
+     */
+    public static byte[] newGetNeighborsRequest(int seqNo) {
+        final int length = StructNlMsgHdr.STRUCT_SIZE + StructNdMsg.STRUCT_SIZE;
+        final byte[] bytes = new byte[length];
+        final ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
+        byteBuffer.order(ByteOrder.nativeOrder());
+
+        final StructNlMsgHdr nlmsghdr = new StructNlMsgHdr();
+        nlmsghdr.nlmsg_len = length;
+        nlmsghdr.nlmsg_type = NetlinkConstants.RTM_GETNEIGH;
+        nlmsghdr.nlmsg_flags = NLM_F_REQUEST | NLM_F_DUMP;
+        nlmsghdr.nlmsg_seq = seqNo;
+        nlmsghdr.pack(byteBuffer);
+
+        final StructNdMsg ndmsg = new StructNdMsg();
+        ndmsg.pack(byteBuffer);
+
+        return bytes;
+    }
+
+    /**
+     * A convenience method to create an RTM_NEWNEIGH message, to modify
+     * the kernel's state information for a specific neighbor.
+     */
+    public static byte[] newNewNeighborMessage(
+            int seqNo, InetAddress ip, short nudState, int ifIndex, byte[] llAddr) {
+        final StructNlMsgHdr nlmsghdr = new StructNlMsgHdr();
+        nlmsghdr.nlmsg_type = NetlinkConstants.RTM_NEWNEIGH;
+        nlmsghdr.nlmsg_flags = NLM_F_REQUEST | NLM_F_ACK | NLM_F_REPLACE;
+        nlmsghdr.nlmsg_seq = seqNo;
+
+        final RtNetlinkNeighborMessage msg = new RtNetlinkNeighborMessage(nlmsghdr);
+        msg.mNdmsg = new StructNdMsg();
+        msg.mNdmsg.ndm_family =
+                (byte) ((ip instanceof Inet6Address) ? OsConstants.AF_INET6 : OsConstants.AF_INET);
+        msg.mNdmsg.ndm_ifindex = ifIndex;
+        msg.mNdmsg.ndm_state = nudState;
+        msg.mDestination = ip;
+        msg.mLinkLayerAddr = llAddr;  // might be null
+
+        final byte[] bytes = new byte[msg.getRequiredSpace()];
+        nlmsghdr.nlmsg_len = bytes.length;
+        final ByteBuffer byteBuffer = ByteBuffer.wrap(bytes);
+        byteBuffer.order(ByteOrder.nativeOrder());
+        msg.pack(byteBuffer);
+        return bytes;
+    }
+
+    private StructNdMsg mNdmsg;
+    private InetAddress mDestination;
+    private byte[] mLinkLayerAddr;
+    private int mNumProbes;
+    private StructNdaCacheInfo mCacheInfo;
+
+    private RtNetlinkNeighborMessage(StructNlMsgHdr header) {
+        super(header);
+        mNdmsg = null;
+        mDestination = null;
+        mLinkLayerAddr = null;
+        mNumProbes = 0;
+        mCacheInfo = null;
+    }
+
+    public StructNdMsg getNdHeader() {
+        return mNdmsg;
+    }
+
+    public InetAddress getDestination() {
+        return mDestination;
+    }
+
+    public byte[] getLinkLayerAddress() {
+        return mLinkLayerAddr;
+    }
+
+    public int getProbes() {
+        return mNumProbes;
+    }
+
+    public StructNdaCacheInfo getCacheInfo() {
+        return mCacheInfo;
+    }
+
+    public int getRequiredSpace() {
+        int spaceRequired = StructNlMsgHdr.STRUCT_SIZE + StructNdMsg.STRUCT_SIZE;
+        if (mDestination != null) {
+            spaceRequired += NetlinkConstants.alignedLengthOf(
+                    StructNlAttr.NLA_HEADERLEN + mDestination.getAddress().length);
+        }
+        if (mLinkLayerAddr != null) {
+            spaceRequired += NetlinkConstants.alignedLengthOf(
+                    StructNlAttr.NLA_HEADERLEN + mLinkLayerAddr.length);
+        }
+        // Currently we don't write messages with NDA_PROBES nor NDA_CACHEINFO
+        // attributes appended.  Fix later, if necessary.
+        return spaceRequired;
+    }
+
+    private static void packNlAttr(short nlType, byte[] nlValue, ByteBuffer byteBuffer) {
+        final StructNlAttr nlAttr = new StructNlAttr();
+        nlAttr.nla_type = nlType;
+        nlAttr.nla_value = nlValue;
+        nlAttr.nla_len = (short) (StructNlAttr.NLA_HEADERLEN + nlAttr.nla_value.length);
+        nlAttr.pack(byteBuffer);
+    }
+
+    public void pack(ByteBuffer byteBuffer) {
+        getHeader().pack(byteBuffer) ;
+        mNdmsg.pack(byteBuffer);
+
+        if (mDestination != null) {
+            packNlAttr(NDA_DST, mDestination.getAddress(), byteBuffer);
+        }
+        if (mLinkLayerAddr != null) {
+            packNlAttr(NDA_LLADDR, mLinkLayerAddr, byteBuffer);
+        }
+    }
+
+    @Override
+    public String toString() {
+        final String ipLiteral = (mDestination == null) ? "" : mDestination.getHostAddress();
+        return "RtNetlinkNeighborMessage{ "
+                + "nlmsghdr{" + (mHeader == null ? "" : mHeader.toString()) + "}, "
+                + "ndmsg{" + (mNdmsg == null ? "" : mNdmsg.toString()) + "}, "
+                + "destination{" + ipLiteral + "} "
+                + "linklayeraddr{" + NetlinkConstants.hexify(mLinkLayerAddr) + "} "
+                + "probes{" + mNumProbes + "} "
+                + "cacheinfo{" + (mCacheInfo == null ? "" : mCacheInfo.toString()) + "} "
+                + "}";
+    }
+}
diff --git a/android/net/netlink/StructInetDiagMsg.java b/android/net/netlink/StructInetDiagMsg.java
new file mode 100644
index 0000000..da824ad
--- /dev/null
+++ b/android/net/netlink/StructInetDiagMsg.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2018 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.netlink;
+
+import static java.nio.ByteOrder.BIG_ENDIAN;
+import static android.system.OsConstants.AF_INET;
+import static android.system.OsConstants.AF_INET6;
+
+import java.net.Inet4Address;
+import java.net.InetSocketAddress;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import android.util.Log;
+
+/**
+ * struct inet_diag_msg
+ *
+ * see &lt;linux_src&gt;/include/uapi/linux/inet_diag.h
+ *
+ * struct inet_diag_msg {
+ *      __u8    idiag_family;
+ *      __u8    idiag_state;
+ *      __u8    idiag_timer;
+ *      __u8    idiag_retrans;
+ *      struct  inet_diag_sockid id;
+ *      __u32   idiag_expires;
+ *      __u32   idiag_rqueue;
+ *      __u32   idiag_wqueue;
+ *      __u32   idiag_uid;
+ *      __u32   idiag_inode;
+ * };
+ *
+ * @hide
+ */
+public class StructInetDiagMsg {
+    public static final int STRUCT_SIZE = 4 + StructInetDiagSockId.STRUCT_SIZE + 20;
+    private static final int IDIAG_UID_OFFSET = StructNlMsgHdr.STRUCT_SIZE + 4 +
+            StructInetDiagSockId.STRUCT_SIZE + 12;
+    public int idiag_uid;
+
+    public static StructInetDiagMsg parse(ByteBuffer byteBuffer) {
+        StructInetDiagMsg struct = new StructInetDiagMsg();
+        struct.idiag_uid = byteBuffer.getInt(IDIAG_UID_OFFSET);
+        return struct;
+    }
+
+    @Override
+    public String toString() {
+        return "StructInetDiagMsg{ "
+                + "idiag_uid{" + idiag_uid + "}, "
+                + "}";
+    }
+}
diff --git a/android/net/netlink/StructInetDiagReqV2.java b/android/net/netlink/StructInetDiagReqV2.java
new file mode 100644
index 0000000..49a9325
--- /dev/null
+++ b/android/net/netlink/StructInetDiagReqV2.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2018 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.netlink;
+
+import static java.nio.ByteOrder.BIG_ENDIAN;
+import java.net.InetSocketAddress;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * struct inet_diag_req_v2
+ *
+ * see &lt;linux_src&gt;/include/uapi/linux/inet_diag.h
+ *
+ *      struct inet_diag_req_v2 {
+ *          __u8    sdiag_family;
+ *          __u8    sdiag_protocol;
+ *          __u8    idiag_ext;
+ *          __u8    pad;
+ *          __u32   idiag_states;
+ *          struct  inet_diag_sockid id;
+ *      };
+ *
+ * @hide
+ */
+public class StructInetDiagReqV2 {
+    public static final int STRUCT_SIZE = 8 + StructInetDiagSockId.STRUCT_SIZE;
+
+    private final byte sdiag_family;
+    private final byte sdiag_protocol;
+    private final StructInetDiagSockId id;
+    private final int INET_DIAG_REQ_V2_ALL_STATES = (int) 0xffffffff;
+
+
+    public StructInetDiagReqV2(int protocol, InetSocketAddress local, InetSocketAddress remote,
+                               int family) {
+        sdiag_family = (byte) family;
+        sdiag_protocol = (byte) protocol;
+        id = new StructInetDiagSockId(local, remote);
+    }
+
+    public void pack(ByteBuffer byteBuffer) {
+        // The ByteOrder must have already been set by the caller.
+        byteBuffer.put((byte) sdiag_family);
+        byteBuffer.put((byte) sdiag_protocol);
+        byteBuffer.put((byte) 0);
+        byteBuffer.put((byte) 0);
+        byteBuffer.putInt(INET_DIAG_REQ_V2_ALL_STATES);
+        id.pack(byteBuffer);
+    }
+
+    @Override
+    public String toString() {
+        final String familyStr = NetlinkConstants.stringForAddressFamily(sdiag_family);
+        final String protocolStr = NetlinkConstants.stringForAddressFamily(sdiag_protocol);
+
+        return "StructInetDiagReqV2{ "
+                + "sdiag_family{" + familyStr + "}, "
+                + "sdiag_protocol{" + protocolStr + "}, "
+                + "idiag_ext{" + 0 + ")}, "
+                + "pad{" + 0 + "}, "
+                + "idiag_states{" + Integer.toHexString(INET_DIAG_REQ_V2_ALL_STATES) + "}, "
+                + id.toString()
+                + "}";
+    }
+}
diff --git a/android/net/netlink/StructInetDiagSockId.java b/android/net/netlink/StructInetDiagSockId.java
new file mode 100644
index 0000000..2e9fa25
--- /dev/null
+++ b/android/net/netlink/StructInetDiagSockId.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (C) 2018 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.netlink;
+
+import static java.nio.ByteOrder.BIG_ENDIAN;
+
+import java.net.Inet4Address;
+import java.net.InetSocketAddress;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+/**
+ * struct inet_diag_req_v2
+ *
+ * see &lt;linux_src&gt;/include/uapi/linux/inet_diag.h
+ *
+ * struct inet_diag_sockid {
+ *        __be16    idiag_sport;
+ *        __be16    idiag_dport;
+ *        __be32    idiag_src[4];
+ *        __be32    idiag_dst[4];
+ *        __u32     idiag_if;
+ *        __u32     idiag_cookie[2];
+ * #define INET_DIAG_NOCOOKIE (~0U)
+ * };
+ *
+ * @hide
+ */
+public class StructInetDiagSockId {
+    public static final int STRUCT_SIZE = 48;
+
+    private final InetSocketAddress mLocSocketAddress;
+    private final InetSocketAddress mRemSocketAddress;
+    private final byte[] INET_DIAG_NOCOOKIE = new byte[]{
+            (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff,
+            (byte) 0xff, (byte) 0xff, (byte) 0xff, (byte) 0xff};
+    private final byte[] IPV4_PADDING = new byte[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
+
+    public StructInetDiagSockId(InetSocketAddress loc, InetSocketAddress rem) {
+        mLocSocketAddress = loc;
+        mRemSocketAddress = rem;
+    }
+
+    public void pack(ByteBuffer byteBuffer) {
+        byteBuffer.order(BIG_ENDIAN);
+        byteBuffer.putShort((short) mLocSocketAddress.getPort());
+        byteBuffer.putShort((short) mRemSocketAddress.getPort());
+        byteBuffer.put(mLocSocketAddress.getAddress().getAddress());
+        if (mLocSocketAddress.getAddress() instanceof Inet4Address) {
+            byteBuffer.put(IPV4_PADDING);
+        }
+        byteBuffer.put(mRemSocketAddress.getAddress().getAddress());
+        if (mRemSocketAddress.getAddress() instanceof Inet4Address) {
+            byteBuffer.put(IPV4_PADDING);
+        }
+        byteBuffer.order(ByteOrder.nativeOrder());
+        byteBuffer.putInt(0);
+        byteBuffer.put(INET_DIAG_NOCOOKIE);
+    }
+
+    @Override
+    public String toString() {
+        return "StructInetDiagSockId{ "
+                + "idiag_sport{" + mLocSocketAddress.getPort() + "}, "
+                + "idiag_dport{" + mRemSocketAddress.getPort() + "}, "
+                + "idiag_src{" + mLocSocketAddress.getAddress().getHostAddress() + "}, "
+                + "idiag_dst{" + mRemSocketAddress.getAddress().getHostAddress() + "}, "
+                + "idiag_if{" + 0 + "} "
+                + "idiag_cookie{INET_DIAG_NOCOOKIE}"
+                + "}";
+    }
+}
diff --git a/android/net/netlink/StructNdMsg.java b/android/net/netlink/StructNdMsg.java
new file mode 100644
index 0000000..e34ec39
--- /dev/null
+++ b/android/net/netlink/StructNdMsg.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2015 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.netlink;
+
+import android.net.netlink.NetlinkConstants;
+import android.system.OsConstants;
+import java.nio.ByteBuffer;
+
+
+/**
+ * struct ndmsg
+ *
+ * see: &lt;linux_src&gt;/include/uapi/linux/neighbour.h
+ *
+ * @hide
+ */
+public class StructNdMsg {
+    // Already aligned.
+    public static final int STRUCT_SIZE = 12;
+
+    // Neighbor Cache Entry States
+    public static final short NUD_NONE        = 0x00;
+    public static final short NUD_INCOMPLETE  = 0x01;
+    public static final short NUD_REACHABLE   = 0x02;
+    public static final short NUD_STALE       = 0x04;
+    public static final short NUD_DELAY       = 0x08;
+    public static final short NUD_PROBE       = 0x10;
+    public static final short NUD_FAILED      = 0x20;
+    public static final short NUD_NOARP       = 0x40;
+    public static final short NUD_PERMANENT   = 0x80;
+
+    public static String stringForNudState(short nudState) {
+        switch (nudState) {
+            case NUD_NONE: return "NUD_NONE";
+            case NUD_INCOMPLETE: return "NUD_INCOMPLETE";
+            case NUD_REACHABLE: return "NUD_REACHABLE";
+            case NUD_STALE: return "NUD_STALE";
+            case NUD_DELAY: return "NUD_DELAY";
+            case NUD_PROBE: return "NUD_PROBE";
+            case NUD_FAILED: return "NUD_FAILED";
+            case NUD_NOARP: return "NUD_NOARP";
+            case NUD_PERMANENT: return "NUD_PERMANENT";
+            default:
+                return "unknown NUD state: " + String.valueOf(nudState);
+        }
+    }
+
+    public static boolean isNudStateConnected(short nudState) {
+        return ((nudState & (NUD_PERMANENT|NUD_NOARP|NUD_REACHABLE)) != 0);
+    }
+
+    public static boolean isNudStateValid(short nudState) {
+        return (isNudStateConnected(nudState) ||
+                ((nudState & (NUD_PROBE|NUD_STALE|NUD_DELAY)) != 0));
+    }
+
+    // Neighbor Cache Entry Flags
+    public static byte NTF_USE       = (byte) 0x01;
+    public static byte NTF_SELF      = (byte) 0x02;
+    public static byte NTF_MASTER    = (byte) 0x04;
+    public static byte NTF_PROXY     = (byte) 0x08;
+    public static byte NTF_ROUTER    = (byte) 0x80;
+
+    public static String stringForNudFlags(byte flags) {
+        final StringBuilder sb = new StringBuilder();
+        if ((flags & NTF_USE) != 0) {
+            sb.append("NTF_USE");
+        }
+        if ((flags & NTF_SELF) != 0) {
+            if (sb.length() > 0) { sb.append("|"); }
+            sb.append("NTF_SELF");
+        }
+        if ((flags & NTF_MASTER) != 0) {
+            if (sb.length() > 0) { sb.append("|"); }
+            sb.append("NTF_MASTER");
+        }
+        if ((flags & NTF_PROXY) != 0) {
+            if (sb.length() > 0) { sb.append("|");
+        }
+            sb.append("NTF_PROXY"); }
+        if ((flags & NTF_ROUTER) != 0) {
+            if (sb.length() > 0) { sb.append("|"); }
+            sb.append("NTF_ROUTER");
+        }
+        return sb.toString();
+    }
+
+    private static boolean hasAvailableSpace(ByteBuffer byteBuffer) {
+        return byteBuffer != null && byteBuffer.remaining() >= STRUCT_SIZE;
+    }
+
+    public static StructNdMsg parse(ByteBuffer byteBuffer) {
+        if (!hasAvailableSpace(byteBuffer)) { return null; }
+
+        // The ByteOrder must have already been set by the caller.  In most
+        // cases ByteOrder.nativeOrder() is correct, with the possible
+        // exception of usage within unittests.
+        final StructNdMsg struct = new StructNdMsg();
+        struct.ndm_family = byteBuffer.get();
+        final byte pad1 = byteBuffer.get();
+        final short pad2 = byteBuffer.getShort();
+        struct.ndm_ifindex = byteBuffer.getInt();
+        struct.ndm_state = byteBuffer.getShort();
+        struct.ndm_flags = byteBuffer.get();
+        struct.ndm_type = byteBuffer.get();
+        return struct;
+    }
+
+    public byte ndm_family;
+    public int ndm_ifindex;
+    public short ndm_state;
+    public byte ndm_flags;
+    public byte ndm_type;
+
+    public StructNdMsg() {
+        ndm_family = (byte) OsConstants.AF_UNSPEC;
+    }
+
+    public void pack(ByteBuffer byteBuffer) {
+        // The ByteOrder must have already been set by the caller.  In most
+        // cases ByteOrder.nativeOrder() is correct, with the exception
+        // of usage within unittests.
+        byteBuffer.put(ndm_family);
+        byteBuffer.put((byte) 0);         // pad1
+        byteBuffer.putShort((short) 0);   // pad2
+        byteBuffer.putInt(ndm_ifindex);
+        byteBuffer.putShort(ndm_state);
+        byteBuffer.put(ndm_flags);
+        byteBuffer.put(ndm_type);
+    }
+
+    public boolean nudConnected() {
+        return isNudStateConnected(ndm_state);
+    }
+
+    public boolean nudValid() {
+        return isNudStateValid(ndm_state);
+    }
+
+    @Override
+    public String toString() {
+        final String stateStr = "" + ndm_state + " (" + stringForNudState(ndm_state) + ")";
+        final String flagsStr = "" + ndm_flags + " (" + stringForNudFlags(ndm_flags) + ")";
+        return "StructNdMsg{ "
+                + "family{" + NetlinkConstants.stringForAddressFamily((int) ndm_family) + "}, "
+                + "ifindex{" + ndm_ifindex + "}, "
+                + "state{" + stateStr + "}, "
+                + "flags{" + flagsStr + "}, "
+                + "type{" + ndm_type + "} "
+                + "}";
+    }
+}
diff --git a/android/net/netlink/StructNdaCacheInfo.java b/android/net/netlink/StructNdaCacheInfo.java
new file mode 100644
index 0000000..16cf563
--- /dev/null
+++ b/android/net/netlink/StructNdaCacheInfo.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2015 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.netlink;
+
+import android.system.Os;
+import android.system.OsConstants;
+
+import java.nio.ByteBuffer;
+
+
+/**
+ * struct nda_cacheinfo
+ *
+ * see: &lt;linux_src&gt;/include/uapi/linux/neighbour.h
+ *
+ * @hide
+ */
+public class StructNdaCacheInfo {
+    // Already aligned.
+    public static final int STRUCT_SIZE = 16;
+
+    private static boolean hasAvailableSpace(ByteBuffer byteBuffer) {
+        return byteBuffer != null && byteBuffer.remaining() >= STRUCT_SIZE;
+    }
+
+    public static StructNdaCacheInfo parse(ByteBuffer byteBuffer) {
+        if (!hasAvailableSpace(byteBuffer)) { return null; }
+
+        // The ByteOrder must have already been set by the caller.  In most
+        // cases ByteOrder.nativeOrder() is correct, with the possible
+        // exception of usage within unittests.
+        final StructNdaCacheInfo struct = new StructNdaCacheInfo();
+        struct.ndm_used = byteBuffer.getInt();
+        struct.ndm_confirmed = byteBuffer.getInt();
+        struct.ndm_updated = byteBuffer.getInt();
+        struct.ndm_refcnt = byteBuffer.getInt();
+        return struct;
+    }
+
+    // TODO: investigate whether this can change during device runtime and
+    // decide what (if anything) should be done about that.
+    private static final long CLOCK_TICKS_PER_SECOND = Os.sysconf(OsConstants._SC_CLK_TCK);
+
+    private static long ticksToMilliSeconds(int intClockTicks) {
+        final long longClockTicks = (long) intClockTicks & 0xffffffff;
+        return (longClockTicks * 1000) / CLOCK_TICKS_PER_SECOND;
+    }
+
+    /**
+     * Explanatory notes, for reference.
+     *
+     * Before being returned to user space, the neighbor entry times are
+     * converted to clock_t's like so:
+     *
+     *     ndm_used      = jiffies_to_clock_t(now - neigh->used);
+     *     ndm_confirmed = jiffies_to_clock_t(now - neigh->confirmed);
+     *     ndm_updated   = jiffies_to_clock_t(now - neigh->updated);
+     *
+     * meaning that these values are expressed as "clock ticks ago".  To
+     * convert these clock ticks to seconds divide by sysconf(_SC_CLK_TCK).
+     * When _SC_CLK_TCK is 100, for example, the ndm_* times are expressed
+     * in centiseconds.
+     *
+     * These values are unsigned, but fortunately being expressed as "some
+     * clock ticks ago", these values are typically very small (and 
+     * 2^31 centiseconds = 248 days).
+     *
+     * By observation, it appears that:
+     *     ndm_used: the last time ARP/ND took place for this neighbor
+     *     ndm_confirmed: the last time ARP/ND succeeded for this neighbor OR
+     *                    higher layer confirmation (TCP or MSG_CONFIRM)
+     *                    was received
+     *     ndm_updated: the time when the current NUD state was entered
+     */
+    public int ndm_used;
+    public int ndm_confirmed;
+    public int ndm_updated;
+    public int ndm_refcnt;
+
+    public StructNdaCacheInfo() {}
+
+    public long lastUsed() {
+        return ticksToMilliSeconds(ndm_used);
+    }
+
+    public long lastConfirmed() {
+        return ticksToMilliSeconds(ndm_confirmed);
+    }
+
+    public long lastUpdated() {
+        return ticksToMilliSeconds(ndm_updated);
+    }
+
+    @Override
+    public String toString() {
+        return "NdaCacheInfo{ "
+                + "ndm_used{" + lastUsed() + "}, "
+                + "ndm_confirmed{" + lastConfirmed() + "}, "
+                + "ndm_updated{" + lastUpdated() + "}, "
+                + "ndm_refcnt{" + ndm_refcnt + "} "
+                + "}";
+    }
+}
diff --git a/android/net/netlink/StructNfGenMsg.java b/android/net/netlink/StructNfGenMsg.java
new file mode 100644
index 0000000..8155977
--- /dev/null
+++ b/android/net/netlink/StructNfGenMsg.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.netlink;
+
+import java.nio.ByteBuffer;
+
+
+/**
+ * struct nfgenmsg
+ *
+ * see &lt;linux_src&gt;/include/uapi/linux/netfilter/nfnetlink.h
+ *
+ * @hide
+ */
+public class StructNfGenMsg {
+    public static final int STRUCT_SIZE = 2 + Short.BYTES;
+
+    public static final int NFNETLINK_V0 = 0;
+
+    final public byte nfgen_family;
+    final public byte version;
+    final public short res_id;  // N.B.: this is big endian in the kernel
+
+    public StructNfGenMsg(byte family) {
+        nfgen_family = family;
+        version = (byte) NFNETLINK_V0;
+        res_id = (short) 0;
+    }
+
+    public void pack(ByteBuffer byteBuffer) {
+        byteBuffer.put(nfgen_family);
+        byteBuffer.put(version);
+        byteBuffer.putShort(res_id);
+    }
+}
diff --git a/android/net/netlink/StructNlAttr.java b/android/net/netlink/StructNlAttr.java
new file mode 100644
index 0000000..28a4e88
--- /dev/null
+++ b/android/net/netlink/StructNlAttr.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2015 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.netlink;
+
+import android.net.netlink.NetlinkConstants;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.ByteOrder;
+import java.nio.ByteBuffer;
+
+
+/**
+ * struct nlattr
+ *
+ * see: &lt;linux_src&gt;/include/uapi/linux/netlink.h
+ *
+ * @hide
+ */
+public class StructNlAttr {
+    // Already aligned.
+    public static final int NLA_HEADERLEN  = 4;
+    public static final int NLA_F_NESTED   = (1 << 15);
+
+    public static short makeNestedType(short type) {
+        return (short) (type | NLA_F_NESTED);
+    }
+
+    // Return a (length, type) object only, without consuming any bytes in
+    // |byteBuffer| and without copying or interpreting any value bytes.
+    // This is used for scanning over a packed set of struct nlattr's,
+    // looking for instances of a particular type.
+    public static StructNlAttr peek(ByteBuffer byteBuffer) {
+        if (byteBuffer == null || byteBuffer.remaining() < NLA_HEADERLEN) {
+            return null;
+        }
+        final int baseOffset = byteBuffer.position();
+
+        // Assume the byte order of the buffer is the expected byte order of the value.
+        final StructNlAttr struct = new StructNlAttr(byteBuffer.order());
+        // The byte order of nla_len and nla_type is always native.
+        final ByteOrder originalOrder = byteBuffer.order();
+        byteBuffer.order(ByteOrder.nativeOrder());
+        try {
+            struct.nla_len = byteBuffer.getShort();
+            struct.nla_type = byteBuffer.getShort();
+        } finally {
+            byteBuffer.order(originalOrder);
+        }
+
+        byteBuffer.position(baseOffset);
+        if (struct.nla_len < NLA_HEADERLEN) {
+            // Malformed.
+            return null;
+        }
+        return struct;
+    }
+
+    public static StructNlAttr parse(ByteBuffer byteBuffer) {
+        final StructNlAttr struct = peek(byteBuffer);
+        if (struct == null || byteBuffer.remaining() < struct.getAlignedLength()) {
+            return null;
+        }
+
+        final int baseOffset = byteBuffer.position();
+        byteBuffer.position(baseOffset + NLA_HEADERLEN);
+
+        int valueLen = ((int) struct.nla_len) & 0xffff;
+        valueLen -= NLA_HEADERLEN;
+        if (valueLen > 0) {
+            struct.nla_value = new byte[valueLen];
+            byteBuffer.get(struct.nla_value, 0, valueLen);
+            byteBuffer.position(baseOffset + struct.getAlignedLength());
+        }
+        return struct;
+    }
+
+    public short nla_len = (short) NLA_HEADERLEN;
+    public short nla_type;
+    public byte[] nla_value;
+
+    // The byte order used to read/write the value member. Netlink length and
+    // type members are always read/written in native order.
+    private ByteOrder mByteOrder = ByteOrder.nativeOrder();
+
+    public StructNlAttr() {}
+
+    public StructNlAttr(ByteOrder byteOrder) {
+        mByteOrder = byteOrder;
+    }
+
+    public StructNlAttr(short type, byte value) {
+        nla_type = type;
+        setValue(new byte[1]);
+        nla_value[0] = value;
+    }
+
+    public StructNlAttr(short type, short value) {
+        this(type, value, ByteOrder.nativeOrder());
+    }
+
+    public StructNlAttr(short type, short value, ByteOrder order) {
+        this(order);
+        nla_type = type;
+        setValue(new byte[Short.BYTES]);
+        getValueAsByteBuffer().putShort(value);
+    }
+
+    public StructNlAttr(short type, int value) {
+        this(type, value, ByteOrder.nativeOrder());
+    }
+
+    public StructNlAttr(short type, int value, ByteOrder order) {
+        this(order);
+        nla_type = type;
+        setValue(new byte[Integer.BYTES]);
+        getValueAsByteBuffer().putInt(value);
+    }
+
+    public StructNlAttr(short type, InetAddress ip) {
+        nla_type = type;
+        setValue(ip.getAddress());
+    }
+
+    public StructNlAttr(short type, StructNlAttr... nested) {
+        this();
+        nla_type = makeNestedType(type);
+
+        int payloadLength = 0;
+        for (StructNlAttr nla : nested) payloadLength += nla.getAlignedLength();
+        setValue(new byte[payloadLength]);
+
+        final ByteBuffer buf = getValueAsByteBuffer();
+        for (StructNlAttr nla : nested) {
+            nla.pack(buf);
+        }
+    }
+
+    public int getAlignedLength() {
+        return NetlinkConstants.alignedLengthOf(nla_len);
+    }
+
+    public ByteBuffer getValueAsByteBuffer() {
+        if (nla_value == null) { return null; }
+        final ByteBuffer byteBuffer = ByteBuffer.wrap(nla_value);
+        byteBuffer.order(mByteOrder);
+        return byteBuffer;
+    }
+
+    public int getValueAsInt(int defaultValue) {
+        final ByteBuffer byteBuffer = getValueAsByteBuffer();
+        if (byteBuffer == null || byteBuffer.remaining() != Integer.BYTES) {
+            return defaultValue;
+        }
+        return getValueAsByteBuffer().getInt();
+    }
+
+    public InetAddress getValueAsInetAddress() {
+        if (nla_value == null) { return null; }
+
+        try {
+            return InetAddress.getByAddress(nla_value);
+        } catch (UnknownHostException ignored) {
+            return null;
+        }
+    }
+
+    public void pack(ByteBuffer byteBuffer) {
+        final ByteOrder originalOrder = byteBuffer.order();
+        final int originalPosition = byteBuffer.position();
+
+        byteBuffer.order(ByteOrder.nativeOrder());
+        try {
+            byteBuffer.putShort(nla_len);
+            byteBuffer.putShort(nla_type);
+            if (nla_value != null) byteBuffer.put(nla_value);
+        } finally {
+            byteBuffer.order(originalOrder);
+        }
+        byteBuffer.position(originalPosition + getAlignedLength());
+    }
+
+    private void setValue(byte[] value) {
+        nla_value = value;
+        nla_len = (short) (NLA_HEADERLEN + ((nla_value != null) ? nla_value.length : 0));
+    }
+
+    @Override
+    public String toString() {
+        return "StructNlAttr{ "
+                + "nla_len{" + nla_len + "}, "
+                + "nla_type{" + nla_type + "}, "
+                + "nla_value{" + NetlinkConstants.hexify(nla_value) + "}, "
+                + "}";
+    }
+}
diff --git a/android/net/netlink/StructNlMsgErr.java b/android/net/netlink/StructNlMsgErr.java
new file mode 100644
index 0000000..6fcc6e6
--- /dev/null
+++ b/android/net/netlink/StructNlMsgErr.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2015 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.netlink;
+
+import android.net.netlink.NetlinkConstants;
+import android.net.netlink.StructNlMsgHdr;
+
+import java.nio.ByteBuffer;
+
+
+/**
+ * struct nlmsgerr
+ *
+ * see &lt;linux_src&gt;/include/uapi/linux/netlink.h
+ *
+ * @hide
+ */
+public class StructNlMsgErr {
+    public static final int STRUCT_SIZE = Integer.BYTES + StructNlMsgHdr.STRUCT_SIZE;
+
+    public static boolean hasAvailableSpace(ByteBuffer byteBuffer) {
+        return byteBuffer != null && byteBuffer.remaining() >= STRUCT_SIZE;
+    }
+
+    public static StructNlMsgErr parse(ByteBuffer byteBuffer) {
+        if (!hasAvailableSpace(byteBuffer)) { return null; }
+
+        // The ByteOrder must have already been set by the caller.  In most
+        // cases ByteOrder.nativeOrder() is correct, with the exception
+        // of usage within unittests.
+        final StructNlMsgErr struct = new StructNlMsgErr();
+        struct.error = byteBuffer.getInt();
+        struct.msg = StructNlMsgHdr.parse(byteBuffer);
+        return struct;
+    }
+
+    public int error;
+    public StructNlMsgHdr msg;
+
+    public void pack(ByteBuffer byteBuffer) {
+        // The ByteOrder must have already been set by the caller.  In most
+        // cases ByteOrder.nativeOrder() is correct, with the possible
+        // exception of usage within unittests.
+        byteBuffer.putInt(error);
+        if (msg != null) {
+            msg.pack(byteBuffer);
+        }
+    }
+
+    @Override
+    public String toString() {
+        return "StructNlMsgErr{ "
+                + "error{" + error + "}, "
+                + "msg{" + (msg == null ? "" : msg.toString()) + "} "
+                + "}";
+    }
+}
diff --git a/android/net/netlink/StructNlMsgHdr.java b/android/net/netlink/StructNlMsgHdr.java
new file mode 100644
index 0000000..98ab5e7
--- /dev/null
+++ b/android/net/netlink/StructNlMsgHdr.java
@@ -0,0 +1,140 @@
+/*
+ * Copyright (C) 2015 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.netlink;
+
+import android.net.netlink.NetlinkConstants;
+import java.nio.ByteBuffer;
+
+
+/**
+ * struct nlmsghdr
+ *
+ * see &lt;linux_src&gt;/include/uapi/linux/netlink.h
+ *
+ * @hide
+ */
+public class StructNlMsgHdr {
+    // Already aligned.
+    public static final int STRUCT_SIZE = 16;
+
+    public static final short NLM_F_REQUEST = 0x0001;
+    public static final short NLM_F_MULTI   = 0x0002;
+    public static final short NLM_F_ACK     = 0x0004;
+    public static final short NLM_F_ECHO    = 0x0008;
+    // Flags for a GET request.
+    public static final short NLM_F_ROOT    = 0x0100;
+    public static final short NLM_F_MATCH   = 0x0200;
+    public static final short NLM_F_DUMP    = NLM_F_ROOT|NLM_F_MATCH;
+    // Flags for a NEW request.
+    public static final short NLM_F_REPLACE   = 0x100;
+    public static final short NLM_F_EXCL      = 0x200;
+    public static final short NLM_F_CREATE    = 0x400;
+    public static final short NLM_F_APPEND    = 0x800;
+
+
+    public static String stringForNlMsgFlags(short flags) {
+        final StringBuilder sb = new StringBuilder();
+        if ((flags & NLM_F_REQUEST) != 0) {
+            sb.append("NLM_F_REQUEST");
+        }
+        if ((flags & NLM_F_MULTI) != 0) {
+            if (sb.length() > 0) { sb.append("|"); }
+            sb.append("NLM_F_MULTI");
+        }
+        if ((flags & NLM_F_ACK) != 0) {
+            if (sb.length() > 0) { sb.append("|"); }
+            sb.append("NLM_F_ACK");
+        }
+        if ((flags & NLM_F_ECHO) != 0) {
+            if (sb.length() > 0) { sb.append("|"); }
+            sb.append("NLM_F_ECHO");
+        }
+        if ((flags & NLM_F_ROOT) != 0) {
+            if (sb.length() > 0) { sb.append("|"); }
+            sb.append("NLM_F_ROOT");
+        }
+        if ((flags & NLM_F_MATCH) != 0) {
+            if (sb.length() > 0) { sb.append("|"); }
+            sb.append("NLM_F_MATCH");
+        }
+        return sb.toString();
+    }
+
+    public static boolean hasAvailableSpace(ByteBuffer byteBuffer) {
+        return byteBuffer != null && byteBuffer.remaining() >= STRUCT_SIZE;
+    }
+
+    public static StructNlMsgHdr parse(ByteBuffer byteBuffer) {
+        if (!hasAvailableSpace(byteBuffer)) { return null; }
+
+        // The ByteOrder must have already been set by the caller.  In most
+        // cases ByteOrder.nativeOrder() is correct, with the exception
+        // of usage within unittests.
+        final StructNlMsgHdr struct = new StructNlMsgHdr();
+        struct.nlmsg_len = byteBuffer.getInt();
+        struct.nlmsg_type = byteBuffer.getShort();
+        struct.nlmsg_flags = byteBuffer.getShort();
+        struct.nlmsg_seq = byteBuffer.getInt();
+        struct.nlmsg_pid = byteBuffer.getInt();
+
+        if (struct.nlmsg_len < STRUCT_SIZE) {
+            // Malformed.
+            return null;
+        }
+        return struct;
+    }
+
+    public int nlmsg_len;
+    public short nlmsg_type;
+    public short nlmsg_flags;
+    public int nlmsg_seq;
+    public int nlmsg_pid;
+
+    public StructNlMsgHdr() {
+        nlmsg_len = 0;
+        nlmsg_type = 0;
+        nlmsg_flags = 0;
+        nlmsg_seq = 0;
+        nlmsg_pid = 0;
+    }
+
+    public void pack(ByteBuffer byteBuffer) {
+        // The ByteOrder must have already been set by the caller.  In most
+        // cases ByteOrder.nativeOrder() is correct, with the possible
+        // exception of usage within unittests.
+        byteBuffer.putInt(nlmsg_len);
+        byteBuffer.putShort(nlmsg_type);
+        byteBuffer.putShort(nlmsg_flags);
+        byteBuffer.putInt(nlmsg_seq);
+        byteBuffer.putInt(nlmsg_pid);
+    }
+
+    @Override
+    public String toString() {
+        final String typeStr = "" + nlmsg_type
+                + "(" + NetlinkConstants.stringForNlMsgType(nlmsg_type) + ")";
+        final String flagsStr = "" + nlmsg_flags
+                + "(" + stringForNlMsgFlags(nlmsg_flags) + ")";
+        return "StructNlMsgHdr{ "
+                + "nlmsg_len{" + nlmsg_len + "}, "
+                + "nlmsg_type{" + typeStr + "}, "
+                + "nlmsg_flags{" + flagsStr + ")}, "
+                + "nlmsg_seq{" + nlmsg_seq + "}, "
+                + "nlmsg_pid{" + nlmsg_pid + "} "
+                + "}";
+    }
+}
diff --git a/android/net/nsd/DnsSdTxtRecord.java b/android/net/nsd/DnsSdTxtRecord.java
new file mode 100644
index 0000000..e4a91c5
--- /dev/null
+++ b/android/net/nsd/DnsSdTxtRecord.java
@@ -0,0 +1,325 @@
+/* -*- Mode: Java; tab-width: 4 -*-
+ *
+ * Copyright (c) 2004 Apple Computer, Inc. All rights reserved.
+ *
+ * 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.
+
+ To do:
+ - implement remove()
+ - fix set() to replace existing values
+ */
+
+package android.net.nsd;
+
+import android.os.Parcelable;
+import android.os.Parcel;
+
+import java.util.Arrays;
+
+/**
+ * This class handles TXT record data for DNS based service discovery as specified at
+ * http://tools.ietf.org/html/draft-cheshire-dnsext-dns-sd-11
+ *
+ * DNS-SD specifies that a TXT record corresponding to an SRV record consist of
+ * a packed array of bytes, each preceded by a length byte. Each string
+ * is an attribute-value pair.
+ *
+ * The DnsSdTxtRecord object stores the entire TXT data as a single byte array, traversing it
+ * as need be to implement its various methods.
+ * @hide
+ *
+ */
+public class DnsSdTxtRecord implements Parcelable {
+    private static final byte mSeperator = '=';
+
+    private byte[] mData;
+
+    /** Constructs a new, empty TXT record. */
+    public DnsSdTxtRecord()  {
+        mData = new byte[0];
+    }
+
+    /** Constructs a new TXT record from a byte array in the standard format. */
+    public DnsSdTxtRecord(byte[] data) {
+        mData = (byte[]) data.clone();
+    }
+
+    /** Copy constructor */
+    public DnsSdTxtRecord(DnsSdTxtRecord src) {
+        if (src != null && src.mData != null) {
+            mData = (byte[]) src.mData.clone();
+        }
+    }
+
+    /**
+     * Set a key/value pair. Setting an existing key will replace its value.
+     * @param key Must be ascii with no '='
+     * @param value matching value to key
+     */
+    public void set(String key, String value) {
+        byte[] keyBytes;
+        byte[] valBytes;
+        int valLen;
+
+        if (value != null) {
+            valBytes = value.getBytes();
+            valLen = valBytes.length;
+        } else {
+            valBytes = null;
+            valLen = 0;
+        }
+
+        try {
+            keyBytes = key.getBytes("US-ASCII");
+        }
+        catch (java.io.UnsupportedEncodingException e) {
+            throw new IllegalArgumentException("key should be US-ASCII");
+        }
+
+        for (int i = 0; i < keyBytes.length; i++) {
+            if (keyBytes[i] == '=') {
+                throw new IllegalArgumentException("= is not a valid character in key");
+            }
+        }
+
+        if (keyBytes.length + valLen >= 255) {
+            throw new IllegalArgumentException("Key and Value length cannot exceed 255 bytes");
+        }
+
+        int currentLoc = remove(key);
+        if (currentLoc == -1)
+            currentLoc = keyCount();
+
+        insert(keyBytes, valBytes, currentLoc);
+    }
+
+    /**
+     * Get a value for a key
+     *
+     * @param key
+     * @return The value associated with the key
+     */
+    public String get(String key) {
+        byte[] val = this.getValue(key);
+        return val != null ? new String(val) : null;
+    }
+
+    /** Remove a key/value pair. If found, returns the index or -1 if not found */
+    public int remove(String key) {
+        int avStart = 0;
+
+        for (int i=0; avStart < mData.length; i++) {
+            int avLen = mData[avStart];
+            if (key.length() <= avLen &&
+                    (key.length() == avLen || mData[avStart + key.length() + 1] == mSeperator)) {
+                String s = new String(mData, avStart + 1, key.length());
+                if (0 == key.compareToIgnoreCase(s)) {
+                    byte[] oldBytes = mData;
+                    mData = new byte[oldBytes.length - avLen - 1];
+                    System.arraycopy(oldBytes, 0, mData, 0, avStart);
+                    System.arraycopy(oldBytes, avStart + avLen + 1, mData, avStart,
+                            oldBytes.length - avStart - avLen - 1);
+                    return i;
+                }
+            }
+            avStart += (0xFF & (avLen + 1));
+        }
+        return -1;
+    }
+
+    /** Return the count of keys */
+    public int keyCount() {
+        int count = 0, nextKey;
+        for (nextKey = 0; nextKey < mData.length; count++) {
+            nextKey += (0xFF & (mData[nextKey] + 1));
+        }
+        return count;
+    }
+
+    /** Return true if key is present, false if not. */
+    public boolean contains(String key) {
+        String s = null;
+        for (int i = 0; null != (s = this.getKey(i)); i++) {
+            if (0 == key.compareToIgnoreCase(s)) return true;
+        }
+        return false;
+    }
+
+    /* Gets the size in bytes */
+    public int size() {
+        return mData.length;
+    }
+
+    /* Gets the raw data in bytes */
+    public byte[] getRawData() {
+        return (byte[]) mData.clone();
+    }
+
+    private void insert(byte[] keyBytes, byte[] value, int index) {
+        byte[] oldBytes = mData;
+        int valLen = (value != null) ? value.length : 0;
+        int insertion = 0;
+        int newLen, avLen;
+
+        for (int i = 0; i < index && insertion < mData.length; i++) {
+            insertion += (0xFF & (mData[insertion] + 1));
+        }
+
+        avLen = keyBytes.length + valLen + (value != null ? 1 : 0);
+        newLen = avLen + oldBytes.length + 1;
+
+        mData = new byte[newLen];
+        System.arraycopy(oldBytes, 0, mData, 0, insertion);
+        int secondHalfLen = oldBytes.length - insertion;
+        System.arraycopy(oldBytes, insertion, mData, newLen - secondHalfLen, secondHalfLen);
+        mData[insertion] = (byte) avLen;
+        System.arraycopy(keyBytes, 0, mData, insertion + 1, keyBytes.length);
+        if (value != null) {
+            mData[insertion + 1 + keyBytes.length] = mSeperator;
+            System.arraycopy(value, 0, mData, insertion + keyBytes.length + 2, valLen);
+        }
+    }
+
+    /** Return a key in the TXT record by zero-based index. Returns null if index exceeds the total number of keys. */
+    private String getKey(int index) {
+        int avStart = 0;
+
+        for (int i=0; i < index && avStart < mData.length; i++) {
+            avStart += mData[avStart] + 1;
+        }
+
+        if (avStart < mData.length) {
+            int avLen = mData[avStart];
+            int aLen = 0;
+
+            for (aLen=0; aLen < avLen; aLen++) {
+                if (mData[avStart + aLen + 1] == mSeperator) break;
+            }
+            return new String(mData, avStart + 1, aLen);
+        }
+        return null;
+    }
+
+    /**
+     * Look up a key in the TXT record by zero-based index and return its value.
+     * Returns null if index exceeds the total number of keys.
+     * Returns null if the key is present with no value.
+     */
+    private byte[] getValue(int index) {
+        int avStart = 0;
+        byte[] value = null;
+
+        for (int i=0; i < index && avStart < mData.length; i++) {
+            avStart += mData[avStart] + 1;
+        }
+
+        if (avStart < mData.length) {
+            int avLen = mData[avStart];
+            int aLen = 0;
+
+            for (aLen=0; aLen < avLen; aLen++) {
+                if (mData[avStart + aLen + 1] == mSeperator) {
+                    value = new byte[avLen - aLen - 1];
+                    System.arraycopy(mData, avStart + aLen + 2, value, 0, avLen - aLen - 1);
+                    break;
+                }
+            }
+        }
+        return value;
+    }
+
+    private String getValueAsString(int index) {
+        byte[] value = this.getValue(index);
+        return value != null ? new String(value) : null;
+    }
+
+    private byte[] getValue(String forKey) {
+        String s = null;
+        int i;
+
+        for (i = 0; null != (s = this.getKey(i)); i++) {
+            if (0 == forKey.compareToIgnoreCase(s)) {
+                return this.getValue(i);
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Return a string representation.
+     * Example : {key1=value1},{key2=value2}..
+     *
+     * For a key say like "key3" with null value
+     * {key1=value1},{key2=value2}{key3}
+     */
+    public String toString() {
+        String a, result = null;
+
+        for (int i = 0; null != (a = this.getKey(i)); i++) {
+            String av =  "{" + a;
+            String val = this.getValueAsString(i);
+            if (val != null)
+                av += "=" + val + "}";
+            else
+                av += "}";
+            if (result == null)
+                result = av;
+            else
+                result = result + ", " + av;
+        }
+        return result != null ? result : "";
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o == this) {
+            return true;
+        }
+        if (!(o instanceof DnsSdTxtRecord)) {
+            return false;
+        }
+
+        DnsSdTxtRecord record = (DnsSdTxtRecord)o;
+        return  Arrays.equals(record.mData, mData);
+    }
+
+    @Override
+    public int hashCode() {
+        return Arrays.hashCode(mData);
+    }
+
+    /** Implement the Parcelable interface */
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Implement the Parcelable interface */
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeByteArray(mData);
+    }
+
+    /** Implement the Parcelable interface */
+    public static final @android.annotation.NonNull Creator<DnsSdTxtRecord> CREATOR =
+        new Creator<DnsSdTxtRecord>() {
+            public DnsSdTxtRecord createFromParcel(Parcel in) {
+                DnsSdTxtRecord info = new DnsSdTxtRecord();
+                in.readByteArray(info.mData);
+                return info;
+            }
+
+            public DnsSdTxtRecord[] newArray(int size) {
+                return new DnsSdTxtRecord[size];
+            }
+        };
+}
diff --git a/android/net/nsd/NsdManager.java b/android/net/nsd/NsdManager.java
new file mode 100644
index 0000000..535bf67
--- /dev/null
+++ b/android/net/nsd/NsdManager.java
@@ -0,0 +1,656 @@
+/*
+ * Copyright (C) 2012 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.nsd;
+
+import static com.android.internal.util.Preconditions.checkArgument;
+import static com.android.internal.util.Preconditions.checkNotNull;
+import static com.android.internal.util.Preconditions.checkStringNotEmpty;
+
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.SystemService;
+import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.AsyncChannel;
+import com.android.internal.util.Protocol;
+
+import java.util.concurrent.CountDownLatch;
+
+/**
+ * The Network Service Discovery Manager class provides the API to discover services
+ * on a network. As an example, if device A and device B are connected over a Wi-Fi
+ * network, a game registered on device A can be discovered by a game on device
+ * B. Another example use case is an application discovering printers on the network.
+ *
+ * <p> The API currently supports DNS based service discovery and discovery is currently
+ * limited to a local network over Multicast DNS. DNS service discovery is described at
+ * http://files.dns-sd.org/draft-cheshire-dnsext-dns-sd.txt
+ *
+ * <p> The API is asynchronous and responses to requests from an application are on listener
+ * callbacks on a seperate internal thread.
+ *
+ * <p> There are three main operations the API supports - registration, discovery and resolution.
+ * <pre>
+ *                          Application start
+ *                                 |
+ *                                 |
+ *                                 |                  onServiceRegistered()
+ *                     Register any local services  /
+ *                      to be advertised with       \
+ *                       registerService()            onRegistrationFailed()
+ *                                 |
+ *                                 |
+ *                          discoverServices()
+ *                                 |
+ *                      Maintain a list to track
+ *                        discovered services
+ *                                 |
+ *                                 |--------->
+ *                                 |          |
+ *                                 |      onServiceFound()
+ *                                 |          |
+ *                                 |     add service to list
+ *                                 |          |
+ *                                 |<----------
+ *                                 |
+ *                                 |--------->
+ *                                 |          |
+ *                                 |      onServiceLost()
+ *                                 |          |
+ *                                 |   remove service from list
+ *                                 |          |
+ *                                 |<----------
+ *                                 |
+ *                                 |
+ *                                 | Connect to a service
+ *                                 | from list ?
+ *                                 |
+ *                          resolveService()
+ *                                 |
+ *                         onServiceResolved()
+ *                                 |
+ *                     Establish connection to service
+ *                     with the host and port information
+ *
+ * </pre>
+ * An application that needs to advertise itself over a network for other applications to
+ * discover it can do so with a call to {@link #registerService}. If Example is a http based
+ * application that can provide HTML data to peer services, it can register a name "Example"
+ * with service type "_http._tcp". A successful registration is notified with a callback to
+ * {@link RegistrationListener#onServiceRegistered} and a failure to register is notified
+ * over {@link RegistrationListener#onRegistrationFailed}
+ *
+ * <p> A peer application looking for http services can initiate a discovery for "_http._tcp"
+ * with a call to {@link #discoverServices}. A service found is notified with a callback
+ * to {@link DiscoveryListener#onServiceFound} and a service lost is notified on
+ * {@link DiscoveryListener#onServiceLost}.
+ *
+ * <p> Once the peer application discovers the "Example" http service, and either needs to read the
+ * attributes of the service or wants to receive data from the "Example" application, it can
+ * initiate a resolve with {@link #resolveService} to resolve the attributes, host, and port
+ * details. A successful resolve is notified on {@link ResolveListener#onServiceResolved} and a
+ * failure is notified on {@link ResolveListener#onResolveFailed}.
+ *
+ * Applications can reserve for a service type at
+ * http://www.iana.org/form/ports-service. Existing services can be found at
+ * http://www.iana.org/assignments/service-names-port-numbers/service-names-port-numbers.xml
+ *
+ * {@see NsdServiceInfo}
+ */
+@SystemService(Context.NSD_SERVICE)
+public final class NsdManager {
+    private static final String TAG = NsdManager.class.getSimpleName();
+    private static final boolean DBG = false;
+
+    /**
+     * Broadcast intent action to indicate whether network service discovery is
+     * enabled or disabled. An extra {@link #EXTRA_NSD_STATE} provides the state
+     * information as int.
+     *
+     * @see #EXTRA_NSD_STATE
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String ACTION_NSD_STATE_CHANGED = "android.net.nsd.STATE_CHANGED";
+
+    /**
+     * The lookup key for an int that indicates whether network service discovery is enabled
+     * or disabled. Retrieve it with {@link android.content.Intent#getIntExtra(String,int)}.
+     *
+     * @see #NSD_STATE_DISABLED
+     * @see #NSD_STATE_ENABLED
+     */
+    public static final String EXTRA_NSD_STATE = "nsd_state";
+
+    /**
+     * Network service discovery is disabled
+     *
+     * @see #ACTION_NSD_STATE_CHANGED
+     */
+    public static final int NSD_STATE_DISABLED = 1;
+
+    /**
+     * Network service discovery is enabled
+     *
+     * @see #ACTION_NSD_STATE_CHANGED
+     */
+    public static final int NSD_STATE_ENABLED = 2;
+
+    private static final int BASE = Protocol.BASE_NSD_MANAGER;
+
+    /** @hide */
+    public static final int DISCOVER_SERVICES                       = BASE + 1;
+    /** @hide */
+    public static final int DISCOVER_SERVICES_STARTED               = BASE + 2;
+    /** @hide */
+    public static final int DISCOVER_SERVICES_FAILED                = BASE + 3;
+    /** @hide */
+    public static final int SERVICE_FOUND                           = BASE + 4;
+    /** @hide */
+    public static final int SERVICE_LOST                            = BASE + 5;
+
+    /** @hide */
+    public static final int STOP_DISCOVERY                          = BASE + 6;
+    /** @hide */
+    public static final int STOP_DISCOVERY_FAILED                   = BASE + 7;
+    /** @hide */
+    public static final int STOP_DISCOVERY_SUCCEEDED                = BASE + 8;
+
+    /** @hide */
+    public static final int REGISTER_SERVICE                        = BASE + 9;
+    /** @hide */
+    public static final int REGISTER_SERVICE_FAILED                 = BASE + 10;
+    /** @hide */
+    public static final int REGISTER_SERVICE_SUCCEEDED              = BASE + 11;
+
+    /** @hide */
+    public static final int UNREGISTER_SERVICE                      = BASE + 12;
+    /** @hide */
+    public static final int UNREGISTER_SERVICE_FAILED               = BASE + 13;
+    /** @hide */
+    public static final int UNREGISTER_SERVICE_SUCCEEDED            = BASE + 14;
+
+    /** @hide */
+    public static final int RESOLVE_SERVICE                         = BASE + 18;
+    /** @hide */
+    public static final int RESOLVE_SERVICE_FAILED                  = BASE + 19;
+    /** @hide */
+    public static final int RESOLVE_SERVICE_SUCCEEDED               = BASE + 20;
+
+    /** @hide */
+    public static final int ENABLE                                  = BASE + 24;
+    /** @hide */
+    public static final int DISABLE                                 = BASE + 25;
+
+    /** @hide */
+    public static final int NATIVE_DAEMON_EVENT                     = BASE + 26;
+
+    /** Dns based service discovery protocol */
+    public static final int PROTOCOL_DNS_SD = 0x0001;
+
+    private static final SparseArray<String> EVENT_NAMES = new SparseArray<>();
+    static {
+        EVENT_NAMES.put(DISCOVER_SERVICES, "DISCOVER_SERVICES");
+        EVENT_NAMES.put(DISCOVER_SERVICES_STARTED, "DISCOVER_SERVICES_STARTED");
+        EVENT_NAMES.put(DISCOVER_SERVICES_FAILED, "DISCOVER_SERVICES_FAILED");
+        EVENT_NAMES.put(SERVICE_FOUND, "SERVICE_FOUND");
+        EVENT_NAMES.put(SERVICE_LOST, "SERVICE_LOST");
+        EVENT_NAMES.put(STOP_DISCOVERY, "STOP_DISCOVERY");
+        EVENT_NAMES.put(STOP_DISCOVERY_FAILED, "STOP_DISCOVERY_FAILED");
+        EVENT_NAMES.put(STOP_DISCOVERY_SUCCEEDED, "STOP_DISCOVERY_SUCCEEDED");
+        EVENT_NAMES.put(REGISTER_SERVICE, "REGISTER_SERVICE");
+        EVENT_NAMES.put(REGISTER_SERVICE_FAILED, "REGISTER_SERVICE_FAILED");
+        EVENT_NAMES.put(REGISTER_SERVICE_SUCCEEDED, "REGISTER_SERVICE_SUCCEEDED");
+        EVENT_NAMES.put(UNREGISTER_SERVICE, "UNREGISTER_SERVICE");
+        EVENT_NAMES.put(UNREGISTER_SERVICE_FAILED, "UNREGISTER_SERVICE_FAILED");
+        EVENT_NAMES.put(UNREGISTER_SERVICE_SUCCEEDED, "UNREGISTER_SERVICE_SUCCEEDED");
+        EVENT_NAMES.put(RESOLVE_SERVICE, "RESOLVE_SERVICE");
+        EVENT_NAMES.put(RESOLVE_SERVICE_FAILED, "RESOLVE_SERVICE_FAILED");
+        EVENT_NAMES.put(RESOLVE_SERVICE_SUCCEEDED, "RESOLVE_SERVICE_SUCCEEDED");
+        EVENT_NAMES.put(ENABLE, "ENABLE");
+        EVENT_NAMES.put(DISABLE, "DISABLE");
+        EVENT_NAMES.put(NATIVE_DAEMON_EVENT, "NATIVE_DAEMON_EVENT");
+    }
+
+    /** @hide */
+    public static String nameOf(int event) {
+        String name = EVENT_NAMES.get(event);
+        if (name == null) {
+            return Integer.toString(event);
+        }
+        return name;
+    }
+
+    private static final int FIRST_LISTENER_KEY = 1;
+
+    private final INsdManager mService;
+    private final Context mContext;
+
+    private int mListenerKey = FIRST_LISTENER_KEY;
+    private final SparseArray mListenerMap = new SparseArray();
+    private final SparseArray<NsdServiceInfo> mServiceMap = new SparseArray<>();
+    private final Object mMapLock = new Object();
+
+    private final AsyncChannel mAsyncChannel = new AsyncChannel();
+    private ServiceHandler mHandler;
+    private final CountDownLatch mConnected = new CountDownLatch(1);
+
+    /**
+     * Create a new Nsd instance. Applications use
+     * {@link android.content.Context#getSystemService Context.getSystemService()} to retrieve
+     * {@link android.content.Context#NSD_SERVICE Context.NSD_SERVICE}.
+     * @param service the Binder interface
+     * @hide - hide this because it takes in a parameter of type INsdManager, which
+     * is a system private class.
+     */
+    public NsdManager(Context context, INsdManager service) {
+        mService = service;
+        mContext = context;
+        init();
+    }
+
+    /**
+     * @hide
+     */
+    @VisibleForTesting
+    public void disconnect() {
+        mAsyncChannel.disconnect();
+        mHandler.getLooper().quitSafely();
+    }
+
+    /**
+     * Failures are passed with {@link RegistrationListener#onRegistrationFailed},
+     * {@link RegistrationListener#onUnregistrationFailed},
+     * {@link DiscoveryListener#onStartDiscoveryFailed},
+     * {@link DiscoveryListener#onStopDiscoveryFailed} or {@link ResolveListener#onResolveFailed}.
+     *
+     * Indicates that the operation failed due to an internal error.
+     */
+    public static final int FAILURE_INTERNAL_ERROR               = 0;
+
+    /**
+     * Indicates that the operation failed because it is already active.
+     */
+    public static final int FAILURE_ALREADY_ACTIVE              = 3;
+
+    /**
+     * Indicates that the operation failed because the maximum outstanding
+     * requests from the applications have reached.
+     */
+    public static final int FAILURE_MAX_LIMIT                   = 4;
+
+    /** Interface for callback invocation for service discovery */
+    public interface DiscoveryListener {
+
+        public void onStartDiscoveryFailed(String serviceType, int errorCode);
+
+        public void onStopDiscoveryFailed(String serviceType, int errorCode);
+
+        public void onDiscoveryStarted(String serviceType);
+
+        public void onDiscoveryStopped(String serviceType);
+
+        public void onServiceFound(NsdServiceInfo serviceInfo);
+
+        public void onServiceLost(NsdServiceInfo serviceInfo);
+    }
+
+    /** Interface for callback invocation for service registration */
+    public interface RegistrationListener {
+
+        public void onRegistrationFailed(NsdServiceInfo serviceInfo, int errorCode);
+
+        public void onUnregistrationFailed(NsdServiceInfo serviceInfo, int errorCode);
+
+        public void onServiceRegistered(NsdServiceInfo serviceInfo);
+
+        public void onServiceUnregistered(NsdServiceInfo serviceInfo);
+    }
+
+    /** Interface for callback invocation for service resolution */
+    public interface ResolveListener {
+
+        public void onResolveFailed(NsdServiceInfo serviceInfo, int errorCode);
+
+        public void onServiceResolved(NsdServiceInfo serviceInfo);
+    }
+
+    @VisibleForTesting
+    class ServiceHandler extends Handler {
+        ServiceHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message message) {
+            final int what = message.what;
+            final int key = message.arg2;
+            switch (what) {
+                case AsyncChannel.CMD_CHANNEL_HALF_CONNECTED:
+                    mAsyncChannel.sendMessage(AsyncChannel.CMD_CHANNEL_FULL_CONNECTION);
+                    return;
+                case AsyncChannel.CMD_CHANNEL_FULLY_CONNECTED:
+                    mConnected.countDown();
+                    return;
+                case AsyncChannel.CMD_CHANNEL_DISCONNECTED:
+                    Log.e(TAG, "Channel lost");
+                    return;
+                default:
+                    break;
+            }
+            final Object listener;
+            final NsdServiceInfo ns;
+            synchronized (mMapLock) {
+                listener = mListenerMap.get(key);
+                ns = mServiceMap.get(key);
+            }
+            if (listener == null) {
+                Log.d(TAG, "Stale key " + message.arg2);
+                return;
+            }
+            if (DBG) {
+                Log.d(TAG, "received " + nameOf(what) + " for key " + key + ", service " + ns);
+            }
+            switch (what) {
+                case DISCOVER_SERVICES_STARTED:
+                    String s = getNsdServiceInfoType((NsdServiceInfo) message.obj);
+                    ((DiscoveryListener) listener).onDiscoveryStarted(s);
+                    break;
+                case DISCOVER_SERVICES_FAILED:
+                    removeListener(key);
+                    ((DiscoveryListener) listener).onStartDiscoveryFailed(getNsdServiceInfoType(ns),
+                            message.arg1);
+                    break;
+                case SERVICE_FOUND:
+                    ((DiscoveryListener) listener).onServiceFound((NsdServiceInfo) message.obj);
+                    break;
+                case SERVICE_LOST:
+                    ((DiscoveryListener) listener).onServiceLost((NsdServiceInfo) message.obj);
+                    break;
+                case STOP_DISCOVERY_FAILED:
+                    // TODO: failure to stop discovery should be internal and retried internally, as
+                    // the effect for the client is indistinguishable from STOP_DISCOVERY_SUCCEEDED
+                    removeListener(key);
+                    ((DiscoveryListener) listener).onStopDiscoveryFailed(getNsdServiceInfoType(ns),
+                            message.arg1);
+                    break;
+                case STOP_DISCOVERY_SUCCEEDED:
+                    removeListener(key);
+                    ((DiscoveryListener) listener).onDiscoveryStopped(getNsdServiceInfoType(ns));
+                    break;
+                case REGISTER_SERVICE_FAILED:
+                    removeListener(key);
+                    ((RegistrationListener) listener).onRegistrationFailed(ns, message.arg1);
+                    break;
+                case REGISTER_SERVICE_SUCCEEDED:
+                    ((RegistrationListener) listener).onServiceRegistered(
+                            (NsdServiceInfo) message.obj);
+                    break;
+                case UNREGISTER_SERVICE_FAILED:
+                    removeListener(key);
+                    ((RegistrationListener) listener).onUnregistrationFailed(ns, message.arg1);
+                    break;
+                case UNREGISTER_SERVICE_SUCCEEDED:
+                    // TODO: do not unregister listener until service is unregistered, or provide
+                    // alternative way for unregistering ?
+                    removeListener(message.arg2);
+                    ((RegistrationListener) listener).onServiceUnregistered(ns);
+                    break;
+                case RESOLVE_SERVICE_FAILED:
+                    removeListener(key);
+                    ((ResolveListener) listener).onResolveFailed(ns, message.arg1);
+                    break;
+                case RESOLVE_SERVICE_SUCCEEDED:
+                    removeListener(key);
+                    ((ResolveListener) listener).onServiceResolved((NsdServiceInfo) message.obj);
+                    break;
+                default:
+                    Log.d(TAG, "Ignored " + message);
+                    break;
+            }
+        }
+    }
+
+    private int nextListenerKey() {
+        // Ensure mListenerKey >= FIRST_LISTENER_KEY;
+        mListenerKey = Math.max(FIRST_LISTENER_KEY, mListenerKey + 1);
+        return mListenerKey;
+    }
+
+    // Assert that the listener is not in the map, then add it and returns its key
+    private int putListener(Object listener, NsdServiceInfo s) {
+        checkListener(listener);
+        final int key;
+        synchronized (mMapLock) {
+            int valueIndex = mListenerMap.indexOfValue(listener);
+            checkArgument(valueIndex == -1, "listener already in use");
+            key = nextListenerKey();
+            mListenerMap.put(key, listener);
+            mServiceMap.put(key, s);
+        }
+        return key;
+    }
+
+    private void removeListener(int key) {
+        synchronized (mMapLock) {
+            mListenerMap.remove(key);
+            mServiceMap.remove(key);
+        }
+    }
+
+    private int getListenerKey(Object listener) {
+        checkListener(listener);
+        synchronized (mMapLock) {
+            int valueIndex = mListenerMap.indexOfValue(listener);
+            checkArgument(valueIndex != -1, "listener not registered");
+            return mListenerMap.keyAt(valueIndex);
+        }
+    }
+
+    private static String getNsdServiceInfoType(NsdServiceInfo s) {
+        if (s == null) return "?";
+        return s.getServiceType();
+    }
+
+    /**
+     * Initialize AsyncChannel
+     */
+    private void init() {
+        final Messenger messenger = getMessenger();
+        if (messenger == null) {
+            fatal("Failed to obtain service Messenger");
+        }
+        HandlerThread t = new HandlerThread("NsdManager");
+        t.start();
+        mHandler = new ServiceHandler(t.getLooper());
+        mAsyncChannel.connect(mContext, mHandler, messenger);
+        try {
+            mConnected.await();
+        } catch (InterruptedException e) {
+            fatal("Interrupted wait at init");
+        }
+    }
+
+    private static void fatal(String msg) {
+        Log.e(TAG, msg);
+        throw new RuntimeException(msg);
+    }
+
+    /**
+     * Register a service to be discovered by other services.
+     *
+     * <p> The function call immediately returns after sending a request to register service
+     * to the framework. The application is notified of a successful registration
+     * through the callback {@link RegistrationListener#onServiceRegistered} or a failure
+     * through {@link RegistrationListener#onRegistrationFailed}.
+     *
+     * <p> The application should call {@link #unregisterService} when the service
+     * registration is no longer required, and/or whenever the application is stopped.
+     *
+     * @param serviceInfo The service being registered
+     * @param protocolType The service discovery protocol
+     * @param listener The listener notifies of a successful registration and is used to
+     * unregister this service through a call on {@link #unregisterService}. Cannot be null.
+     * Cannot be in use for an active service registration.
+     */
+    public void registerService(NsdServiceInfo serviceInfo, int protocolType,
+            RegistrationListener listener) {
+        checkArgument(serviceInfo.getPort() > 0, "Invalid port number");
+        checkServiceInfo(serviceInfo);
+        checkProtocol(protocolType);
+        int key = putListener(listener, serviceInfo);
+        mAsyncChannel.sendMessage(REGISTER_SERVICE, 0, key, serviceInfo);
+    }
+
+    /**
+     * Unregister a service registered through {@link #registerService}. A successful
+     * unregister is notified to the application with a call to
+     * {@link RegistrationListener#onServiceUnregistered}.
+     *
+     * @param listener This should be the listener object that was passed to
+     * {@link #registerService}. It identifies the service that should be unregistered
+     * and notifies of a successful or unsuccessful unregistration via the listener
+     * callbacks.  In API versions 20 and above, the listener object may be used for
+     * another service registration once the callback has been called.  In API versions <= 19,
+     * there is no entirely reliable way to know when a listener may be re-used, and a new
+     * listener should be created for each service registration request.
+     */
+    public void unregisterService(RegistrationListener listener) {
+        int id = getListenerKey(listener);
+        mAsyncChannel.sendMessage(UNREGISTER_SERVICE, 0, id);
+    }
+
+    /**
+     * Initiate service discovery to browse for instances of a service type. Service discovery
+     * consumes network bandwidth and will continue until the application calls
+     * {@link #stopServiceDiscovery}.
+     *
+     * <p> The function call immediately returns after sending a request to start service
+     * discovery to the framework. The application is notified of a success to initiate
+     * discovery through the callback {@link DiscoveryListener#onDiscoveryStarted} or a failure
+     * through {@link DiscoveryListener#onStartDiscoveryFailed}.
+     *
+     * <p> Upon successful start, application is notified when a service is found with
+     * {@link DiscoveryListener#onServiceFound} or when a service is lost with
+     * {@link DiscoveryListener#onServiceLost}.
+     *
+     * <p> Upon failure to start, service discovery is not active and application does
+     * not need to invoke {@link #stopServiceDiscovery}
+     *
+     * <p> The application should call {@link #stopServiceDiscovery} when discovery of this
+     * service type is no longer required, and/or whenever the application is paused or
+     * stopped.
+     *
+     * @param serviceType The service type being discovered. Examples include "_http._tcp" for
+     * http services or "_ipp._tcp" for printers
+     * @param protocolType The service discovery protocol
+     * @param listener  The listener notifies of a successful discovery and is used
+     * to stop discovery on this serviceType through a call on {@link #stopServiceDiscovery}.
+     * Cannot be null. Cannot be in use for an active service discovery.
+     */
+    public void discoverServices(String serviceType, int protocolType, DiscoveryListener listener) {
+        checkStringNotEmpty(serviceType, "Service type cannot be empty");
+        checkProtocol(protocolType);
+
+        NsdServiceInfo s = new NsdServiceInfo();
+        s.setServiceType(serviceType);
+
+        int key = putListener(listener, s);
+        mAsyncChannel.sendMessage(DISCOVER_SERVICES, 0, key, s);
+    }
+
+    /**
+     * Stop service discovery initiated with {@link #discoverServices}.  An active service
+     * discovery is notified to the application with {@link DiscoveryListener#onDiscoveryStarted}
+     * and it stays active until the application invokes a stop service discovery. A successful
+     * stop is notified to with a call to {@link DiscoveryListener#onDiscoveryStopped}.
+     *
+     * <p> Upon failure to stop service discovery, application is notified through
+     * {@link DiscoveryListener#onStopDiscoveryFailed}.
+     *
+     * @param listener This should be the listener object that was passed to {@link #discoverServices}.
+     * It identifies the discovery that should be stopped and notifies of a successful or
+     * unsuccessful stop.  In API versions 20 and above, the listener object may be used for
+     * another service discovery once the callback has been called.  In API versions <= 19,
+     * there is no entirely reliable way to know when a listener may be re-used, and a new
+     * listener should be created for each service discovery request.
+     */
+    public void stopServiceDiscovery(DiscoveryListener listener) {
+        int id = getListenerKey(listener);
+        mAsyncChannel.sendMessage(STOP_DISCOVERY, 0, id);
+    }
+
+    /**
+     * Resolve a discovered service. An application can resolve a service right before
+     * establishing a connection to fetch the IP and port details on which to setup
+     * the connection.
+     *
+     * @param serviceInfo service to be resolved
+     * @param listener to receive callback upon success or failure. Cannot be null.
+     * Cannot be in use for an active service resolution.
+     */
+    public void resolveService(NsdServiceInfo serviceInfo, ResolveListener listener) {
+        checkServiceInfo(serviceInfo);
+        int key = putListener(listener, serviceInfo);
+        mAsyncChannel.sendMessage(RESOLVE_SERVICE, 0, key, serviceInfo);
+    }
+
+    /** Internal use only @hide */
+    public void setEnabled(boolean enabled) {
+        try {
+            mService.setEnabled(enabled);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Get a reference to NsdService handler. This is used to establish
+     * an AsyncChannel communication with the service
+     *
+     * @return Messenger pointing to the NsdService handler
+     */
+    private Messenger getMessenger() {
+        try {
+            return mService.getMessenger();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    private static void checkListener(Object listener) {
+        checkNotNull(listener, "listener cannot be null");
+    }
+
+    private static void checkProtocol(int protocolType) {
+        checkArgument(protocolType == PROTOCOL_DNS_SD, "Unsupported protocol");
+    }
+
+    private static void checkServiceInfo(NsdServiceInfo serviceInfo) {
+        checkNotNull(serviceInfo, "NsdServiceInfo cannot be null");
+        checkStringNotEmpty(serviceInfo.getServiceName(), "Service name cannot be empty");
+        checkStringNotEmpty(serviceInfo.getServiceType(), "Service type cannot be empty");
+    }
+}
diff --git a/android/net/nsd/NsdServiceInfo.java b/android/net/nsd/NsdServiceInfo.java
new file mode 100644
index 0000000..459b140
--- /dev/null
+++ b/android/net/nsd/NsdServiceInfo.java
@@ -0,0 +1,391 @@
+/*
+ * Copyright (C) 2012 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.nsd;
+
+import android.annotation.NonNull;
+import android.annotation.UnsupportedAppUsage;
+import android.os.Parcelable;
+import android.os.Parcel;
+import android.text.TextUtils;
+import android.util.Base64;
+import android.util.Log;
+import android.util.ArrayMap;
+
+import java.io.UnsupportedEncodingException;
+import java.net.InetAddress;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.Map;
+
+/**
+ * A class representing service information for network service discovery
+ * {@see NsdManager}
+ */
+public final class NsdServiceInfo implements Parcelable {
+
+    private static final String TAG = "NsdServiceInfo";
+
+    private String mServiceName;
+
+    private String mServiceType;
+
+    private final ArrayMap<String, byte[]> mTxtRecord = new ArrayMap<>();
+
+    private InetAddress mHost;
+
+    private int mPort;
+
+    public NsdServiceInfo() {
+    }
+
+    /** @hide */
+    public NsdServiceInfo(String sn, String rt) {
+        mServiceName = sn;
+        mServiceType = rt;
+    }
+
+    /** Get the service name */
+    public String getServiceName() {
+        return mServiceName;
+    }
+
+    /** Set the service name */
+    public void setServiceName(String s) {
+        mServiceName = s;
+    }
+
+    /** Get the service type */
+    public String getServiceType() {
+        return mServiceType;
+    }
+
+    /** Set the service type */
+    public void setServiceType(String s) {
+        mServiceType = s;
+    }
+
+    /** Get the host address. The host address is valid for a resolved service. */
+    public InetAddress getHost() {
+        return mHost;
+    }
+
+    /** Set the host address */
+    public void setHost(InetAddress s) {
+        mHost = s;
+    }
+
+    /** Get port number. The port number is valid for a resolved service. */
+    public int getPort() {
+        return mPort;
+    }
+
+    /** Set port number */
+    public void setPort(int p) {
+        mPort = p;
+    }
+
+    /**
+     * Unpack txt information from a base-64 encoded byte array.
+     *
+     * @param rawRecords The raw base64 encoded records string read from netd.
+     *
+     * @hide
+     */
+    public void setTxtRecords(@NonNull String rawRecords) {
+        byte[] txtRecordsRawBytes = Base64.decode(rawRecords, Base64.DEFAULT);
+
+        // There can be multiple TXT records after each other. Each record has to following format:
+        //
+        // byte                  type                  required   meaning
+        // -------------------   -------------------   --------   ----------------------------------
+        // 0                     unsigned 8 bit        yes        size of record excluding this byte
+        // 1 - n                 ASCII but not '='     yes        key
+        // n + 1                 '='                   optional   separator of key and value
+        // n + 2 - record size   uninterpreted bytes   optional   value
+        //
+        // Example legal records:
+        // [11, 'm', 'y', 'k', 'e', 'y', '=', 0x0, 0x4, 0x65, 0x7, 0xff]
+        // [17, 'm', 'y', 'K', 'e', 'y', 'W', 'i', 't', 'h', 'N', 'o', 'V', 'a', 'l', 'u', 'e', '=']
+        // [12, 'm', 'y', 'B', 'o', 'o', 'l', 'e', 'a', 'n', 'K', 'e', 'y']
+        //
+        // Example corrupted records
+        // [3, =, 1, 2]    <- key is empty
+        // [3, 0, =, 2]    <- key contains non-ASCII character. We handle this by replacing the
+        //                    invalid characters instead of skipping the record.
+        // [30, 'a', =, 2] <- length exceeds total left over bytes in the TXT records array, we
+        //                    handle this by reducing the length of the record as needed.
+        int pos = 0;
+        while (pos < txtRecordsRawBytes.length) {
+            // recordLen is an unsigned 8 bit value
+            int recordLen = txtRecordsRawBytes[pos] & 0xff;
+            pos += 1;
+
+            try {
+                if (recordLen == 0) {
+                    throw new IllegalArgumentException("Zero sized txt record");
+                } else if (pos + recordLen > txtRecordsRawBytes.length) {
+                    Log.w(TAG, "Corrupt record length (pos = " + pos + "): " + recordLen);
+                    recordLen = txtRecordsRawBytes.length - pos;
+                }
+
+                // Decode key-value records
+                String key = null;
+                byte[] value = null;
+                int valueLen = 0;
+                for (int i = pos; i < pos + recordLen; i++) {
+                    if (key == null) {
+                        if (txtRecordsRawBytes[i] == '=') {
+                            key = new String(txtRecordsRawBytes, pos, i - pos,
+                                    StandardCharsets.US_ASCII);
+                        }
+                    } else {
+                        if (value == null) {
+                            value = new byte[recordLen - key.length() - 1];
+                        }
+                        value[valueLen] = txtRecordsRawBytes[i];
+                        valueLen++;
+                    }
+                }
+
+                // If '=' was not found we have a boolean record
+                if (key == null) {
+                    key = new String(txtRecordsRawBytes, pos, recordLen, StandardCharsets.US_ASCII);
+                }
+
+                if (TextUtils.isEmpty(key)) {
+                    // Empty keys are not allowed (RFC6763 6.4)
+                    throw new IllegalArgumentException("Invalid txt record (key is empty)");
+                }
+
+                if (getAttributes().containsKey(key)) {
+                    // When we have a duplicate record, the later ones are ignored (RFC6763 6.4)
+                    throw new IllegalArgumentException("Invalid txt record (duplicate key \"" + key + "\")");
+                }
+
+                setAttribute(key, value);
+            } catch (IllegalArgumentException e) {
+                Log.e(TAG, "While parsing txt records (pos = " + pos + "): " + e.getMessage());
+            }
+
+            pos += recordLen;
+        }
+    }
+
+    /** @hide */
+    @UnsupportedAppUsage
+    public void setAttribute(String key, byte[] value) {
+        if (TextUtils.isEmpty(key)) {
+            throw new IllegalArgumentException("Key cannot be empty");
+        }
+
+        // Key must be printable US-ASCII, excluding =.
+        for (int i = 0; i < key.length(); ++i) {
+            char character = key.charAt(i);
+            if (character < 0x20 || character > 0x7E) {
+                throw new IllegalArgumentException("Key strings must be printable US-ASCII");
+            } else if (character == 0x3D) {
+                throw new IllegalArgumentException("Key strings must not include '='");
+            }
+        }
+
+        // Key length + value length must be < 255.
+        if (key.length() + (value == null ? 0 : value.length) >= 255) {
+            throw new IllegalArgumentException("Key length + value length must be < 255 bytes");
+        }
+
+        // Warn if key is > 9 characters, as recommended by RFC 6763 section 6.4.
+        if (key.length() > 9) {
+            Log.w(TAG, "Key lengths > 9 are discouraged: " + key);
+        }
+
+        // Check against total TXT record size limits.
+        // Arbitrary 400 / 1300 byte limits taken from RFC 6763 section 6.2.
+        int txtRecordSize = getTxtRecordSize();
+        int futureSize = txtRecordSize + key.length() + (value == null ? 0 : value.length) + 2;
+        if (futureSize > 1300) {
+            throw new IllegalArgumentException("Total length of attributes must be < 1300 bytes");
+        } else if (futureSize > 400) {
+            Log.w(TAG, "Total length of all attributes exceeds 400 bytes; truncation may occur");
+        }
+
+        mTxtRecord.put(key, value);
+    }
+
+    /**
+     * Add a service attribute as a key/value pair.
+     *
+     * <p> Service attributes are included as DNS-SD TXT record pairs.
+     *
+     * <p> The key must be US-ASCII printable characters, excluding the '=' character.  Values may
+     * be UTF-8 strings or null.  The total length of key + value must be less than 255 bytes.
+     *
+     * <p> Keys should be short, ideally no more than 9 characters, and unique per instance of
+     * {@link NsdServiceInfo}.  Calling {@link #setAttribute} twice with the same key will overwrite
+     * first value.
+     */
+    public void setAttribute(String key, String value) {
+        try {
+            setAttribute(key, value == null ? (byte []) null : value.getBytes("UTF-8"));
+        } catch (UnsupportedEncodingException e) {
+            throw new IllegalArgumentException("Value must be UTF-8");
+        }
+    }
+
+    /** Remove an attribute by key */
+    public void removeAttribute(String key) {
+        mTxtRecord.remove(key);
+    }
+
+    /**
+     * Retrieve attributes as a map of String keys to byte[] values. The attributes map is only
+     * valid for a resolved service.
+     *
+     * <p> The returned map is unmodifiable; changes must be made through {@link #setAttribute} and
+     * {@link #removeAttribute}.
+     */
+    public Map<String, byte[]> getAttributes() {
+        return Collections.unmodifiableMap(mTxtRecord);
+    }
+
+    private int getTxtRecordSize() {
+        int txtRecordSize = 0;
+        for (Map.Entry<String, byte[]> entry : mTxtRecord.entrySet()) {
+            txtRecordSize += 2;  // One for the length byte, one for the = between key and value.
+            txtRecordSize += entry.getKey().length();
+            byte[] value = entry.getValue();
+            txtRecordSize += value == null ? 0 : value.length;
+        }
+        return txtRecordSize;
+    }
+
+    /** @hide */
+    public @NonNull byte[] getTxtRecord() {
+        int txtRecordSize = getTxtRecordSize();
+        if (txtRecordSize == 0) {
+            return new byte[]{};
+        }
+
+        byte[] txtRecord = new byte[txtRecordSize];
+        int ptr = 0;
+        for (Map.Entry<String, byte[]> entry : mTxtRecord.entrySet()) {
+            String key = entry.getKey();
+            byte[] value = entry.getValue();
+
+            // One byte to record the length of this key/value pair.
+            txtRecord[ptr++] = (byte) (key.length() + (value == null ? 0 : value.length) + 1);
+
+            // The key, in US-ASCII.
+            // Note: use the StandardCharsets const here because it doesn't raise exceptions and we
+            // already know the key is ASCII at this point.
+            System.arraycopy(key.getBytes(StandardCharsets.US_ASCII), 0, txtRecord, ptr,
+                    key.length());
+            ptr += key.length();
+
+            // US-ASCII '=' character.
+            txtRecord[ptr++] = (byte)'=';
+
+            // The value, as any raw bytes.
+            if (value != null) {
+                System.arraycopy(value, 0, txtRecord, ptr, value.length);
+                ptr += value.length;
+            }
+        }
+        return txtRecord;
+    }
+
+    public String toString() {
+        StringBuffer sb = new StringBuffer();
+
+        sb.append("name: ").append(mServiceName)
+                .append(", type: ").append(mServiceType)
+                .append(", host: ").append(mHost)
+                .append(", port: ").append(mPort);
+
+        byte[] txtRecord = getTxtRecord();
+        if (txtRecord != null) {
+            sb.append(", txtRecord: ").append(new String(txtRecord, StandardCharsets.UTF_8));
+        }
+        return sb.toString();
+    }
+
+    /** Implement the Parcelable interface */
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Implement the Parcelable interface */
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeString(mServiceName);
+        dest.writeString(mServiceType);
+        if (mHost != null) {
+            dest.writeInt(1);
+            dest.writeByteArray(mHost.getAddress());
+        } else {
+            dest.writeInt(0);
+        }
+        dest.writeInt(mPort);
+
+        // TXT record key/value pairs.
+        dest.writeInt(mTxtRecord.size());
+        for (String key : mTxtRecord.keySet()) {
+            byte[] value = mTxtRecord.get(key);
+            if (value != null) {
+                dest.writeInt(1);
+                dest.writeInt(value.length);
+                dest.writeByteArray(value);
+            } else {
+                dest.writeInt(0);
+            }
+            dest.writeString(key);
+        }
+    }
+
+    /** Implement the Parcelable interface */
+    public static final @android.annotation.NonNull Creator<NsdServiceInfo> CREATOR =
+        new Creator<NsdServiceInfo>() {
+            public NsdServiceInfo createFromParcel(Parcel in) {
+                NsdServiceInfo info = new NsdServiceInfo();
+                info.mServiceName = in.readString();
+                info.mServiceType = in.readString();
+
+                if (in.readInt() == 1) {
+                    try {
+                        info.mHost = InetAddress.getByAddress(in.createByteArray());
+                    } catch (java.net.UnknownHostException e) {}
+                }
+
+                info.mPort = in.readInt();
+
+                // TXT record key/value pairs.
+                int recordCount = in.readInt();
+                for (int i = 0; i < recordCount; ++i) {
+                    byte[] valueArray = null;
+                    if (in.readInt() == 1) {
+                        int valueLength = in.readInt();
+                        valueArray = new byte[valueLength];
+                        in.readByteArray(valueArray);
+                    }
+                    info.mTxtRecord.put(in.readString(), valueArray);
+                }
+                return info;
+            }
+
+            public NsdServiceInfo[] newArray(int size) {
+                return new NsdServiceInfo[size];
+            }
+        };
+}
diff --git a/android/net/rtp/AudioCodec.java b/android/net/rtp/AudioCodec.java
new file mode 100644
index 0000000..85255c8
--- /dev/null
+++ b/android/net/rtp/AudioCodec.java
@@ -0,0 +1,146 @@
+/*
+ * Copyright (C) 2010 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.rtp;
+
+import java.util.Arrays;
+
+/**
+ * This class defines a collection of audio codecs to be used with
+ * {@link AudioStream}s. Their parameters are designed to be exchanged using
+ * Session Description Protocol (SDP). Most of the values listed here can be
+ * found in RFC 3551, while others are described in separated standards.
+ *
+ * <p>Few simple configurations are defined as public static instances for the
+ * convenience of direct uses. More complicated ones could be obtained using
+ * {@link #getCodec(int, String, String)}. For example, one can use the
+ * following snippet to create a mode-1-only AMR codec.</p>
+ * <pre>
+ * AudioCodec codec = AudioCodec.getCodec(100, "AMR/8000", "mode-set=1");
+ * </pre>
+ *
+ * @see AudioStream
+ */
+public class AudioCodec {
+    /**
+     * The RTP payload type of the encoding.
+     */
+    public final int type;
+
+    /**
+     * The encoding parameters to be used in the corresponding SDP attribute.
+     */
+    public final String rtpmap;
+
+    /**
+     * The format parameters to be used in the corresponding SDP attribute.
+     */
+    public final String fmtp;
+
+    /**
+     * G.711 u-law audio codec.
+     */
+    public static final AudioCodec PCMU = new AudioCodec(0, "PCMU/8000", null);
+
+    /**
+     * G.711 a-law audio codec.
+     */
+    public static final AudioCodec PCMA = new AudioCodec(8, "PCMA/8000", null);
+
+    /**
+     * GSM Full-Rate audio codec, also known as GSM-FR, GSM 06.10, GSM, or
+     * simply FR.
+     */
+    public static final AudioCodec GSM = new AudioCodec(3, "GSM/8000", null);
+
+    /**
+     * GSM Enhanced Full-Rate audio codec, also known as GSM-EFR, GSM 06.60, or
+     * simply EFR.
+     */
+    public static final AudioCodec GSM_EFR = new AudioCodec(96, "GSM-EFR/8000", null);
+
+    /**
+     * Adaptive Multi-Rate narrowband audio codec, also known as AMR or AMR-NB.
+     * Currently CRC, robust sorting, and interleaving are not supported. See
+     * more details about these features in RFC 4867.
+     */
+    public static final AudioCodec AMR = new AudioCodec(97, "AMR/8000", null);
+
+    private static final AudioCodec[] sCodecs = {GSM_EFR, AMR, GSM, PCMU, PCMA};
+
+    private AudioCodec(int type, String rtpmap, String fmtp) {
+        this.type = type;
+        this.rtpmap = rtpmap;
+        this.fmtp = fmtp;
+    }
+
+    /**
+     * Returns system supported audio codecs.
+     */
+    public static AudioCodec[] getCodecs() {
+        return Arrays.copyOf(sCodecs, sCodecs.length);
+    }
+
+    /**
+     * Creates an AudioCodec according to the given configuration.
+     *
+     * @param type The payload type of the encoding defined in RTP/AVP.
+     * @param rtpmap The encoding parameters specified in the corresponding SDP
+     *     attribute, or null if it is not available.
+     * @param fmtp The format parameters specified in the corresponding SDP
+     *     attribute, or null if it is not available.
+     * @return The configured AudioCodec or {@code null} if it is not supported.
+     */
+    public static AudioCodec getCodec(int type, String rtpmap, String fmtp) {
+        if (type < 0 || type > 127) {
+            return null;
+        }
+
+        AudioCodec hint = null;
+        if (rtpmap != null) {
+            String clue = rtpmap.trim().toUpperCase();
+            for (AudioCodec codec : sCodecs) {
+                if (clue.startsWith(codec.rtpmap)) {
+                    String channels = clue.substring(codec.rtpmap.length());
+                    if (channels.length() == 0 || channels.equals("/1")) {
+                        hint = codec;
+                    }
+                    break;
+                }
+            }
+        } else if (type < 96) {
+            for (AudioCodec codec : sCodecs) {
+                if (type == codec.type) {
+                    hint = codec;
+                    rtpmap = codec.rtpmap;
+                    break;
+                }
+            }
+        }
+
+        if (hint == null) {
+            return null;
+        }
+        if (hint == AMR && fmtp != null) {
+            String clue = fmtp.toLowerCase();
+            if (clue.contains("crc=1") || clue.contains("robust-sorting=1") ||
+                    clue.contains("interleaving=")) {
+                return null;
+            }
+        }
+        return new AudioCodec(type, rtpmap, fmtp);
+    }
+}
diff --git a/android/net/rtp/AudioGroup.java b/android/net/rtp/AudioGroup.java
new file mode 100644
index 0000000..15390d3
--- /dev/null
+++ b/android/net/rtp/AudioGroup.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright (C) 2010 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.rtp;
+
+import android.app.ActivityThread;
+import android.media.AudioManager;
+
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * An AudioGroup is an audio hub for the speaker, the microphone, and
+ * {@link AudioStream}s. Each of these components can be logically turned on
+ * or off by calling {@link #setMode(int)} or {@link RtpStream#setMode(int)}.
+ * The AudioGroup will go through these components and process them one by one
+ * within its execution loop. The loop consists of four steps. First, for each
+ * AudioStream not in {@link RtpStream#MODE_SEND_ONLY}, decodes its incoming
+ * packets and stores in its buffer. Then, if the microphone is enabled,
+ * processes the recorded audio and stores in its buffer. Third, if the speaker
+ * is enabled, mixes all AudioStream buffers and plays back. Finally, for each
+ * AudioStream not in {@link RtpStream#MODE_RECEIVE_ONLY}, mixes all other
+ * buffers and sends back the encoded packets. An AudioGroup does nothing if
+ * there is no AudioStream in it.
+ *
+ * <p>Few things must be noticed before using these classes. The performance is
+ * highly related to the system load and the network bandwidth. Usually a
+ * simpler {@link AudioCodec} costs fewer CPU cycles but requires more network
+ * bandwidth, and vise versa. Using two AudioStreams at the same time doubles
+ * not only the load but also the bandwidth. The condition varies from one
+ * device to another, and developers should choose the right combination in
+ * order to get the best result.</p>
+ *
+ * <p>It is sometimes useful to keep multiple AudioGroups at the same time. For
+ * example, a Voice over IP (VoIP) application might want to put a conference
+ * call on hold in order to make a new call but still allow people in the
+ * conference call talking to each other. This can be done easily using two
+ * AudioGroups, but there are some limitations. Since the speaker and the
+ * microphone are globally shared resources, only one AudioGroup at a time is
+ * allowed to run in a mode other than {@link #MODE_ON_HOLD}. The others will
+ * be unable to acquire these resources and fail silently.</p>
+ *
+ * <p class="note">Using this class requires
+ * {@link android.Manifest.permission#RECORD_AUDIO} permission. Developers
+ * should set the audio mode to {@link AudioManager#MODE_IN_COMMUNICATION}
+ * using {@link AudioManager#setMode(int)} and change it back when none of
+ * the AudioGroups is in use.</p>
+ *
+ * @see AudioStream
+ */
+public class AudioGroup {
+    /**
+     * This mode is similar to {@link #MODE_NORMAL} except the speaker and
+     * the microphone are both disabled.
+     */
+    public static final int MODE_ON_HOLD = 0;
+
+    /**
+     * This mode is similar to {@link #MODE_NORMAL} except the microphone is
+     * disabled.
+     */
+    public static final int MODE_MUTED = 1;
+
+    /**
+     * This mode indicates that the speaker, the microphone, and all
+     * {@link AudioStream}s in the group are enabled. First, the packets
+     * received from the streams are decoded and mixed with the audio recorded
+     * from the microphone. Then, the results are played back to the speaker,
+     * encoded and sent back to each stream.
+     */
+    public static final int MODE_NORMAL = 2;
+
+    /**
+     * This mode is similar to {@link #MODE_NORMAL} except the echo suppression
+     * is enabled. It should be only used when the speaker phone is on.
+     */
+    public static final int MODE_ECHO_SUPPRESSION = 3;
+
+    private static final int MODE_LAST = 3;
+
+    private final Map<AudioStream, Long> mStreams;
+    private int mMode = MODE_ON_HOLD;
+
+    private long mNative;
+    static {
+        System.loadLibrary("rtp_jni");
+    }
+
+    /**
+     * Creates an empty AudioGroup.
+     */
+    public AudioGroup() {
+        mStreams = new HashMap<AudioStream, Long>();
+    }
+
+    /**
+     * Returns the {@link AudioStream}s in this group.
+     */
+    public AudioStream[] getStreams() {
+        synchronized (this) {
+            return mStreams.keySet().toArray(new AudioStream[mStreams.size()]);
+        }
+    }
+
+    /**
+     * Returns the current mode.
+     */
+    public int getMode() {
+        return mMode;
+    }
+
+    /**
+     * Changes the current mode. It must be one of {@link #MODE_ON_HOLD},
+     * {@link #MODE_MUTED}, {@link #MODE_NORMAL}, and
+     * {@link #MODE_ECHO_SUPPRESSION}.
+     *
+     * @param mode The mode to change to.
+     * @throws IllegalArgumentException if the mode is invalid.
+     */
+    public void setMode(int mode) {
+        if (mode < 0 || mode > MODE_LAST) {
+            throw new IllegalArgumentException("Invalid mode");
+        }
+        synchronized (this) {
+            nativeSetMode(mode);
+            mMode = mode;
+        }
+    }
+
+    private native void nativeSetMode(int mode);
+
+    // Package-private method used by AudioStream.join().
+    synchronized void add(AudioStream stream) {
+        if (!mStreams.containsKey(stream)) {
+            try {
+                AudioCodec codec = stream.getCodec();
+                String codecSpec = String.format(Locale.US, "%d %s %s", codec.type,
+                        codec.rtpmap, codec.fmtp);
+                long id = nativeAdd(stream.getMode(), stream.getSocket(),
+                        stream.getRemoteAddress().getHostAddress(),
+                        stream.getRemotePort(), codecSpec, stream.getDtmfType(),
+                        ActivityThread.currentOpPackageName());
+                mStreams.put(stream, id);
+            } catch (NullPointerException e) {
+                throw new IllegalStateException(e);
+            }
+        }
+    }
+
+    private native long nativeAdd(int mode, int socket, String remoteAddress,
+            int remotePort, String codecSpec, int dtmfType, String opPackageName);
+
+    // Package-private method used by AudioStream.join().
+    synchronized void remove(AudioStream stream) {
+        Long id = mStreams.remove(stream);
+        if (id != null) {
+            nativeRemove(id);
+        }
+    }
+
+    private native void nativeRemove(long id);
+
+    /**
+     * Sends a DTMF digit to every {@link AudioStream} in this group. Currently
+     * only event {@code 0} to {@code 15} are supported.
+     *
+     * @throws IllegalArgumentException if the event is invalid.
+     */
+    public void sendDtmf(int event) {
+        if (event < 0 || event > 15) {
+            throw new IllegalArgumentException("Invalid event");
+        }
+        synchronized (this) {
+            nativeSendDtmf(event);
+        }
+    }
+
+    private native void nativeSendDtmf(int event);
+
+    /**
+     * Removes every {@link AudioStream} in this group.
+     */
+    public void clear() {
+        for (AudioStream stream : getStreams()) {
+            stream.join(null);
+        }
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        nativeRemove(0L);
+        super.finalize();
+    }
+}
diff --git a/android/net/rtp/AudioStream.java b/android/net/rtp/AudioStream.java
new file mode 100644
index 0000000..5cd1abc
--- /dev/null
+++ b/android/net/rtp/AudioStream.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2010 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.rtp;
+
+import java.net.InetAddress;
+import java.net.SocketException;
+
+/**
+ * An AudioStream is a {@link RtpStream} which carrys audio payloads over
+ * Real-time Transport Protocol (RTP). Two different classes are developed in
+ * order to support various usages such as audio conferencing. An AudioStream
+ * represents a remote endpoint which consists of a network mapping and a
+ * configured {@link AudioCodec}. On the other side, An {@link AudioGroup}
+ * represents a local endpoint which mixes all the AudioStreams and optionally
+ * interacts with the speaker and the microphone at the same time. The simplest
+ * usage includes one for each endpoints. For other combinations, developers
+ * should be aware of the limitations described in {@link AudioGroup}.
+ *
+ * <p>An AudioStream becomes busy when it joins an AudioGroup. In this case most
+ * of the setter methods are disabled. This is designed to ease the task of
+ * managing native resources. One can always make an AudioStream leave its
+ * AudioGroup by calling {@link #join(AudioGroup)} with {@code null} and put it
+ * back after the modification is done.</p>
+ *
+ * <p class="note">Using this class requires
+ * {@link android.Manifest.permission#INTERNET} permission.</p>
+ *
+ * @see RtpStream
+ * @see AudioGroup
+ */
+public class AudioStream extends RtpStream {
+    private AudioCodec mCodec;
+    private int mDtmfType = -1;
+    private AudioGroup mGroup;
+
+    /**
+     * Creates an AudioStream on the given local address. Note that the local
+     * port is assigned automatically to conform with RFC 3550.
+     *
+     * @param address The network address of the local host to bind to.
+     * @throws SocketException if the address cannot be bound or a problem
+     *     occurs during binding.
+     */
+    public AudioStream(InetAddress address) throws SocketException {
+        super(address);
+    }
+
+    /**
+     * Returns {@code true} if the stream has already joined an
+     * {@link AudioGroup}.
+     */
+    @Override
+    public final boolean isBusy() {
+        return mGroup != null;
+    }
+
+    /**
+     * Returns the joined {@link AudioGroup}.
+     */
+    public AudioGroup getGroup() {
+        return mGroup;
+    }
+
+    /**
+     * Joins an {@link AudioGroup}. Each stream can join only one group at a
+     * time. The group can be changed by passing a different one or removed
+     * by calling this method with {@code null}.
+     *
+     * @param group The AudioGroup to join or {@code null} to leave.
+     * @throws IllegalStateException if the stream is not properly configured.
+     * @see AudioGroup
+     */
+    public void join(AudioGroup group) {
+        synchronized (this) {
+            if (mGroup == group) {
+                return;
+            }
+            if (mGroup != null) {
+                mGroup.remove(this);
+                mGroup = null;
+            }
+            if (group != null) {
+                group.add(this);
+                mGroup = group;
+            }
+        }
+    }
+
+    /**
+     * Returns the {@link AudioCodec}, or {@code null} if it is not set.
+     *
+     * @see #setCodec(AudioCodec)
+     */
+    public AudioCodec getCodec() {
+        return mCodec;
+    }
+
+    /**
+     * Sets the {@link AudioCodec}.
+     *
+     * @param codec The AudioCodec to be used.
+     * @throws IllegalArgumentException if its type is used by DTMF.
+     * @throws IllegalStateException if the stream is busy.
+     */
+    public void setCodec(AudioCodec codec) {
+        if (isBusy()) {
+            throw new IllegalStateException("Busy");
+        }
+        if (codec.type == mDtmfType) {
+            throw new IllegalArgumentException("The type is used by DTMF");
+        }
+        mCodec = codec;
+    }
+
+    /**
+     * Returns the RTP payload type for dual-tone multi-frequency (DTMF) digits,
+     * or {@code -1} if it is not enabled.
+     *
+     * @see #setDtmfType(int)
+     */
+    public int getDtmfType() {
+        return mDtmfType;
+    }
+
+    /**
+     * Sets the RTP payload type for dual-tone multi-frequency (DTMF) digits.
+     * The primary usage is to send digits to the remote gateway to perform
+     * certain tasks, such as second-stage dialing. According to RFC 2833, the
+     * RTP payload type for DTMF is assigned dynamically, so it must be in the
+     * range of 96 and 127. One can use {@code -1} to disable DTMF and free up
+     * the previous assigned type. This method cannot be called when the stream
+     * already joined an {@link AudioGroup}.
+     *
+     * @param type The RTP payload type to be used or {@code -1} to disable it.
+     * @throws IllegalArgumentException if the type is invalid or used by codec.
+     * @throws IllegalStateException if the stream is busy.
+     * @see AudioGroup#sendDtmf(int)
+     */
+    public void setDtmfType(int type) {
+        if (isBusy()) {
+            throw new IllegalStateException("Busy");
+        }
+        if (type != -1) {
+            if (type < 96 || type > 127) {
+                throw new IllegalArgumentException("Invalid type");
+            }
+            if (mCodec != null && type == mCodec.type) {
+                throw new IllegalArgumentException("The type is used by codec");
+            }
+        }
+        mDtmfType = type;
+    }
+}
diff --git a/android/net/rtp/RtpStream.java b/android/net/rtp/RtpStream.java
new file mode 100644
index 0000000..b9d75cd
--- /dev/null
+++ b/android/net/rtp/RtpStream.java
@@ -0,0 +1,195 @@
+/*
+ * Copyright (C) 2010 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.rtp;
+
+import java.net.InetAddress;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.SocketException;
+
+/**
+ * RtpStream represents the base class of streams which send and receive network
+ * packets with media payloads over Real-time Transport Protocol (RTP).
+ *
+ * <p class="note">Using this class requires
+ * {@link android.Manifest.permission#INTERNET} permission.</p>
+ */
+public class RtpStream {
+    /**
+     * This mode indicates that the stream sends and receives packets at the
+     * same time. This is the initial mode for new streams.
+     */
+    public static final int MODE_NORMAL = 0;
+
+    /**
+     * This mode indicates that the stream only sends packets.
+     */
+    public static final int MODE_SEND_ONLY = 1;
+
+    /**
+     * This mode indicates that the stream only receives packets.
+     */
+    public static final int MODE_RECEIVE_ONLY = 2;
+
+    private static final int MODE_LAST = 2;
+
+    private final InetAddress mLocalAddress;
+    private final int mLocalPort;
+
+    private InetAddress mRemoteAddress;
+    private int mRemotePort = -1;
+    private int mMode = MODE_NORMAL;
+
+    private int mSocket = -1;
+    static {
+        System.loadLibrary("rtp_jni");
+    }
+
+    /**
+     * Creates a RtpStream on the given local address. Note that the local
+     * port is assigned automatically to conform with RFC 3550.
+     *
+     * @param address The network address of the local host to bind to.
+     * @throws SocketException if the address cannot be bound or a problem
+     *     occurs during binding.
+     */
+    RtpStream(InetAddress address) throws SocketException {
+        mLocalPort = create(address.getHostAddress());
+        mLocalAddress = address;
+    }
+
+    private native int create(String address) throws SocketException;
+
+    /**
+     * Returns the network address of the local host.
+     */
+    public InetAddress getLocalAddress() {
+        return mLocalAddress;
+    }
+
+    /**
+     * Returns the network port of the local host.
+     */
+    public int getLocalPort() {
+        return mLocalPort;
+    }
+
+    /**
+     * Returns the network address of the remote host or {@code null} if the
+     * stream is not associated.
+     */
+    public InetAddress getRemoteAddress() {
+        return mRemoteAddress;
+    }
+
+    /**
+     * Returns the network port of the remote host or {@code -1} if the stream
+     * is not associated.
+     */
+    public int getRemotePort() {
+        return mRemotePort;
+    }
+
+    /**
+     * Returns {@code true} if the stream is busy. In this case most of the
+     * setter methods are disabled. This method is intended to be overridden
+     * by subclasses.
+     */
+    public boolean isBusy() {
+        return false;
+    }
+
+    /**
+     * Returns the current mode.
+     */
+    public int getMode() {
+        return mMode;
+    }
+
+    /**
+     * Changes the current mode. It must be one of {@link #MODE_NORMAL},
+     * {@link #MODE_SEND_ONLY}, and {@link #MODE_RECEIVE_ONLY}.
+     *
+     * @param mode The mode to change to.
+     * @throws IllegalArgumentException if the mode is invalid.
+     * @throws IllegalStateException if the stream is busy.
+     * @see #isBusy()
+     */
+    public void setMode(int mode) {
+        if (isBusy()) {
+            throw new IllegalStateException("Busy");
+        }
+        if (mode < 0 || mode > MODE_LAST) {
+            throw new IllegalArgumentException("Invalid mode");
+        }
+        mMode = mode;
+    }
+
+    /**
+     * Associates with a remote host. This defines the destination of the
+     * outgoing packets.
+     *
+     * @param address The network address of the remote host.
+     * @param port The network port of the remote host.
+     * @throws IllegalArgumentException if the address is not supported or the
+     *     port is invalid.
+     * @throws IllegalStateException if the stream is busy.
+     * @see #isBusy()
+     */
+    public void associate(InetAddress address, int port) {
+        if (isBusy()) {
+            throw new IllegalStateException("Busy");
+        }
+        if (!(address instanceof Inet4Address && mLocalAddress instanceof Inet4Address) &&
+                !(address instanceof Inet6Address && mLocalAddress instanceof Inet6Address)) {
+            throw new IllegalArgumentException("Unsupported address");
+        }
+        if (port < 0 || port > 65535) {
+            throw new IllegalArgumentException("Invalid port");
+        }
+        mRemoteAddress = address;
+        mRemotePort = port;
+    }
+
+    int getSocket() {
+        return mSocket;
+    }
+
+    /**
+     * Releases allocated resources. The stream becomes inoperable after calling
+     * this method.
+     *
+     * @throws IllegalStateException if the stream is busy.
+     * @see #isBusy()
+     */
+    public void release() {
+        synchronized (this) {
+            if (isBusy()) {
+                throw new IllegalStateException("Busy");
+            }
+            close();
+        }
+    }
+
+    private native void close();
+
+    @Override
+    protected void finalize() throws Throwable {
+        close();
+        super.finalize();
+    }
+}
diff --git a/android/net/shared/Inet4AddressUtils.java b/android/net/shared/Inet4AddressUtils.java
new file mode 100644
index 0000000..bec0c84
--- /dev/null
+++ b/android/net/shared/Inet4AddressUtils.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2019 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.shared;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+/**
+ * Collection of utilities to work with IPv4 addresses.
+ * @hide
+ */
+public class Inet4AddressUtils {
+
+    /**
+     * Convert a IPv4 address from an integer to an InetAddress (0x04030201 -> 1.2.3.4)
+     *
+     * <p>This method uses the higher-order int bytes as the lower-order IPv4 address bytes,
+     * which is an unusual convention. Consider {@link #intToInet4AddressHTH(int)} instead.
+     * @param hostAddress an int coding for an IPv4 address, where higher-order int byte is
+     *                    lower-order IPv4 address byte
+     */
+    public static Inet4Address intToInet4AddressHTL(int hostAddress) {
+        return intToInet4AddressHTH(Integer.reverseBytes(hostAddress));
+    }
+
+    /**
+     * Convert a IPv4 address from an integer to an InetAddress (0x01020304 -> 1.2.3.4)
+     * @param hostAddress an int coding for an IPv4 address
+     */
+    public static Inet4Address intToInet4AddressHTH(int hostAddress) {
+        byte[] addressBytes = { (byte) (0xff & (hostAddress >> 24)),
+                (byte) (0xff & (hostAddress >> 16)),
+                (byte) (0xff & (hostAddress >> 8)),
+                (byte) (0xff & hostAddress) };
+
+        try {
+            return (Inet4Address) InetAddress.getByAddress(addressBytes);
+        } catch (UnknownHostException e) {
+            throw new AssertionError();
+        }
+    }
+
+    /**
+     * Convert an IPv4 address from an InetAddress to an integer (1.2.3.4 -> 0x01020304)
+     *
+     * <p>This conversion can help order IP addresses: considering the ordering
+     * 192.0.2.1 < 192.0.2.2 < ..., resulting ints will follow that ordering if read as unsigned
+     * integers with {@link Integer#toUnsignedLong}.
+     * @param inetAddr is an InetAddress corresponding to the IPv4 address
+     * @return the IP address as integer
+     */
+    public static int inet4AddressToIntHTH(Inet4Address inetAddr)
+            throws IllegalArgumentException {
+        byte [] addr = inetAddr.getAddress();
+        return ((addr[0] & 0xff) << 24) | ((addr[1] & 0xff) << 16)
+                | ((addr[2] & 0xff) << 8) | (addr[3] & 0xff);
+    }
+
+    /**
+     * Convert a IPv4 address from an InetAddress to an integer (1.2.3.4 -> 0x04030201)
+     *
+     * <p>This method stores the higher-order IPv4 address bytes in the lower-order int bytes,
+     * which is an unusual convention. Consider {@link #inet4AddressToIntHTH(Inet4Address)} instead.
+     * @param inetAddr is an InetAddress corresponding to the IPv4 address
+     * @return the IP address as integer
+     */
+    public static int inet4AddressToIntHTL(Inet4Address inetAddr) {
+        return Integer.reverseBytes(inet4AddressToIntHTH(inetAddr));
+    }
+
+    /**
+     * Convert a network prefix length to an IPv4 netmask integer (prefixLength 17 -> 0xffff8000)
+     * @return the IPv4 netmask as an integer
+     */
+    public static int prefixLengthToV4NetmaskIntHTH(int prefixLength)
+            throws IllegalArgumentException {
+        if (prefixLength < 0 || prefixLength > 32) {
+            throw new IllegalArgumentException("Invalid prefix length (0 <= prefix <= 32)");
+        }
+        // (int)a << b is equivalent to a << (b & 0x1f): can't shift by 32 (-1 << 32 == -1)
+        return prefixLength == 0 ? 0 : 0xffffffff << (32 - prefixLength);
+    }
+
+    /**
+     * Convert a network prefix length to an IPv4 netmask integer (prefixLength 17 -> 0x0080ffff).
+     *
+     * <p>This method stores the higher-order IPv4 address bytes in the lower-order int bytes,
+     * which is an unusual convention. Consider {@link #prefixLengthToV4NetmaskIntHTH(int)} instead.
+     * @return the IPv4 netmask as an integer
+     */
+    public static int prefixLengthToV4NetmaskIntHTL(int prefixLength)
+            throws IllegalArgumentException {
+        return Integer.reverseBytes(prefixLengthToV4NetmaskIntHTH(prefixLength));
+    }
+
+    /**
+     * Convert an IPv4 netmask to a prefix length, checking that the netmask is contiguous.
+     * @param netmask as a {@code Inet4Address}.
+     * @return the network prefix length
+     * @throws IllegalArgumentException the specified netmask was not contiguous.
+     * @hide
+     */
+    public static int netmaskToPrefixLength(Inet4Address netmask) {
+        // inetAddressToInt returns an int in *network* byte order.
+        int i = inet4AddressToIntHTH(netmask);
+        int prefixLength = Integer.bitCount(i);
+        int trailingZeros = Integer.numberOfTrailingZeros(i);
+        if (trailingZeros != 32 - prefixLength) {
+            throw new IllegalArgumentException("Non-contiguous netmask: " + Integer.toHexString(i));
+        }
+        return prefixLength;
+    }
+
+    /**
+     * Returns the implicit netmask of an IPv4 address, as was the custom before 1993.
+     */
+    public static int getImplicitNetmask(Inet4Address address) {
+        int firstByte = address.getAddress()[0] & 0xff;  // Convert to an unsigned value.
+        if (firstByte < 128) {
+            return 8;
+        } else if (firstByte < 192) {
+            return 16;
+        } else if (firstByte < 224) {
+            return 24;
+        } else {
+            return 32;  // Will likely not end well for other reasons.
+        }
+    }
+
+    /**
+     * Get the broadcast address for a given prefix.
+     *
+     * <p>For example 192.168.0.1/24 -> 192.168.0.255
+     */
+    public static Inet4Address getBroadcastAddress(Inet4Address addr, int prefixLength)
+            throws IllegalArgumentException {
+        final int intBroadcastAddr = inet4AddressToIntHTH(addr)
+                | ~prefixLengthToV4NetmaskIntHTH(prefixLength);
+        return intToInet4AddressHTH(intBroadcastAddr);
+    }
+
+    /**
+     * Get a prefix mask as Inet4Address for a given prefix length.
+     *
+     * <p>For example 20 -> 255.255.240.0
+     */
+    public static Inet4Address getPrefixMaskAsInet4Address(int prefixLength)
+            throws IllegalArgumentException {
+        return intToInet4AddressHTH(prefixLengthToV4NetmaskIntHTH(prefixLength));
+    }
+}
diff --git a/android/net/shared/InetAddressUtils.java b/android/net/shared/InetAddressUtils.java
new file mode 100644
index 0000000..c9ee3a7
--- /dev/null
+++ b/android/net/shared/InetAddressUtils.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2012 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.shared;
+
+import android.os.Parcel;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+/**
+ * Collection of utilities to interact with {@link InetAddress}
+ * @hide
+ */
+public class InetAddressUtils {
+
+    /**
+     * Writes an InetAddress to a parcel. The address may be null. This is likely faster than
+     * calling writeSerializable.
+     * @hide
+     */
+    public static void parcelInetAddress(Parcel parcel, InetAddress address, int flags) {
+        byte[] addressArray = (address != null) ? address.getAddress() : null;
+        parcel.writeByteArray(addressArray);
+    }
+
+    /**
+     * Reads an InetAddress from a parcel. Returns null if the address that was written was null
+     * or if the data is invalid.
+     * @hide
+     */
+    public static InetAddress unparcelInetAddress(Parcel in) {
+        byte[] addressArray = in.createByteArray();
+        if (addressArray == null) {
+            return null;
+        }
+        try {
+            return InetAddress.getByAddress(addressArray);
+        } catch (UnknownHostException e) {
+            return null;
+        }
+    }
+
+    private InetAddressUtils() {}
+}
diff --git a/android/net/shared/InitialConfiguration.java b/android/net/shared/InitialConfiguration.java
new file mode 100644
index 0000000..007c8ca
--- /dev/null
+++ b/android/net/shared/InitialConfiguration.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright (C) 2019 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.shared;
+
+import static android.net.shared.ParcelableUtil.fromParcelableArray;
+import static android.net.shared.ParcelableUtil.toParcelableArray;
+import static android.text.TextUtils.join;
+
+import android.net.InetAddresses;
+import android.net.InitialConfigurationParcelable;
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.RouteInfo;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Predicate;
+
+/** @hide */
+public class InitialConfiguration {
+    public final Set<LinkAddress> ipAddresses = new HashSet<>();
+    public final Set<IpPrefix> directlyConnectedRoutes = new HashSet<>();
+    public final Set<InetAddress> dnsServers = new HashSet<>();
+
+    private static final int RFC6177_MIN_PREFIX_LENGTH = 48;
+    private static final int RFC7421_PREFIX_LENGTH = 64;
+
+    public static final InetAddress INET6_ANY = InetAddresses.parseNumericAddress("::");
+
+    /**
+     * Create a InitialConfiguration that is a copy of the specified configuration.
+     */
+    public static InitialConfiguration copy(InitialConfiguration config) {
+        if (config == null) {
+            return null;
+        }
+        InitialConfiguration configCopy = new InitialConfiguration();
+        configCopy.ipAddresses.addAll(config.ipAddresses);
+        configCopy.directlyConnectedRoutes.addAll(config.directlyConnectedRoutes);
+        configCopy.dnsServers.addAll(config.dnsServers);
+        return configCopy;
+    }
+
+    @Override
+    public String toString() {
+        return String.format(
+                "InitialConfiguration(IPs: {%s}, prefixes: {%s}, DNS: {%s})",
+                join(", ", ipAddresses), join(", ", directlyConnectedRoutes),
+                join(", ", dnsServers));
+    }
+
+    /**
+     * Tests whether the contents of this IpConfiguration represent a valid configuration.
+     */
+    public boolean isValid() {
+        if (ipAddresses.isEmpty()) {
+            return false;
+        }
+
+        // For every IP address, there must be at least one prefix containing that address.
+        for (LinkAddress addr : ipAddresses) {
+            if (!any(directlyConnectedRoutes, (p) -> p.contains(addr.getAddress()))) {
+                return false;
+            }
+        }
+        // For every dns server, there must be at least one prefix containing that address.
+        for (InetAddress addr : dnsServers) {
+            if (!any(directlyConnectedRoutes, (p) -> p.contains(addr))) {
+                return false;
+            }
+        }
+        // All IPv6 LinkAddresses have an RFC7421-suitable prefix length
+        // (read: compliant with RFC4291#section2.5.4).
+        if (any(ipAddresses, not(InitialConfiguration::isPrefixLengthCompliant))) {
+            return false;
+        }
+        // If directlyConnectedRoutes contains an IPv6 default route
+        // then ipAddresses MUST contain at least one non-ULA GUA.
+        if (any(directlyConnectedRoutes, InitialConfiguration::isIPv6DefaultRoute)
+                && all(ipAddresses, not(InitialConfiguration::isIPv6GUA))) {
+            return false;
+        }
+        // The prefix length of routes in directlyConnectedRoutes be within reasonable
+        // bounds for IPv6: /48-/64 just as we’d accept in RIOs.
+        if (any(directlyConnectedRoutes, not(InitialConfiguration::isPrefixLengthCompliant))) {
+            return false;
+        }
+        // There no more than one IPv4 address
+        if (ipAddresses.stream().filter(InitialConfiguration::isIPv4).count() > 1) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * @return true if the given list of addressess and routes satisfies provisioning for this
+     * InitialConfiguration. LinkAddresses and RouteInfo objects are not compared with equality
+     * because addresses and routes seen by Netlink will contain additional fields like flags,
+     * interfaces, and so on. If this InitialConfiguration has no IP address specified, the
+     * provisioning check always fails.
+     *
+     * If the given list of routes is null, only addresses are taken into considerations.
+     */
+    public boolean isProvisionedBy(List<LinkAddress> addresses, List<RouteInfo> routes) {
+        if (ipAddresses.isEmpty()) {
+            return false;
+        }
+
+        for (LinkAddress addr : ipAddresses) {
+            if (!any(addresses, (addrSeen) -> addr.isSameAddressAs(addrSeen))) {
+                return false;
+            }
+        }
+
+        if (routes != null) {
+            for (IpPrefix prefix : directlyConnectedRoutes) {
+                if (!any(routes, (routeSeen) -> isDirectlyConnectedRoute(routeSeen, prefix))) {
+                    return false;
+                }
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Convert this configuration to a {@link InitialConfigurationParcelable}.
+     */
+    public InitialConfigurationParcelable toStableParcelable() {
+        final InitialConfigurationParcelable p = new InitialConfigurationParcelable();
+        p.ipAddresses = ipAddresses.toArray(new LinkAddress[0]);
+        p.directlyConnectedRoutes = directlyConnectedRoutes.toArray(new IpPrefix[0]);
+        p.dnsServers = toParcelableArray(
+                dnsServers, IpConfigurationParcelableUtil::parcelAddress, String.class);
+        return p;
+    }
+
+    /**
+     * Create an instance of {@link InitialConfiguration} based on the contents of the specified
+     * {@link InitialConfigurationParcelable}.
+     */
+    public static InitialConfiguration fromStableParcelable(InitialConfigurationParcelable p) {
+        if (p == null) return null;
+        final InitialConfiguration config = new InitialConfiguration();
+        config.ipAddresses.addAll(Arrays.asList(p.ipAddresses));
+        config.directlyConnectedRoutes.addAll(Arrays.asList(p.directlyConnectedRoutes));
+        config.dnsServers.addAll(
+                fromParcelableArray(p.dnsServers, IpConfigurationParcelableUtil::unparcelAddress));
+        return config;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof InitialConfiguration)) return false;
+        final InitialConfiguration other = (InitialConfiguration) obj;
+        return ipAddresses.equals(other.ipAddresses)
+                && directlyConnectedRoutes.equals(other.directlyConnectedRoutes)
+                && dnsServers.equals(other.dnsServers);
+    }
+
+    private static boolean isDirectlyConnectedRoute(RouteInfo route, IpPrefix prefix) {
+        return !route.hasGateway() && prefix.equals(route.getDestination());
+    }
+
+    private static boolean isPrefixLengthCompliant(LinkAddress addr) {
+        return isIPv4(addr) || isCompliantIPv6PrefixLength(addr.getPrefixLength());
+    }
+
+    private static boolean isPrefixLengthCompliant(IpPrefix prefix) {
+        return isIPv4(prefix) || isCompliantIPv6PrefixLength(prefix.getPrefixLength());
+    }
+
+    private static boolean isCompliantIPv6PrefixLength(int prefixLength) {
+        return (RFC6177_MIN_PREFIX_LENGTH <= prefixLength)
+                && (prefixLength <= RFC7421_PREFIX_LENGTH);
+    }
+
+    private static boolean isIPv4(IpPrefix prefix) {
+        return prefix.getAddress() instanceof Inet4Address;
+    }
+
+    private static boolean isIPv4(LinkAddress addr) {
+        return addr.getAddress() instanceof Inet4Address;
+    }
+
+    private static boolean isIPv6DefaultRoute(IpPrefix prefix) {
+        return prefix.getAddress().equals(INET6_ANY);
+    }
+
+    private static boolean isIPv6GUA(LinkAddress addr) {
+        return addr.isIpv6() && addr.isGlobalPreferred();
+    }
+
+    // TODO: extract out into CollectionUtils.
+
+    /**
+     * Indicate whether any element of the specified iterable verifies the specified predicate.
+     */
+    public static <T> boolean any(Iterable<T> coll, Predicate<T> fn) {
+        for (T t : coll) {
+            if (fn.test(t)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Indicate whether all elements of the specified iterable verifies the specified predicate.
+     */
+    public static <T> boolean all(Iterable<T> coll, Predicate<T> fn) {
+        return !any(coll, not(fn));
+    }
+
+    /**
+     * Create a predicate that returns the opposite value of the specified predicate.
+     */
+    public static <T> Predicate<T> not(Predicate<T> fn) {
+        return (t) -> !fn.test(t);
+    }
+}
diff --git a/android/net/shared/IpConfigurationParcelableUtil.java b/android/net/shared/IpConfigurationParcelableUtil.java
new file mode 100644
index 0000000..172dc24
--- /dev/null
+++ b/android/net/shared/IpConfigurationParcelableUtil.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2019 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.shared;
+
+import android.annotation.Nullable;
+import android.net.DhcpResults;
+import android.net.DhcpResultsParcelable;
+import android.net.InetAddresses;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+
+/**
+ * Collection of utility methods to convert to and from stable AIDL parcelables for IpClient
+ * configuration classes.
+ * @hide
+ */
+public final class IpConfigurationParcelableUtil {
+    /**
+     * Convert DhcpResults to a DhcpResultsParcelable.
+     */
+    public static DhcpResultsParcelable toStableParcelable(@Nullable DhcpResults results) {
+        if (results == null) return null;
+        final DhcpResultsParcelable p = new DhcpResultsParcelable();
+        p.baseConfiguration = results.toStaticIpConfiguration();
+        p.leaseDuration = results.leaseDuration;
+        p.mtu = results.mtu;
+        p.serverAddress = parcelAddress(results.serverAddress);
+        p.vendorInfo = results.vendorInfo;
+        p.serverHostName = results.serverHostName;
+        return p;
+    }
+
+    /**
+     * Convert a DhcpResultsParcelable to DhcpResults.
+     */
+    public static DhcpResults fromStableParcelable(@Nullable DhcpResultsParcelable p) {
+        if (p == null) return null;
+        final DhcpResults results = new DhcpResults(p.baseConfiguration);
+        results.leaseDuration = p.leaseDuration;
+        results.mtu = p.mtu;
+        results.serverAddress = (Inet4Address) unparcelAddress(p.serverAddress);
+        results.vendorInfo = p.vendorInfo;
+        results.serverHostName = p.serverHostName;
+        return results;
+    }
+
+    /**
+     * Convert InetAddress to String.
+     * TODO: have an InetAddressParcelable
+     */
+    public static String parcelAddress(@Nullable InetAddress addr) {
+        if (addr == null) return null;
+        return addr.getHostAddress();
+    }
+
+    /**
+     * Convert String to InetAddress.
+     * TODO: have an InetAddressParcelable
+     */
+    public static InetAddress unparcelAddress(@Nullable String addr) {
+        if (addr == null) return null;
+        return InetAddresses.parseNumericAddress(addr);
+    }
+}
diff --git a/android/net/shared/LinkPropertiesParcelableUtil.java b/android/net/shared/LinkPropertiesParcelableUtil.java
new file mode 100644
index 0000000..1729da6
--- /dev/null
+++ b/android/net/shared/LinkPropertiesParcelableUtil.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2019 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.shared;
+
+import android.annotation.Nullable;
+import android.net.LinkProperties;
+import android.net.ProxyInfo;
+
+/**
+ * Collection of utility methods to convert to and from stable AIDL parcelables for LinkProperties
+ * and its attributes.
+ * @hide
+ */
+public final class LinkPropertiesParcelableUtil {
+    // Temporary methods to facilitate migrating clients away from LinkPropertiesParcelable
+    // TODO: remove the following methods after migrating clients.
+
+    /**
+     * @deprecated conversion to stable parcelable is no longer necessary.
+     */
+    @Deprecated
+    public static LinkProperties toStableParcelable(@Nullable LinkProperties lp) {
+        return lp;
+    }
+
+    /**
+     * @deprecated conversion to stable parcelable is no longer necessary.
+     */
+    @Deprecated
+    public static ProxyInfo toStableParcelable(@Nullable ProxyInfo info) {
+        return info;
+    }
+}
diff --git a/android/net/shared/NetworkMonitorUtils.java b/android/net/shared/NetworkMonitorUtils.java
new file mode 100644
index 0000000..46e9c73
--- /dev/null
+++ b/android/net/shared/NetworkMonitorUtils.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2018 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.shared;
+
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_NOT_VPN;
+import static android.net.NetworkCapabilities.NET_CAPABILITY_TRUSTED;
+
+import android.net.NetworkCapabilities;
+
+/** @hide */
+public class NetworkMonitorUtils {
+
+    // Network conditions broadcast constants
+    public static final String ACTION_NETWORK_CONDITIONS_MEASURED =
+            "android.net.conn.NETWORK_CONDITIONS_MEASURED";
+    public static final String EXTRA_CONNECTIVITY_TYPE = "extra_connectivity_type";
+    public static final String EXTRA_NETWORK_TYPE = "extra_network_type";
+    public static final String EXTRA_RESPONSE_RECEIVED = "extra_response_received";
+    public static final String EXTRA_IS_CAPTIVE_PORTAL = "extra_is_captive_portal";
+    public static final String EXTRA_CELL_ID = "extra_cellid";
+    public static final String EXTRA_SSID = "extra_ssid";
+    public static final String EXTRA_BSSID = "extra_bssid";
+    /** real time since boot */
+    public static final String EXTRA_REQUEST_TIMESTAMP_MS = "extra_request_timestamp_ms";
+    public static final String EXTRA_RESPONSE_TIMESTAMP_MS = "extra_response_timestamp_ms";
+    public static final String PERMISSION_ACCESS_NETWORK_CONDITIONS =
+            "android.permission.ACCESS_NETWORK_CONDITIONS";
+
+    /**
+     * Return whether validation is required for private DNS in strict mode.
+     * @param nc Network capabilities of the network to test.
+     */
+    public static boolean isPrivateDnsValidationRequired(NetworkCapabilities nc) {
+        // TODO: Consider requiring validation for DUN networks.
+        return nc != null
+                && nc.hasCapability(NET_CAPABILITY_INTERNET)
+                && nc.hasCapability(NET_CAPABILITY_NOT_RESTRICTED)
+                && nc.hasCapability(NET_CAPABILITY_TRUSTED);
+    }
+
+    /**
+     * Return whether validation is required for a network.
+     * @param nc Network capabilities of the network to test.
+     */
+    public static boolean isValidationRequired(NetworkCapabilities nc) {
+        // TODO: Consider requiring validation for DUN networks.
+        return isPrivateDnsValidationRequired(nc) && nc.hasCapability(NET_CAPABILITY_NOT_VPN);
+    }
+}
diff --git a/android/net/shared/ParcelableUtil.java b/android/net/shared/ParcelableUtil.java
new file mode 100644
index 0000000..3f40300
--- /dev/null
+++ b/android/net/shared/ParcelableUtil.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2019 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.shared;
+
+import android.annotation.NonNull;
+
+import java.lang.reflect.Array;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.function.Function;
+
+/**
+ * Utility methods to help convert to/from stable parcelables.
+ * @hide
+ */
+public final class ParcelableUtil {
+    // Below methods could be implemented easily with streams, but streams are frowned upon in
+    // frameworks code.
+
+    /**
+     * Convert a list of BaseType items to an array of ParcelableType items using the specified
+     * converter function.
+     */
+    public static <ParcelableType, BaseType> ParcelableType[] toParcelableArray(
+            @NonNull Collection<BaseType> base,
+            @NonNull Function<BaseType, ParcelableType> conv,
+            @NonNull Class<ParcelableType> parcelClass) {
+        final ParcelableType[] out = (ParcelableType[]) Array.newInstance(parcelClass, base.size());
+        int i = 0;
+        for (BaseType b : base) {
+            out[i] = conv.apply(b);
+            i++;
+        }
+        return out;
+    }
+
+    /**
+     * Convert an array of ParcelableType items to a list of BaseType items using the specified
+     * converter function.
+     */
+    public static <ParcelableType, BaseType> ArrayList<BaseType> fromParcelableArray(
+            @NonNull ParcelableType[] parceled, @NonNull Function<ParcelableType, BaseType> conv) {
+        final ArrayList<BaseType> out = new ArrayList<>(parceled.length);
+        for (ParcelableType t : parceled) {
+            out.add(conv.apply(t));
+        }
+        return out;
+    }
+}
diff --git a/android/net/shared/PrivateDnsConfig.java b/android/net/shared/PrivateDnsConfig.java
new file mode 100644
index 0000000..c7dc530
--- /dev/null
+++ b/android/net/shared/PrivateDnsConfig.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2018 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.shared;
+
+import static android.net.shared.ParcelableUtil.fromParcelableArray;
+import static android.net.shared.ParcelableUtil.toParcelableArray;
+
+import android.net.PrivateDnsConfigParcel;
+import android.text.TextUtils;
+
+import java.net.InetAddress;
+import java.util.Arrays;
+
+/** @hide */
+public class PrivateDnsConfig {
+    public final boolean useTls;
+    public final String hostname;
+    public final InetAddress[] ips;
+
+    public PrivateDnsConfig() {
+        this(false);
+    }
+
+    public PrivateDnsConfig(boolean useTls) {
+        this.useTls = useTls;
+        this.hostname = "";
+        this.ips = new InetAddress[0];
+    }
+
+    public PrivateDnsConfig(String hostname, InetAddress[] ips) {
+        this.useTls = !TextUtils.isEmpty(hostname);
+        this.hostname = useTls ? hostname : "";
+        this.ips = (ips != null) ? ips : new InetAddress[0];
+    }
+
+    public PrivateDnsConfig(PrivateDnsConfig cfg) {
+        useTls = cfg.useTls;
+        hostname = cfg.hostname;
+        ips = cfg.ips;
+    }
+
+    /**
+     * Indicates whether this is a strict mode private DNS configuration.
+     */
+    public boolean inStrictMode() {
+        return useTls && !TextUtils.isEmpty(hostname);
+    }
+
+    @Override
+    public String toString() {
+        return PrivateDnsConfig.class.getSimpleName()
+                + "{" + useTls + ":" + hostname + "/" + Arrays.toString(ips) + "}";
+    }
+
+    /**
+     * Create a stable AIDL-compatible parcel from the current instance.
+     */
+    public PrivateDnsConfigParcel toParcel() {
+        final PrivateDnsConfigParcel parcel = new PrivateDnsConfigParcel();
+        parcel.hostname = hostname;
+        parcel.ips = toParcelableArray(
+                Arrays.asList(ips), IpConfigurationParcelableUtil::parcelAddress, String.class);
+
+        return parcel;
+    }
+
+    /**
+     * Build a configuration from a stable AIDL-compatible parcel.
+     */
+    public static PrivateDnsConfig fromParcel(PrivateDnsConfigParcel parcel) {
+        InetAddress[] ips = new InetAddress[parcel.ips.length];
+        ips = fromParcelableArray(parcel.ips, IpConfigurationParcelableUtil::unparcelAddress)
+                .toArray(ips);
+        return new PrivateDnsConfig(parcel.hostname, ips);
+    }
+}
diff --git a/android/net/shared/ProvisioningConfiguration.java b/android/net/shared/ProvisioningConfiguration.java
new file mode 100644
index 0000000..6f9c294
--- /dev/null
+++ b/android/net/shared/ProvisioningConfiguration.java
@@ -0,0 +1,312 @@
+/*
+ * Copyright (C) 2019 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.shared;
+
+import android.annotation.Nullable;
+import android.net.INetd;
+import android.net.Network;
+import android.net.ProvisioningConfigurationParcelable;
+import android.net.StaticIpConfiguration;
+import android.net.apf.ApfCapabilities;
+import android.net.ip.IIpClient;
+
+import java.util.Objects;
+import java.util.StringJoiner;
+
+/**
+ * This class encapsulates parameters to be passed to
+ * IpClient#startProvisioning(). A defensive copy is made by IpClient
+ * and the values specified herein are in force until IpClient#stop()
+ * is called.
+ *
+ * Example use:
+ *
+ *     final ProvisioningConfiguration config =
+ *             new ProvisioningConfiguration.Builder()
+ *                     .withPreDhcpAction()
+ *                     .withProvisioningTimeoutMs(36 * 1000)
+ *                     .build();
+ *     mIpClient.startProvisioning(config.toStableParcelable());
+ *     ...
+ *     mIpClient.stop();
+ *
+ * The specified provisioning configuration will only be active until
+ * IIpClient#stop() is called. Future calls to IIpClient#startProvisioning()
+ * must specify the configuration again.
+ * @hide
+ */
+public class ProvisioningConfiguration {
+    // TODO: Delete this default timeout once those callers that care are
+    // fixed to pass in their preferred timeout.
+    //
+    // We pick 36 seconds so we can send DHCP requests at
+    //
+    //     t=0, t=2, t=6, t=14, t=30
+    //
+    // allowing for 10% jitter.
+    private static final int DEFAULT_TIMEOUT_MS = 36 * 1000;
+
+    /**
+     * Builder to create a {@link ProvisioningConfiguration}.
+     */
+    public static class Builder {
+        protected ProvisioningConfiguration mConfig = new ProvisioningConfiguration();
+
+        /**
+         * Specify that the configuration should not enable IPv4. It is enabled by default.
+         */
+        public Builder withoutIPv4() {
+            mConfig.mEnableIPv4 = false;
+            return this;
+        }
+
+        /**
+         * Specify that the configuration should not enable IPv6. It is enabled by default.
+         */
+        public Builder withoutIPv6() {
+            mConfig.mEnableIPv6 = false;
+            return this;
+        }
+
+        /**
+         * Specify that the configuration should not use a MultinetworkPolicyTracker. It is used
+         * by default.
+         */
+        public Builder withoutMultinetworkPolicyTracker() {
+            mConfig.mUsingMultinetworkPolicyTracker = false;
+            return this;
+        }
+
+        /**
+         * Specify that the configuration should not use a IpReachabilityMonitor. It is used by
+         * default.
+         */
+        public Builder withoutIpReachabilityMonitor() {
+            mConfig.mUsingIpReachabilityMonitor = false;
+            return this;
+        }
+
+        /**
+         * Identical to {@link #withPreDhcpAction(int)}, using a default timeout.
+         * @see #withPreDhcpAction(int)
+         */
+        public Builder withPreDhcpAction() {
+            mConfig.mRequestedPreDhcpActionMs = DEFAULT_TIMEOUT_MS;
+            return this;
+        }
+
+        /**
+         * Specify that {@link IpClientCallbacks#onPreDhcpAction()} should be called. Clients must
+         * call {@link IIpClient#completedPreDhcpAction()} when the callback called. This behavior
+         * is disabled by default.
+         * @param dhcpActionTimeoutMs Timeout for clients to call completedPreDhcpAction().
+         */
+        public Builder withPreDhcpAction(int dhcpActionTimeoutMs) {
+            mConfig.mRequestedPreDhcpActionMs = dhcpActionTimeoutMs;
+            return this;
+        }
+
+        /**
+         * Specify the initial provisioning configuration.
+         */
+        public Builder withInitialConfiguration(InitialConfiguration initialConfig) {
+            mConfig.mInitialConfig = initialConfig;
+            return this;
+        }
+
+        /**
+         * Specify a static configuration for provisioning.
+         */
+        public Builder withStaticConfiguration(StaticIpConfiguration staticConfig) {
+            mConfig.mStaticIpConfig = staticConfig;
+            return this;
+        }
+
+        /**
+         * Specify ApfCapabilities.
+         */
+        public Builder withApfCapabilities(ApfCapabilities apfCapabilities) {
+            mConfig.mApfCapabilities = apfCapabilities;
+            return this;
+        }
+
+        /**
+         * Specify the timeout to use for provisioning.
+         */
+        public Builder withProvisioningTimeoutMs(int timeoutMs) {
+            mConfig.mProvisioningTimeoutMs = timeoutMs;
+            return this;
+        }
+
+        /**
+         * Specify that IPv6 address generation should use a random MAC address.
+         */
+        public Builder withRandomMacAddress() {
+            mConfig.mIPv6AddrGenMode = INetd.IPV6_ADDR_GEN_MODE_EUI64;
+            return this;
+        }
+
+        /**
+         * Specify that IPv6 address generation should use a stable MAC address.
+         */
+        public Builder withStableMacAddress() {
+            mConfig.mIPv6AddrGenMode = INetd.IPV6_ADDR_GEN_MODE_STABLE_PRIVACY;
+            return this;
+        }
+
+        /**
+         * Specify the network to use for provisioning.
+         */
+        public Builder withNetwork(Network network) {
+            mConfig.mNetwork = network;
+            return this;
+        }
+
+        /**
+         * Specify the display name that the IpClient should use.
+         */
+        public Builder withDisplayName(String displayName) {
+            mConfig.mDisplayName = displayName;
+            return this;
+        }
+
+        /**
+         * Build the configuration using previously specified parameters.
+         */
+        public ProvisioningConfiguration build() {
+            return new ProvisioningConfiguration(mConfig);
+        }
+    }
+
+    public boolean mEnableIPv4 = true;
+    public boolean mEnableIPv6 = true;
+    public boolean mUsingMultinetworkPolicyTracker = true;
+    public boolean mUsingIpReachabilityMonitor = true;
+    public int mRequestedPreDhcpActionMs;
+    public InitialConfiguration mInitialConfig;
+    public StaticIpConfiguration mStaticIpConfig;
+    public ApfCapabilities mApfCapabilities;
+    public int mProvisioningTimeoutMs = DEFAULT_TIMEOUT_MS;
+    public int mIPv6AddrGenMode = INetd.IPV6_ADDR_GEN_MODE_STABLE_PRIVACY;
+    public Network mNetwork = null;
+    public String mDisplayName = null;
+
+    public ProvisioningConfiguration() {} // used by Builder
+
+    public ProvisioningConfiguration(ProvisioningConfiguration other) {
+        mEnableIPv4 = other.mEnableIPv4;
+        mEnableIPv6 = other.mEnableIPv6;
+        mUsingMultinetworkPolicyTracker = other.mUsingMultinetworkPolicyTracker;
+        mUsingIpReachabilityMonitor = other.mUsingIpReachabilityMonitor;
+        mRequestedPreDhcpActionMs = other.mRequestedPreDhcpActionMs;
+        mInitialConfig = InitialConfiguration.copy(other.mInitialConfig);
+        mStaticIpConfig = other.mStaticIpConfig == null
+                ? null
+                : new StaticIpConfiguration(other.mStaticIpConfig);
+        mApfCapabilities = other.mApfCapabilities;
+        mProvisioningTimeoutMs = other.mProvisioningTimeoutMs;
+        mIPv6AddrGenMode = other.mIPv6AddrGenMode;
+        mNetwork = other.mNetwork;
+        mDisplayName = other.mDisplayName;
+    }
+
+    /**
+     * Create a ProvisioningConfigurationParcelable from this ProvisioningConfiguration.
+     */
+    public ProvisioningConfigurationParcelable toStableParcelable() {
+        final ProvisioningConfigurationParcelable p = new ProvisioningConfigurationParcelable();
+        p.enableIPv4 = mEnableIPv4;
+        p.enableIPv6 = mEnableIPv6;
+        p.usingMultinetworkPolicyTracker = mUsingMultinetworkPolicyTracker;
+        p.usingIpReachabilityMonitor = mUsingIpReachabilityMonitor;
+        p.requestedPreDhcpActionMs = mRequestedPreDhcpActionMs;
+        p.initialConfig = mInitialConfig == null ? null : mInitialConfig.toStableParcelable();
+        p.staticIpConfig = mStaticIpConfig == null
+                ? null
+                : new StaticIpConfiguration(mStaticIpConfig);
+        p.apfCapabilities = mApfCapabilities; // ApfCapabilities is immutable
+        p.provisioningTimeoutMs = mProvisioningTimeoutMs;
+        p.ipv6AddrGenMode = mIPv6AddrGenMode;
+        p.network = mNetwork;
+        p.displayName = mDisplayName;
+        return p;
+    }
+
+    /**
+     * Create a ProvisioningConfiguration from a ProvisioningConfigurationParcelable.
+     */
+    public static ProvisioningConfiguration fromStableParcelable(
+            @Nullable ProvisioningConfigurationParcelable p) {
+        if (p == null) return null;
+        final ProvisioningConfiguration config = new ProvisioningConfiguration();
+        config.mEnableIPv4 = p.enableIPv4;
+        config.mEnableIPv6 = p.enableIPv6;
+        config.mUsingMultinetworkPolicyTracker = p.usingMultinetworkPolicyTracker;
+        config.mUsingIpReachabilityMonitor = p.usingIpReachabilityMonitor;
+        config.mRequestedPreDhcpActionMs = p.requestedPreDhcpActionMs;
+        config.mInitialConfig = InitialConfiguration.fromStableParcelable(p.initialConfig);
+        config.mStaticIpConfig = p.staticIpConfig == null
+                ? null
+                : new StaticIpConfiguration(p.staticIpConfig);
+        config.mApfCapabilities = p.apfCapabilities; // ApfCapabilities is immutable
+        config.mProvisioningTimeoutMs = p.provisioningTimeoutMs;
+        config.mIPv6AddrGenMode = p.ipv6AddrGenMode;
+        config.mNetwork = p.network;
+        config.mDisplayName = p.displayName;
+        return config;
+    }
+
+    @Override
+    public String toString() {
+        return new StringJoiner(", ", getClass().getSimpleName() + "{", "}")
+                .add("mEnableIPv4: " + mEnableIPv4)
+                .add("mEnableIPv6: " + mEnableIPv6)
+                .add("mUsingMultinetworkPolicyTracker: " + mUsingMultinetworkPolicyTracker)
+                .add("mUsingIpReachabilityMonitor: " + mUsingIpReachabilityMonitor)
+                .add("mRequestedPreDhcpActionMs: " + mRequestedPreDhcpActionMs)
+                .add("mInitialConfig: " + mInitialConfig)
+                .add("mStaticIpConfig: " + mStaticIpConfig)
+                .add("mApfCapabilities: " + mApfCapabilities)
+                .add("mProvisioningTimeoutMs: " + mProvisioningTimeoutMs)
+                .add("mIPv6AddrGenMode: " + mIPv6AddrGenMode)
+                .add("mNetwork: " + mNetwork)
+                .add("mDisplayName: " + mDisplayName)
+                .toString();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof ProvisioningConfiguration)) return false;
+        final ProvisioningConfiguration other = (ProvisioningConfiguration) obj;
+        return mEnableIPv4 == other.mEnableIPv4
+                && mEnableIPv6 == other.mEnableIPv6
+                && mUsingMultinetworkPolicyTracker == other.mUsingMultinetworkPolicyTracker
+                && mUsingIpReachabilityMonitor == other.mUsingIpReachabilityMonitor
+                && mRequestedPreDhcpActionMs == other.mRequestedPreDhcpActionMs
+                && Objects.equals(mInitialConfig, other.mInitialConfig)
+                && Objects.equals(mStaticIpConfig, other.mStaticIpConfig)
+                && Objects.equals(mApfCapabilities, other.mApfCapabilities)
+                && mProvisioningTimeoutMs == other.mProvisioningTimeoutMs
+                && mIPv6AddrGenMode == other.mIPv6AddrGenMode
+                && Objects.equals(mNetwork, other.mNetwork)
+                && Objects.equals(mDisplayName, other.mDisplayName);
+    }
+
+    public boolean isValid() {
+        return (mInitialConfig == null) || mInitialConfig.isValid();
+    }
+}
diff --git a/android/net/sip/SimpleSessionDescription.java b/android/net/sip/SimpleSessionDescription.java
new file mode 100644
index 0000000..9fcd21d
--- /dev/null
+++ b/android/net/sip/SimpleSessionDescription.java
@@ -0,0 +1,613 @@
+/*
+ * Copyright (C) 2010 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.sip;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Locale;
+
+/**
+ * An object used to manipulate messages of Session Description Protocol (SDP).
+ * It is mainly designed for the uses of Session Initiation Protocol (SIP).
+ * Therefore, it only handles connection addresses ("c="), bandwidth limits,
+ * ("b="), encryption keys ("k="), and attribute fields ("a="). Currently this
+ * implementation does not support multicast sessions.
+ *
+ * <p>Here is an example code to create a session description.</p>
+ * <pre>
+ * SimpleSessionDescription description = new SimpleSessionDescription(
+ *     System.currentTimeMillis(), "1.2.3.4");
+ * Media media = description.newMedia("audio", 56789, 1, "RTP/AVP");
+ * media.setRtpPayload(0, "PCMU/8000", null);
+ * media.setRtpPayload(8, "PCMA/8000", null);
+ * media.setRtpPayload(127, "telephone-event/8000", "0-15");
+ * media.setAttribute("sendrecv", "");
+ * </pre>
+ * <p>Invoking <code>description.encode()</code> will produce a result like the
+ * one below.</p>
+ * <pre>
+ * v=0
+ * o=- 1284970442706 1284970442709 IN IP4 1.2.3.4
+ * s=-
+ * c=IN IP4 1.2.3.4
+ * t=0 0
+ * m=audio 56789 RTP/AVP 0 8 127
+ * a=rtpmap:0 PCMU/8000
+ * a=rtpmap:8 PCMA/8000
+ * a=rtpmap:127 telephone-event/8000
+ * a=fmtp:127 0-15
+ * a=sendrecv
+ * </pre>
+ * @hide
+ */
+public class SimpleSessionDescription {
+    private final Fields mFields = new Fields("voscbtka");
+    private final ArrayList<Media> mMedia = new ArrayList<Media>();
+
+    /**
+     * Creates a minimal session description from the given session ID and
+     * unicast address. The address is used in the origin field ("o=") and the
+     * connection field ("c="). See {@link SimpleSessionDescription} for an
+     * example of its usage.
+     */
+    public SimpleSessionDescription(long sessionId, String address) {
+        address = (address.indexOf(':') < 0 ? "IN IP4 " : "IN IP6 ") + address;
+        mFields.parse("v=0");
+        mFields.parse(String.format(Locale.US, "o=- %d %d %s", sessionId,
+                System.currentTimeMillis(), address));
+        mFields.parse("s=-");
+        mFields.parse("t=0 0");
+        mFields.parse("c=" + address);
+    }
+
+    /**
+     * Creates a session description from the given message.
+     *
+     * @throws IllegalArgumentException if message is invalid.
+     */
+    public SimpleSessionDescription(String message) {
+        String[] lines = message.trim().replaceAll(" +", " ").split("[\r\n]+");
+        Fields fields = mFields;
+
+        for (String line : lines) {
+            try {
+                if (line.charAt(1) != '=') {
+                    throw new IllegalArgumentException();
+                }
+                if (line.charAt(0) == 'm') {
+                    String[] parts = line.substring(2).split(" ", 4);
+                    String[] ports = parts[1].split("/", 2);
+                    Media media = newMedia(parts[0], Integer.parseInt(ports[0]),
+                            (ports.length < 2) ? 1 : Integer.parseInt(ports[1]),
+                            parts[2]);
+                    for (String format : parts[3].split(" ")) {
+                        media.setFormat(format, null);
+                    }
+                    fields = media;
+                } else {
+                    fields.parse(line);
+                }
+            } catch (Exception e) {
+                throw new IllegalArgumentException("Invalid SDP: " + line);
+            }
+        }
+    }
+
+    /**
+     * Creates a new media description in this session description.
+     *
+     * @param type The media type, e.g. {@code "audio"}.
+     * @param port The first transport port used by this media.
+     * @param portCount The number of contiguous ports used by this media.
+     * @param protocol The transport protocol, e.g. {@code "RTP/AVP"}.
+     */
+    public Media newMedia(String type, int port, int portCount,
+            String protocol) {
+        Media media = new Media(type, port, portCount, protocol);
+        mMedia.add(media);
+        return media;
+    }
+
+    /**
+     * Returns all the media descriptions in this session description.
+     */
+    public Media[] getMedia() {
+        return mMedia.toArray(new Media[mMedia.size()]);
+    }
+
+    /**
+     * Encodes the session description and all its media descriptions in a
+     * string. Note that the result might be incomplete if a required field
+     * has never been added before.
+     */
+    public String encode() {
+        StringBuilder buffer = new StringBuilder();
+        mFields.write(buffer);
+        for (Media media : mMedia) {
+            media.write(buffer);
+        }
+        return buffer.toString();
+    }
+
+    /**
+     * Returns the connection address or {@code null} if it is not present.
+     */
+    public String getAddress() {
+        return mFields.getAddress();
+    }
+
+    /**
+     * Sets the connection address. The field will be removed if the address
+     * is {@code null}.
+     */
+    public void setAddress(String address) {
+        mFields.setAddress(address);
+    }
+
+    /**
+     * Returns the encryption method or {@code null} if it is not present.
+     */
+    public String getEncryptionMethod() {
+        return mFields.getEncryptionMethod();
+    }
+
+    /**
+     * Returns the encryption key or {@code null} if it is not present.
+     */
+    public String getEncryptionKey() {
+        return mFields.getEncryptionKey();
+    }
+
+    /**
+     * Sets the encryption method and the encryption key. The field will be
+     * removed if the method is {@code null}.
+     */
+    public void setEncryption(String method, String key) {
+        mFields.setEncryption(method, key);
+    }
+
+    /**
+     * Returns the types of the bandwidth limits.
+     */
+    public String[] getBandwidthTypes() {
+        return mFields.getBandwidthTypes();
+    }
+
+    /**
+     * Returns the bandwidth limit of the given type or {@code -1} if it is not
+     * present.
+     */
+    public int getBandwidth(String type) {
+        return mFields.getBandwidth(type);
+    }
+
+    /**
+     * Sets the bandwith limit for the given type. The field will be removed if
+     * the value is negative.
+     */
+    public void setBandwidth(String type, int value) {
+        mFields.setBandwidth(type, value);
+    }
+
+    /**
+     * Returns the names of all the attributes.
+     */
+    public String[] getAttributeNames() {
+        return mFields.getAttributeNames();
+    }
+
+    /**
+     * Returns the attribute of the given name or {@code null} if it is not
+     * present.
+     */
+    public String getAttribute(String name) {
+        return mFields.getAttribute(name);
+    }
+
+    /**
+     * Sets the attribute for the given name. The field will be removed if
+     * the value is {@code null}. To set a binary attribute, use an empty
+     * string as the value.
+     */
+    public void setAttribute(String name, String value) {
+        mFields.setAttribute(name, value);
+    }
+
+    /**
+     * This class represents a media description of a session description. It
+     * can only be created by {@link SimpleSessionDescription#newMedia}. Since
+     * the syntax is more restricted for RTP based protocols, two sets of access
+     * methods are implemented. See {@link SimpleSessionDescription} for an
+     * example of its usage.
+     */
+    public static class Media extends Fields {
+        private final String mType;
+        private final int mPort;
+        private final int mPortCount;
+        private final String mProtocol;
+        private ArrayList<String> mFormats = new ArrayList<String>();
+
+        private Media(String type, int port, int portCount, String protocol) {
+            super("icbka");
+            mType = type;
+            mPort = port;
+            mPortCount = portCount;
+            mProtocol = protocol;
+        }
+
+        /**
+         * Returns the media type.
+         */
+        public String getType() {
+            return mType;
+        }
+
+        /**
+         * Returns the first transport port used by this media.
+         */
+        public int getPort() {
+            return mPort;
+        }
+
+        /**
+         * Returns the number of contiguous ports used by this media.
+         */
+        public int getPortCount() {
+            return mPortCount;
+        }
+
+        /**
+         * Returns the transport protocol.
+         */
+        public String getProtocol() {
+            return mProtocol;
+        }
+
+        /**
+         * Returns the media formats.
+         */
+        public String[] getFormats() {
+            return mFormats.toArray(new String[mFormats.size()]);
+        }
+
+        /**
+         * Returns the {@code fmtp} attribute of the given format or
+         * {@code null} if it is not present.
+         */
+        public String getFmtp(String format) {
+            return super.get("a=fmtp:" + format, ' ');
+        }
+
+        /**
+         * Sets a format and its {@code fmtp} attribute. If the attribute is
+         * {@code null}, the corresponding field will be removed.
+         */
+        public void setFormat(String format, String fmtp) {
+            mFormats.remove(format);
+            mFormats.add(format);
+            super.set("a=rtpmap:" + format, ' ', null);
+            super.set("a=fmtp:" + format, ' ', fmtp);
+        }
+
+        /**
+         * Removes a format and its {@code fmtp} attribute.
+         */
+        public void removeFormat(String format) {
+            mFormats.remove(format);
+            super.set("a=rtpmap:" + format, ' ', null);
+            super.set("a=fmtp:" + format, ' ', null);
+        }
+
+        /**
+         * Returns the RTP payload types.
+         */
+        public int[] getRtpPayloadTypes() {
+            int[] types = new int[mFormats.size()];
+            int length = 0;
+            for (String format : mFormats) {
+                try {
+                    types[length] = Integer.parseInt(format);
+                    ++length;
+                } catch (NumberFormatException e) { }
+            }
+            return Arrays.copyOf(types, length);
+        }
+
+        /**
+         * Returns the {@code rtpmap} attribute of the given RTP payload type
+         * or {@code null} if it is not present.
+         */
+        public String getRtpmap(int type) {
+            return super.get("a=rtpmap:" + type, ' ');
+        }
+
+        /**
+         * Returns the {@code fmtp} attribute of the given RTP payload type or
+         * {@code null} if it is not present.
+         */
+        public String getFmtp(int type) {
+            return super.get("a=fmtp:" + type, ' ');
+        }
+
+        /**
+         * Sets a RTP payload type and its {@code rtpmap} and {@code fmtp}
+         * attributes. If any of the attributes is {@code null}, the
+         * corresponding field will be removed. See
+         * {@link SimpleSessionDescription} for an example of its usage.
+         */
+        public void setRtpPayload(int type, String rtpmap, String fmtp) {
+            String format = String.valueOf(type);
+            mFormats.remove(format);
+            mFormats.add(format);
+            super.set("a=rtpmap:" + format, ' ', rtpmap);
+            super.set("a=fmtp:" + format, ' ', fmtp);
+        }
+
+        /**
+         * Removes a RTP payload and its {@code rtpmap} and {@code fmtp}
+         * attributes.
+         */
+        public void removeRtpPayload(int type) {
+            removeFormat(String.valueOf(type));
+        }
+
+        private void write(StringBuilder buffer) {
+            buffer.append("m=").append(mType).append(' ').append(mPort);
+            if (mPortCount != 1) {
+                buffer.append('/').append(mPortCount);
+            }
+            buffer.append(' ').append(mProtocol);
+            for (String format : mFormats) {
+                buffer.append(' ').append(format);
+            }
+            buffer.append("\r\n");
+            super.write(buffer);
+        }
+    }
+
+    /**
+     * This class acts as a set of fields, and the size of the set is expected
+     * to be small. Therefore, it uses a simple list instead of maps. Each field
+     * has three parts: a key, a delimiter, and a value. Delimiters are special
+     * because they are not included in binary attributes. As a result, the
+     * private methods, which are the building blocks of this class, all take
+     * the delimiter as an argument.
+     */
+    private static class Fields {
+        private final String mOrder;
+        private final ArrayList<String> mLines = new ArrayList<String>();
+
+        Fields(String order) {
+            mOrder = order;
+        }
+
+        /**
+         * Returns the connection address or {@code null} if it is not present.
+         */
+        public String getAddress() {
+            String address = get("c", '=');
+            if (address == null) {
+                return null;
+            }
+            String[] parts = address.split(" ");
+            if (parts.length != 3) {
+                return null;
+            }
+            int slash = parts[2].indexOf('/');
+            return (slash < 0) ? parts[2] : parts[2].substring(0, slash);
+        }
+
+        /**
+         * Sets the connection address. The field will be removed if the address
+         * is {@code null}.
+         */
+        public void setAddress(String address) {
+            if (address != null) {
+                address = (address.indexOf(':') < 0 ? "IN IP4 " : "IN IP6 ") +
+                        address;
+            }
+            set("c", '=', address);
+        }
+
+        /**
+         * Returns the encryption method or {@code null} if it is not present.
+         */
+        public String getEncryptionMethod() {
+            String encryption = get("k", '=');
+            if (encryption == null) {
+                return null;
+            }
+            int colon = encryption.indexOf(':');
+            return (colon == -1) ? encryption : encryption.substring(0, colon);
+        }
+
+        /**
+         * Returns the encryption key or {@code null} if it is not present.
+         */
+        public String getEncryptionKey() {
+            String encryption = get("k", '=');
+            if (encryption == null) {
+                return null;
+            }
+            int colon = encryption.indexOf(':');
+            return (colon == -1) ? null : encryption.substring(0, colon + 1);
+        }
+
+        /**
+         * Sets the encryption method and the encryption key. The field will be
+         * removed if the method is {@code null}.
+         */
+        public void setEncryption(String method, String key) {
+            set("k", '=', (method == null || key == null) ?
+                    method : method + ':' + key);
+        }
+
+        /**
+         * Returns the types of the bandwidth limits.
+         */
+        public String[] getBandwidthTypes() {
+            return cut("b=", ':');
+        }
+
+        /**
+         * Returns the bandwidth limit of the given type or {@code -1} if it is
+         * not present.
+         */
+        public int getBandwidth(String type) {
+            String value = get("b=" + type, ':');
+            if (value != null) {
+                try {
+                    return Integer.parseInt(value);
+                } catch (NumberFormatException e) { }
+                setBandwidth(type, -1);
+            }
+            return -1;
+        }
+
+        /**
+         * Sets the bandwith limit for the given type. The field will be removed
+         * if the value is negative.
+         */
+        public void setBandwidth(String type, int value) {
+            set("b=" + type, ':', (value < 0) ? null : String.valueOf(value));
+        }
+
+        /**
+         * Returns the names of all the attributes.
+         */
+        public String[] getAttributeNames() {
+            return cut("a=", ':');
+        }
+
+        /**
+         * Returns the attribute of the given name or {@code null} if it is not
+         * present.
+         */
+        public String getAttribute(String name) {
+            return get("a=" + name, ':');
+        }
+
+        /**
+         * Sets the attribute for the given name. The field will be removed if
+         * the value is {@code null}. To set a binary attribute, use an empty
+         * string as the value.
+         */
+        public void setAttribute(String name, String value) {
+            set("a=" + name, ':', value);
+        }
+
+        private void write(StringBuilder buffer) {
+            for (int i = 0; i < mOrder.length(); ++i) {
+                char type = mOrder.charAt(i);
+                for (String line : mLines) {
+                    if (line.charAt(0) == type) {
+                        buffer.append(line).append("\r\n");
+                    }
+                }
+            }
+        }
+
+        /**
+         * Invokes {@link #set} after splitting the line into three parts.
+         */
+        private void parse(String line) {
+            char type = line.charAt(0);
+            if (mOrder.indexOf(type) == -1) {
+                return;
+            }
+            char delimiter = '=';
+            if (line.startsWith("a=rtpmap:") || line.startsWith("a=fmtp:")) {
+                delimiter = ' ';
+            } else if (type == 'b' || type == 'a') {
+                delimiter = ':';
+            }
+            int i = line.indexOf(delimiter);
+            if (i == -1) {
+                set(line, delimiter, "");
+            } else {
+                set(line.substring(0, i), delimiter, line.substring(i + 1));
+            }
+        }
+
+        /**
+         * Finds the key with the given prefix and returns its suffix.
+         */
+        private String[] cut(String prefix, char delimiter) {
+            String[] names = new String[mLines.size()];
+            int length = 0;
+            for (String line : mLines) {
+                if (line.startsWith(prefix)) {
+                    int i = line.indexOf(delimiter);
+                    if (i == -1) {
+                        i = line.length();
+                    }
+                    names[length] = line.substring(prefix.length(), i);
+                    ++length;
+                }
+            }
+            return Arrays.copyOf(names, length);
+        }
+
+        /**
+         * Returns the index of the key.
+         */
+        private int find(String key, char delimiter) {
+            int length = key.length();
+            for (int i = mLines.size() - 1; i >= 0; --i) {
+                String line = mLines.get(i);
+                if (line.startsWith(key) && (line.length() == length ||
+                        line.charAt(length) == delimiter)) {
+                    return i;
+                }
+            }
+            return -1;
+        }
+
+        /**
+         * Sets the key with the value or removes the key if the value is
+         * {@code null}.
+         */
+        private void set(String key, char delimiter, String value) {
+            int index = find(key, delimiter);
+            if (value != null) {
+                if (value.length() != 0) {
+                    key = key + delimiter + value;
+                }
+                if (index == -1) {
+                    mLines.add(key);
+                } else {
+                    mLines.set(index, key);
+                }
+            } else if (index != -1) {
+                mLines.remove(index);
+            }
+        }
+
+        /**
+         * Returns the value of the key.
+         */
+        private String get(String key, char delimiter) {
+            int index = find(key, delimiter);
+            if (index == -1) {
+                return null;
+            }
+            String line = mLines.get(index);
+            int length = key.length();
+            return (line.length() == length) ? "" : line.substring(length + 1);
+        }
+    }
+}
diff --git a/android/net/sip/SipAudioCall.java b/android/net/sip/SipAudioCall.java
new file mode 100644
index 0000000..ea943e9
--- /dev/null
+++ b/android/net/sip/SipAudioCall.java
@@ -0,0 +1,1143 @@
+/*
+ * Copyright (C) 2010 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.sip;
+
+import android.content.Context;
+import android.media.AudioManager;
+import android.net.rtp.AudioCodec;
+import android.net.rtp.AudioGroup;
+import android.net.rtp.AudioStream;
+import android.net.rtp.RtpStream;
+import android.net.sip.SimpleSessionDescription.Media;
+import android.net.wifi.WifiManager;
+import android.os.Message;
+import android.telephony.Rlog;
+import android.text.TextUtils;
+import java.io.IOException;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+/**
+ * Handles an Internet audio call over SIP. You can instantiate this class with {@link SipManager},
+ * using {@link SipManager#makeAudioCall makeAudioCall()} and  {@link SipManager#takeAudioCall
+ * takeAudioCall()}.
+ *
+ * <p class="note"><strong>Note:</strong> Using this class require the
+ *   {@link android.Manifest.permission#INTERNET} and
+ *   {@link android.Manifest.permission#USE_SIP} permissions. In addition, {@link
+ *   #startAudio} requires the
+ *   {@link android.Manifest.permission#RECORD_AUDIO},
+ *   {@link android.Manifest.permission#ACCESS_WIFI_STATE}, and
+ *   {@link android.Manifest.permission#WAKE_LOCK} permissions; and {@link #setSpeakerMode
+ *   setSpeakerMode()} requires the
+ *   {@link android.Manifest.permission#MODIFY_AUDIO_SETTINGS} permission.</p>
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about using SIP, read the
+ * <a href="{@docRoot}guide/topics/network/sip.html">Session Initiation Protocol</a>
+ * developer guide.</p>
+ * </div>
+ */
+public class SipAudioCall {
+    private static final String LOG_TAG = SipAudioCall.class.getSimpleName();
+    private static final boolean DBG = true;
+    private static final boolean RELEASE_SOCKET = true;
+    private static final boolean DONT_RELEASE_SOCKET = false;
+    private static final int SESSION_TIMEOUT = 5; // in seconds
+    private static final int TRANSFER_TIMEOUT = 15; // in seconds
+
+    /** Listener for events relating to a SIP call, such as when a call is being
+     * recieved ("on ringing") or a call is outgoing ("on calling").
+     * <p>Many of these events are also received by {@link SipSession.Listener}.</p>
+     */
+    public static class Listener {
+        /**
+         * Called when the call object is ready to make another call.
+         * The default implementation calls {@link #onChanged}.
+         *
+         * @param call the call object that is ready to make another call
+         */
+        public void onReadyToCall(SipAudioCall call) {
+            onChanged(call);
+        }
+
+        /**
+         * Called when a request is sent out to initiate a new call.
+         * The default implementation calls {@link #onChanged}.
+         *
+         * @param call the call object that carries out the audio call
+         */
+        public void onCalling(SipAudioCall call) {
+            onChanged(call);
+        }
+
+        /**
+         * Called when a new call comes in.
+         * The default implementation calls {@link #onChanged}.
+         *
+         * @param call the call object that carries out the audio call
+         * @param caller the SIP profile of the caller
+         */
+        public void onRinging(SipAudioCall call, SipProfile caller) {
+            onChanged(call);
+        }
+
+        /**
+         * Called when a RINGING response is received for the INVITE request
+         * sent. The default implementation calls {@link #onChanged}.
+         *
+         * @param call the call object that carries out the audio call
+         */
+        public void onRingingBack(SipAudioCall call) {
+            onChanged(call);
+        }
+
+        /**
+         * Called when the session is established.
+         * The default implementation calls {@link #onChanged}.
+         *
+         * @param call the call object that carries out the audio call
+         */
+        public void onCallEstablished(SipAudioCall call) {
+            onChanged(call);
+        }
+
+        /**
+         * Called when the session is terminated.
+         * The default implementation calls {@link #onChanged}.
+         *
+         * @param call the call object that carries out the audio call
+         */
+        public void onCallEnded(SipAudioCall call) {
+            onChanged(call);
+        }
+
+        /**
+         * Called when the peer is busy during session initialization.
+         * The default implementation calls {@link #onChanged}.
+         *
+         * @param call the call object that carries out the audio call
+         */
+        public void onCallBusy(SipAudioCall call) {
+            onChanged(call);
+        }
+
+        /**
+         * Called when the call is on hold.
+         * The default implementation calls {@link #onChanged}.
+         *
+         * @param call the call object that carries out the audio call
+         */
+        public void onCallHeld(SipAudioCall call) {
+            onChanged(call);
+        }
+
+        /**
+         * Called when an error occurs. The default implementation is no op.
+         *
+         * @param call the call object that carries out the audio call
+         * @param errorCode error code of this error
+         * @param errorMessage error message
+         * @see SipErrorCode
+         */
+        public void onError(SipAudioCall call, int errorCode,
+                String errorMessage) {
+            // no-op
+        }
+
+        /**
+         * Called when an event occurs and the corresponding callback is not
+         * overridden. The default implementation is no op. Error events are
+         * not re-directed to this callback and are handled in {@link #onError}.
+         */
+        public void onChanged(SipAudioCall call) {
+            // no-op
+        }
+    }
+
+    private Context mContext;
+    private SipProfile mLocalProfile;
+    private SipAudioCall.Listener mListener;
+    private SipSession mSipSession;
+    private SipSession mTransferringSession;
+
+    private long mSessionId = System.currentTimeMillis();
+    private String mPeerSd;
+
+    private AudioStream mAudioStream;
+    private AudioGroup mAudioGroup;
+
+    private boolean mInCall = false;
+    private boolean mMuted = false;
+    private boolean mHold = false;
+
+    private WifiManager mWm;
+    private WifiManager.WifiLock mWifiHighPerfLock;
+
+    private int mErrorCode = SipErrorCode.NO_ERROR;
+    private String mErrorMessage;
+
+    /**
+     * Creates a call object with the local SIP profile.
+     * @param context the context for accessing system services such as
+     *        ringtone, audio, WIFI etc
+     */
+    public SipAudioCall(Context context, SipProfile localProfile) {
+        mContext = context;
+        mLocalProfile = localProfile;
+        mWm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+    }
+
+    /**
+     * Sets the listener to listen to the audio call events. The method calls
+     * {@link #setListener setListener(listener, false)}.
+     *
+     * @param listener to listen to the audio call events of this object
+     * @see #setListener(Listener, boolean)
+     */
+    public void setListener(SipAudioCall.Listener listener) {
+        setListener(listener, false);
+    }
+
+    /**
+     * Sets the listener to listen to the audio call events. A
+     * {@link SipAudioCall} can only hold one listener at a time. Subsequent
+     * calls to this method override the previous listener.
+     *
+     * @param listener to listen to the audio call events of this object
+     * @param callbackImmediately set to true if the caller wants to be called
+     *      back immediately on the current state
+     */
+    public void setListener(SipAudioCall.Listener listener,
+            boolean callbackImmediately) {
+        mListener = listener;
+        try {
+            if ((listener == null) || !callbackImmediately) {
+                // do nothing
+            } else if (mErrorCode != SipErrorCode.NO_ERROR) {
+                listener.onError(this, mErrorCode, mErrorMessage);
+            } else if (mInCall) {
+                if (mHold) {
+                    listener.onCallHeld(this);
+                } else {
+                    listener.onCallEstablished(this);
+                }
+            } else {
+                int state = getState();
+                switch (state) {
+                    case SipSession.State.READY_TO_CALL:
+                        listener.onReadyToCall(this);
+                        break;
+                    case SipSession.State.INCOMING_CALL:
+                        listener.onRinging(this, getPeerProfile());
+                        break;
+                    case SipSession.State.OUTGOING_CALL:
+                        listener.onCalling(this);
+                        break;
+                    case SipSession.State.OUTGOING_CALL_RING_BACK:
+                        listener.onRingingBack(this);
+                        break;
+                }
+            }
+        } catch (Throwable t) {
+            loge("setListener()", t);
+        }
+    }
+
+    /**
+     * Checks if the call is established.
+     *
+     * @return true if the call is established
+     */
+    public boolean isInCall() {
+        synchronized (this) {
+            return mInCall;
+        }
+    }
+
+    /**
+     * Checks if the call is on hold.
+     *
+     * @return true if the call is on hold
+     */
+    public boolean isOnHold() {
+        synchronized (this) {
+            return mHold;
+        }
+    }
+
+    /**
+     * Closes this object. This object is not usable after being closed.
+     */
+    public void close() {
+        close(true);
+    }
+
+    private synchronized void close(boolean closeRtp) {
+        if (closeRtp) stopCall(RELEASE_SOCKET);
+
+        mInCall = false;
+        mHold = false;
+        mSessionId = System.currentTimeMillis();
+        mErrorCode = SipErrorCode.NO_ERROR;
+        mErrorMessage = null;
+
+        if (mSipSession != null) {
+            mSipSession.setListener(null);
+            mSipSession = null;
+        }
+    }
+
+    /**
+     * Gets the local SIP profile.
+     *
+     * @return the local SIP profile
+     */
+    public SipProfile getLocalProfile() {
+        synchronized (this) {
+            return mLocalProfile;
+        }
+    }
+
+    /**
+     * Gets the peer's SIP profile.
+     *
+     * @return the peer's SIP profile
+     */
+    public SipProfile getPeerProfile() {
+        synchronized (this) {
+            return (mSipSession == null) ? null : mSipSession.getPeerProfile();
+        }
+    }
+
+    /**
+     * Gets the state of the {@link SipSession} that carries this call.
+     * The value returned must be one of the states in {@link SipSession.State}.
+     *
+     * @return the session state
+     */
+    public int getState() {
+        synchronized (this) {
+            if (mSipSession == null) return SipSession.State.READY_TO_CALL;
+            return mSipSession.getState();
+        }
+    }
+
+
+    /**
+     * Gets the {@link SipSession} that carries this call.
+     *
+     * @return the session object that carries this call
+     * @hide
+     */
+    public SipSession getSipSession() {
+        synchronized (this) {
+            return mSipSession;
+        }
+    }
+
+    private synchronized void transferToNewSession() {
+        if (mTransferringSession == null) return;
+        SipSession origin = mSipSession;
+        mSipSession = mTransferringSession;
+        mTransferringSession = null;
+
+        // stop the replaced call.
+        if (mAudioStream != null) {
+            mAudioStream.join(null);
+        } else {
+            try {
+                mAudioStream = new AudioStream(InetAddress.getByName(
+                        getLocalIp()));
+            } catch (Throwable t) {
+                loge("transferToNewSession():", t);
+            }
+        }
+        if (origin != null) origin.endCall();
+        startAudio();
+    }
+
+    private SipSession.Listener createListener() {
+        return new SipSession.Listener() {
+            @Override
+            public void onCalling(SipSession session) {
+                if (DBG) log("onCalling: session=" + session);
+                Listener listener = mListener;
+                if (listener != null) {
+                    try {
+                        listener.onCalling(SipAudioCall.this);
+                    } catch (Throwable t) {
+                        loge("onCalling():", t);
+                    }
+                }
+            }
+
+            @Override
+            public void onRingingBack(SipSession session) {
+                if (DBG) log("onRingingBackk: " + session);
+                Listener listener = mListener;
+                if (listener != null) {
+                    try {
+                        listener.onRingingBack(SipAudioCall.this);
+                    } catch (Throwable t) {
+                        loge("onRingingBack():", t);
+                    }
+                }
+            }
+
+            @Override
+            public void onRinging(SipSession session,
+                    SipProfile peerProfile, String sessionDescription) {
+                // this callback is triggered only for reinvite.
+                synchronized (SipAudioCall.this) {
+                    if ((mSipSession == null) || !mInCall
+                            || !session.getCallId().equals(
+                                    mSipSession.getCallId())) {
+                        // should not happen
+                        session.endCall();
+                        return;
+                    }
+
+                    // session changing request
+                    try {
+                        String answer = createAnswer(sessionDescription).encode();
+                        mSipSession.answerCall(answer, SESSION_TIMEOUT);
+                    } catch (Throwable e) {
+                        loge("onRinging():", e);
+                        session.endCall();
+                    }
+                }
+            }
+
+            @Override
+            public void onCallEstablished(SipSession session,
+                    String sessionDescription) {
+                mPeerSd = sessionDescription;
+                if (DBG) log("onCallEstablished(): " + mPeerSd);
+
+                // TODO: how to notify the UI that the remote party is changed
+                if ((mTransferringSession != null)
+                        && (session == mTransferringSession)) {
+                    transferToNewSession();
+                    return;
+                }
+
+                Listener listener = mListener;
+                if (listener != null) {
+                    try {
+                        if (mHold) {
+                            listener.onCallHeld(SipAudioCall.this);
+                        } else {
+                            listener.onCallEstablished(SipAudioCall.this);
+                        }
+                    } catch (Throwable t) {
+                        loge("onCallEstablished(): ", t);
+                    }
+                }
+            }
+
+            @Override
+            public void onCallEnded(SipSession session) {
+                if (DBG) log("onCallEnded: " + session + " mSipSession:" + mSipSession);
+                // reset the trasnferring session if it is the one.
+                if (session == mTransferringSession) {
+                    mTransferringSession = null;
+                    return;
+                }
+                // or ignore the event if the original session is being
+                // transferred to the new one.
+                if ((mTransferringSession != null) ||
+                    (session != mSipSession)) return;
+
+                Listener listener = mListener;
+                if (listener != null) {
+                    try {
+                        listener.onCallEnded(SipAudioCall.this);
+                    } catch (Throwable t) {
+                        loge("onCallEnded(): ", t);
+                    }
+                }
+                close();
+            }
+
+            @Override
+            public void onCallBusy(SipSession session) {
+                if (DBG) log("onCallBusy: " + session);
+                Listener listener = mListener;
+                if (listener != null) {
+                    try {
+                        listener.onCallBusy(SipAudioCall.this);
+                    } catch (Throwable t) {
+                        loge("onCallBusy(): ", t);
+                    }
+                }
+                close(false);
+            }
+
+            @Override
+            public void onCallChangeFailed(SipSession session, int errorCode,
+                    String message) {
+                if (DBG) log("onCallChangedFailed: " + message);
+                mErrorCode = errorCode;
+                mErrorMessage = message;
+                Listener listener = mListener;
+                if (listener != null) {
+                    try {
+                        listener.onError(SipAudioCall.this, mErrorCode,
+                                message);
+                    } catch (Throwable t) {
+                        loge("onCallBusy():", t);
+                    }
+                }
+            }
+
+            @Override
+            public void onError(SipSession session, int errorCode,
+                    String message) {
+                SipAudioCall.this.onError(errorCode, message);
+            }
+
+            @Override
+            public void onRegistering(SipSession session) {
+                // irrelevant
+            }
+
+            @Override
+            public void onRegistrationTimeout(SipSession session) {
+                // irrelevant
+            }
+
+            @Override
+            public void onRegistrationFailed(SipSession session, int errorCode,
+                    String message) {
+                // irrelevant
+            }
+
+            @Override
+            public void onRegistrationDone(SipSession session, int duration) {
+                // irrelevant
+            }
+
+            @Override
+            public void onCallTransferring(SipSession newSession,
+                    String sessionDescription) {
+                if (DBG) log("onCallTransferring: mSipSession="
+                        + mSipSession + " newSession=" + newSession);
+                mTransferringSession = newSession;
+                try {
+                    if (sessionDescription == null) {
+                        newSession.makeCall(newSession.getPeerProfile(),
+                                createOffer().encode(), TRANSFER_TIMEOUT);
+                    } else {
+                        String answer = createAnswer(sessionDescription).encode();
+                        newSession.answerCall(answer, SESSION_TIMEOUT);
+                    }
+                } catch (Throwable e) {
+                    loge("onCallTransferring()", e);
+                    newSession.endCall();
+                }
+            }
+        };
+    }
+
+    private void onError(int errorCode, String message) {
+        if (DBG) log("onError: "
+                + SipErrorCode.toString(errorCode) + ": " + message);
+        mErrorCode = errorCode;
+        mErrorMessage = message;
+        Listener listener = mListener;
+        if (listener != null) {
+            try {
+                listener.onError(this, errorCode, message);
+            } catch (Throwable t) {
+                loge("onError():", t);
+            }
+        }
+        synchronized (this) {
+            if ((errorCode == SipErrorCode.DATA_CONNECTION_LOST)
+                    || !isInCall()) {
+                close(true);
+            }
+        }
+    }
+
+    /**
+     * Attaches an incoming call to this call object.
+     *
+     * @param session the session that receives the incoming call
+     * @param sessionDescription the session description of the incoming call
+     * @throws SipException if the SIP service fails to attach this object to
+     *        the session or VOIP API is not supported by the device
+     * @see SipManager#isVoipSupported
+     */
+    public void attachCall(SipSession session, String sessionDescription)
+            throws SipException {
+        if (!SipManager.isVoipSupported(mContext)) {
+            throw new SipException("VOIP API is not supported");
+        }
+
+        synchronized (this) {
+            mSipSession = session;
+            mPeerSd = sessionDescription;
+            if (DBG) log("attachCall(): " + mPeerSd);
+            try {
+                session.setListener(createListener());
+            } catch (Throwable e) {
+                loge("attachCall()", e);
+                throwSipException(e);
+            }
+        }
+    }
+
+    /**
+     * Initiates an audio call to the specified profile. The attempt will be
+     * timed out if the call is not established within {@code timeout} seconds
+     * and {@link Listener#onError onError(SipAudioCall, SipErrorCode.TIME_OUT, String)}
+     * will be called.
+     *
+     * @param peerProfile the SIP profile to make the call to
+     * @param sipSession the {@link SipSession} for carrying out the call
+     * @param timeout the timeout value in seconds. Default value (defined by
+     *        SIP protocol) is used if {@code timeout} is zero or negative.
+     * @see Listener#onError
+     * @throws SipException if the SIP service fails to create a session for the
+     *        call or VOIP API is not supported by the device
+     * @see SipManager#isVoipSupported
+     */
+    public void makeCall(SipProfile peerProfile, SipSession sipSession,
+            int timeout) throws SipException {
+        if (DBG) log("makeCall: " + peerProfile + " session=" + sipSession + " timeout=" + timeout);
+        if (!SipManager.isVoipSupported(mContext)) {
+            throw new SipException("VOIP API is not supported");
+        }
+
+        synchronized (this) {
+            mSipSession = sipSession;
+            try {
+                mAudioStream = new AudioStream(InetAddress.getByName(
+                        getLocalIp()));
+                sipSession.setListener(createListener());
+                sipSession.makeCall(peerProfile, createOffer().encode(),
+                        timeout);
+            } catch (IOException e) {
+                loge("makeCall:", e);
+                throw new SipException("makeCall()", e);
+            }
+        }
+    }
+
+    /**
+     * Ends a call.
+     * @throws SipException if the SIP service fails to end the call
+     */
+    public void endCall() throws SipException {
+        if (DBG) log("endCall: mSipSession" + mSipSession);
+        synchronized (this) {
+            stopCall(RELEASE_SOCKET);
+            mInCall = false;
+
+            // perform the above local ops first and then network op
+            if (mSipSession != null) mSipSession.endCall();
+        }
+    }
+
+    /**
+     * Puts a call on hold.  When succeeds, {@link Listener#onCallHeld} is
+     * called. The attempt will be timed out if the call is not established
+     * within {@code timeout} seconds and
+     * {@link Listener#onError onError(SipAudioCall, SipErrorCode.TIME_OUT, String)}
+     * will be called.
+     *
+     * @param timeout the timeout value in seconds. Default value (defined by
+     *        SIP protocol) is used if {@code timeout} is zero or negative.
+     * @see Listener#onError
+     * @throws SipException if the SIP service fails to hold the call
+     */
+    public void holdCall(int timeout) throws SipException {
+        if (DBG) log("holdCall: mSipSession" + mSipSession + " timeout=" + timeout);
+        synchronized (this) {
+            if (mHold) return;
+            if (mSipSession == null) {
+                loge("holdCall:");
+                throw new SipException("Not in a call to hold call");
+            }
+            mSipSession.changeCall(createHoldOffer().encode(), timeout);
+            mHold = true;
+            setAudioGroupMode();
+        }
+    }
+
+    /**
+     * Answers a call. The attempt will be timed out if the call is not
+     * established within {@code timeout} seconds and
+     * {@link Listener#onError onError(SipAudioCall, SipErrorCode.TIME_OUT, String)}
+     * will be called.
+     *
+     * @param timeout the timeout value in seconds. Default value (defined by
+     *        SIP protocol) is used if {@code timeout} is zero or negative.
+     * @see Listener#onError
+     * @throws SipException if the SIP service fails to answer the call
+     */
+    public void answerCall(int timeout) throws SipException {
+        if (DBG) log("answerCall: mSipSession" + mSipSession + " timeout=" + timeout);
+        synchronized (this) {
+            if (mSipSession == null) {
+                throw new SipException("No call to answer");
+            }
+            try {
+                mAudioStream = new AudioStream(InetAddress.getByName(
+                        getLocalIp()));
+                mSipSession.answerCall(createAnswer(mPeerSd).encode(), timeout);
+            } catch (IOException e) {
+                loge("answerCall:", e);
+                throw new SipException("answerCall()", e);
+            }
+        }
+    }
+
+    /**
+     * Continues a call that's on hold. When succeeds,
+     * {@link Listener#onCallEstablished} is called. The attempt will be timed
+     * out if the call is not established within {@code timeout} seconds and
+     * {@link Listener#onError onError(SipAudioCall, SipErrorCode.TIME_OUT, String)}
+     * will be called.
+     *
+     * @param timeout the timeout value in seconds. Default value (defined by
+     *        SIP protocol) is used if {@code timeout} is zero or negative.
+     * @see Listener#onError
+     * @throws SipException if the SIP service fails to unhold the call
+     */
+    public void continueCall(int timeout) throws SipException {
+        if (DBG) log("continueCall: mSipSession" + mSipSession + " timeout=" + timeout);
+        synchronized (this) {
+            if (!mHold) return;
+            mSipSession.changeCall(createContinueOffer().encode(), timeout);
+            mHold = false;
+            setAudioGroupMode();
+        }
+    }
+
+    private SimpleSessionDescription createOffer() {
+        SimpleSessionDescription offer =
+                new SimpleSessionDescription(mSessionId, getLocalIp());
+        AudioCodec[] codecs = AudioCodec.getCodecs();
+        Media media = offer.newMedia(
+                "audio", mAudioStream.getLocalPort(), 1, "RTP/AVP");
+        for (AudioCodec codec : AudioCodec.getCodecs()) {
+            media.setRtpPayload(codec.type, codec.rtpmap, codec.fmtp);
+        }
+        media.setRtpPayload(127, "telephone-event/8000", "0-15");
+        if (DBG) log("createOffer: offer=" + offer);
+        return offer;
+    }
+
+    private SimpleSessionDescription createAnswer(String offerSd) {
+        if (TextUtils.isEmpty(offerSd)) return createOffer();
+        SimpleSessionDescription offer =
+                new SimpleSessionDescription(offerSd);
+        SimpleSessionDescription answer =
+                new SimpleSessionDescription(mSessionId, getLocalIp());
+        AudioCodec codec = null;
+        for (Media media : offer.getMedia()) {
+            if ((codec == null) && (media.getPort() > 0)
+                    && "audio".equals(media.getType())
+                    && "RTP/AVP".equals(media.getProtocol())) {
+                // Find the first audio codec we supported.
+                for (int type : media.getRtpPayloadTypes()) {
+                    codec = AudioCodec.getCodec(type, media.getRtpmap(type),
+                            media.getFmtp(type));
+                    if (codec != null) {
+                        break;
+                    }
+                }
+                if (codec != null) {
+                    Media reply = answer.newMedia(
+                            "audio", mAudioStream.getLocalPort(), 1, "RTP/AVP");
+                    reply.setRtpPayload(codec.type, codec.rtpmap, codec.fmtp);
+
+                    // Check if DTMF is supported in the same media.
+                    for (int type : media.getRtpPayloadTypes()) {
+                        String rtpmap = media.getRtpmap(type);
+                        if ((type != codec.type) && (rtpmap != null)
+                                && rtpmap.startsWith("telephone-event")) {
+                            reply.setRtpPayload(
+                                    type, rtpmap, media.getFmtp(type));
+                        }
+                    }
+
+                    // Handle recvonly and sendonly.
+                    if (media.getAttribute("recvonly") != null) {
+                        answer.setAttribute("sendonly", "");
+                    } else if(media.getAttribute("sendonly") != null) {
+                        answer.setAttribute("recvonly", "");
+                    } else if(offer.getAttribute("recvonly") != null) {
+                        answer.setAttribute("sendonly", "");
+                    } else if(offer.getAttribute("sendonly") != null) {
+                        answer.setAttribute("recvonly", "");
+                    }
+                    continue;
+                }
+            }
+            // Reject the media.
+            Media reply = answer.newMedia(
+                    media.getType(), 0, 1, media.getProtocol());
+            for (String format : media.getFormats()) {
+                reply.setFormat(format, null);
+            }
+        }
+        if (codec == null) {
+            loge("createAnswer: no suitable codes");
+            throw new IllegalStateException("Reject SDP: no suitable codecs");
+        }
+        if (DBG) log("createAnswer: answer=" + answer);
+        return answer;
+    }
+
+    private SimpleSessionDescription createHoldOffer() {
+        SimpleSessionDescription offer = createContinueOffer();
+        offer.setAttribute("sendonly", "");
+        if (DBG) log("createHoldOffer: offer=" + offer);
+        return offer;
+    }
+
+    private SimpleSessionDescription createContinueOffer() {
+        if (DBG) log("createContinueOffer");
+        SimpleSessionDescription offer =
+                new SimpleSessionDescription(mSessionId, getLocalIp());
+        Media media = offer.newMedia(
+                "audio", mAudioStream.getLocalPort(), 1, "RTP/AVP");
+        AudioCodec codec = mAudioStream.getCodec();
+        media.setRtpPayload(codec.type, codec.rtpmap, codec.fmtp);
+        int dtmfType = mAudioStream.getDtmfType();
+        if (dtmfType != -1) {
+            media.setRtpPayload(dtmfType, "telephone-event/8000", "0-15");
+        }
+        return offer;
+    }
+
+    private void grabWifiHighPerfLock() {
+        if (mWifiHighPerfLock == null) {
+            if (DBG) log("grabWifiHighPerfLock:");
+            mWifiHighPerfLock = ((WifiManager)
+                    mContext.getSystemService(Context.WIFI_SERVICE))
+                    .createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, LOG_TAG);
+            mWifiHighPerfLock.acquire();
+        }
+    }
+
+    private void releaseWifiHighPerfLock() {
+        if (mWifiHighPerfLock != null) {
+            if (DBG) log("releaseWifiHighPerfLock:");
+            mWifiHighPerfLock.release();
+            mWifiHighPerfLock = null;
+        }
+    }
+
+    private boolean isWifiOn() {
+        return (mWm.getConnectionInfo().getBSSID() == null) ? false : true;
+    }
+
+    /** Toggles mute. */
+    public void toggleMute() {
+        synchronized (this) {
+            mMuted = !mMuted;
+            setAudioGroupMode();
+        }
+    }
+
+    /**
+     * Checks if the call is muted.
+     *
+     * @return true if the call is muted
+     */
+    public boolean isMuted() {
+        synchronized (this) {
+            return mMuted;
+        }
+    }
+
+    /**
+     * Puts the device to speaker mode.
+     * <p class="note"><strong>Note:</strong> Requires the
+     *   {@link android.Manifest.permission#MODIFY_AUDIO_SETTINGS} permission.</p>
+     *
+     * @param speakerMode set true to enable speaker mode; false to disable
+     */
+    public void setSpeakerMode(boolean speakerMode) {
+        synchronized (this) {
+            ((AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE))
+                    .setSpeakerphoneOn(speakerMode);
+            setAudioGroupMode();
+        }
+    }
+
+    private boolean isSpeakerOn() {
+        return ((AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE))
+                .isSpeakerphoneOn();
+    }
+
+    /**
+     * Sends a DTMF code. According to <a href="http://tools.ietf.org/html/rfc2833">RFC 2883</a>,
+     * event 0--9 maps to decimal
+     * value 0--9, '*' to 10, '#' to 11, event 'A'--'D' to 12--15, and event
+     * flash to 16. Currently, event flash is not supported.
+     *
+     * @param code the DTMF code to send. Value 0 to 15 (inclusive) are valid
+     *        inputs.
+     */
+    public void sendDtmf(int code) {
+        sendDtmf(code, null);
+    }
+
+    /**
+     * Sends a DTMF code. According to <a href="http://tools.ietf.org/html/rfc2833">RFC 2883</a>,
+     * event 0--9 maps to decimal
+     * value 0--9, '*' to 10, '#' to 11, event 'A'--'D' to 12--15, and event
+     * flash to 16. Currently, event flash is not supported.
+     *
+     * @param code the DTMF code to send. Value 0 to 15 (inclusive) are valid
+     *        inputs.
+     * @param result the result message to send when done
+     */
+    public void sendDtmf(int code, Message result) {
+        synchronized (this) {
+            AudioGroup audioGroup = getAudioGroup();
+            if ((audioGroup != null) && (mSipSession != null)
+                    && (SipSession.State.IN_CALL == getState())) {
+                if (DBG) log("sendDtmf: code=" + code + " result=" + result);
+                audioGroup.sendDtmf(code);
+            }
+            if (result != null) result.sendToTarget();
+        }
+    }
+
+    /**
+     * Gets the {@link AudioStream} object used in this call. The object
+     * represents the RTP stream that carries the audio data to and from the
+     * peer. The object may not be created before the call is established. And
+     * it is undefined after the call ends or the {@link #close} method is
+     * called.
+     *
+     * @return the {@link AudioStream} object or null if the RTP stream has not
+     *      yet been set up
+     * @hide
+     */
+    public AudioStream getAudioStream() {
+        synchronized (this) {
+            return mAudioStream;
+        }
+    }
+
+    /**
+     * Gets the {@link AudioGroup} object which the {@link AudioStream} object
+     * joins. The group object may not exist before the call is established.
+     * Also, the {@code AudioStream} may change its group during a call (e.g.,
+     * after the call is held/un-held). Finally, the {@code AudioGroup} object
+     * returned by this method is undefined after the call ends or the
+     * {@link #close} method is called. If a group object is set by
+     * {@link #setAudioGroup(AudioGroup)}, then this method returns that object.
+     *
+     * @return the {@link AudioGroup} object or null if the RTP stream has not
+     *      yet been set up
+     * @see #getAudioStream
+     * @hide
+     */
+    public AudioGroup getAudioGroup() {
+        synchronized (this) {
+            if (mAudioGroup != null) return mAudioGroup;
+            return ((mAudioStream == null) ? null : mAudioStream.getGroup());
+        }
+    }
+
+    /**
+     * Sets the {@link AudioGroup} object which the {@link AudioStream} object
+     * joins. If {@code audioGroup} is null, then the {@code AudioGroup} object
+     * will be dynamically created when needed. Note that the mode of the
+     * {@code AudioGroup} is not changed according to the audio settings (i.e.,
+     * hold, mute, speaker phone) of this object. This is mainly used to merge
+     * multiple {@code SipAudioCall} objects to form a conference call. The
+     * settings of the first object (that merges others) override others'.
+     *
+     * @see #getAudioStream
+     * @hide
+     */
+    public void setAudioGroup(AudioGroup group) {
+        synchronized (this) {
+            if (DBG) log("setAudioGroup: group=" + group);
+            if ((mAudioStream != null) && (mAudioStream.getGroup() != null)) {
+                mAudioStream.join(group);
+            }
+            mAudioGroup = group;
+        }
+    }
+
+    /**
+     * Starts the audio for the established call. This method should be called
+     * after {@link Listener#onCallEstablished} is called.
+     * <p class="note"><strong>Note:</strong> Requires the
+     *   {@link android.Manifest.permission#RECORD_AUDIO},
+     *   {@link android.Manifest.permission#ACCESS_WIFI_STATE} and
+     *   {@link android.Manifest.permission#WAKE_LOCK} permissions.</p>
+     */
+    public void startAudio() {
+        try {
+            startAudioInternal();
+        } catch (UnknownHostException e) {
+            onError(SipErrorCode.PEER_NOT_REACHABLE, e.getMessage());
+        } catch (Throwable e) {
+            onError(SipErrorCode.CLIENT_ERROR, e.getMessage());
+        }
+    }
+
+    private synchronized void startAudioInternal() throws UnknownHostException {
+        if (DBG) loge("startAudioInternal: mPeerSd=" + mPeerSd);
+        if (mPeerSd == null) {
+            throw new IllegalStateException("mPeerSd = null");
+        }
+
+        stopCall(DONT_RELEASE_SOCKET);
+        mInCall = true;
+
+        // Run exact the same logic in createAnswer() to setup mAudioStream.
+        SimpleSessionDescription offer =
+                new SimpleSessionDescription(mPeerSd);
+        AudioStream stream = mAudioStream;
+        AudioCodec codec = null;
+        for (Media media : offer.getMedia()) {
+            if ((codec == null) && (media.getPort() > 0)
+                    && "audio".equals(media.getType())
+                    && "RTP/AVP".equals(media.getProtocol())) {
+                // Find the first audio codec we supported.
+                for (int type : media.getRtpPayloadTypes()) {
+                    codec = AudioCodec.getCodec(
+                            type, media.getRtpmap(type), media.getFmtp(type));
+                    if (codec != null) {
+                        break;
+                    }
+                }
+
+                if (codec != null) {
+                    // Associate with the remote host.
+                    String address = media.getAddress();
+                    if (address == null) {
+                        address = offer.getAddress();
+                    }
+                    stream.associate(InetAddress.getByName(address),
+                            media.getPort());
+
+                    stream.setDtmfType(-1);
+                    stream.setCodec(codec);
+                    // Check if DTMF is supported in the same media.
+                    for (int type : media.getRtpPayloadTypes()) {
+                        String rtpmap = media.getRtpmap(type);
+                        if ((type != codec.type) && (rtpmap != null)
+                                && rtpmap.startsWith("telephone-event")) {
+                            stream.setDtmfType(type);
+                        }
+                    }
+
+                    // Handle recvonly and sendonly.
+                    if (mHold) {
+                        stream.setMode(RtpStream.MODE_NORMAL);
+                    } else if (media.getAttribute("recvonly") != null) {
+                        stream.setMode(RtpStream.MODE_SEND_ONLY);
+                    } else if(media.getAttribute("sendonly") != null) {
+                        stream.setMode(RtpStream.MODE_RECEIVE_ONLY);
+                    } else if(offer.getAttribute("recvonly") != null) {
+                        stream.setMode(RtpStream.MODE_SEND_ONLY);
+                    } else if(offer.getAttribute("sendonly") != null) {
+                        stream.setMode(RtpStream.MODE_RECEIVE_ONLY);
+                    } else {
+                        stream.setMode(RtpStream.MODE_NORMAL);
+                    }
+                    break;
+                }
+            }
+        }
+        if (codec == null) {
+            throw new IllegalStateException("Reject SDP: no suitable codecs");
+        }
+
+        if (isWifiOn()) grabWifiHighPerfLock();
+
+        // AudioGroup logic:
+        AudioGroup audioGroup = getAudioGroup();
+        if (mHold) {
+            // don't create an AudioGroup here; doing so will fail if
+            // there's another AudioGroup out there that's active
+        } else {
+            if (audioGroup == null) audioGroup = new AudioGroup();
+            stream.join(audioGroup);
+        }
+        setAudioGroupMode();
+    }
+
+    // set audio group mode based on current audio configuration
+    private void setAudioGroupMode() {
+        AudioGroup audioGroup = getAudioGroup();
+        if (DBG) log("setAudioGroupMode: audioGroup=" + audioGroup);
+        if (audioGroup != null) {
+            if (mHold) {
+                audioGroup.setMode(AudioGroup.MODE_ON_HOLD);
+            } else if (mMuted) {
+                audioGroup.setMode(AudioGroup.MODE_MUTED);
+            } else if (isSpeakerOn()) {
+                audioGroup.setMode(AudioGroup.MODE_ECHO_SUPPRESSION);
+            } else {
+                audioGroup.setMode(AudioGroup.MODE_NORMAL);
+            }
+        }
+    }
+
+    private void stopCall(boolean releaseSocket) {
+        if (DBG) log("stopCall: releaseSocket=" + releaseSocket);
+        releaseWifiHighPerfLock();
+        if (mAudioStream != null) {
+            mAudioStream.join(null);
+
+            if (releaseSocket) {
+                mAudioStream.release();
+                mAudioStream = null;
+            }
+        }
+    }
+
+    private String getLocalIp() {
+        return mSipSession.getLocalIp();
+    }
+
+    private void throwSipException(Throwable throwable) throws SipException {
+        if (throwable instanceof SipException) {
+            throw (SipException) throwable;
+        } else {
+            throw new SipException("", throwable);
+        }
+    }
+
+    private void log(String s) {
+        Rlog.d(LOG_TAG, s);
+    }
+
+    private void loge(String s) {
+        Rlog.e(LOG_TAG, s);
+    }
+
+    private void loge(String s, Throwable t) {
+        Rlog.e(LOG_TAG, s, t);
+    }
+}
diff --git a/android/net/sip/SipErrorCode.java b/android/net/sip/SipErrorCode.java
new file mode 100644
index 0000000..509728f
--- /dev/null
+++ b/android/net/sip/SipErrorCode.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2010 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.sip;
+
+/**
+ * Defines error codes returned during SIP actions. For example, during
+ * {@link SipRegistrationListener#onRegistrationFailed onRegistrationFailed()},
+ * {@link SipSession.Listener#onError onError()},
+ * {@link SipSession.Listener#onCallChangeFailed onCallChangeFailed()} and
+ * {@link SipSession.Listener#onRegistrationFailed onRegistrationFailed()}.
+ */
+public class SipErrorCode {
+    /** Not an error. */
+    public static final int NO_ERROR = 0;
+
+    /** When some socket error occurs. */
+    public static final int SOCKET_ERROR = -1;
+
+    /** When server responds with an error. */
+    public static final int SERVER_ERROR = -2;
+
+    /** When transaction is terminated unexpectedly. */
+    public static final int TRANSACTION_TERMINTED = -3;
+
+    /** When some error occurs on the device, possibly due to a bug. */
+    public static final int CLIENT_ERROR = -4;
+
+    /** When the transaction gets timed out. */
+    public static final int TIME_OUT = -5;
+
+    /** When the remote URI is not valid. */
+    public static final int INVALID_REMOTE_URI = -6;
+
+    /** When the peer is not reachable. */
+    public static final int PEER_NOT_REACHABLE = -7;
+
+    /** When invalid credentials are provided. */
+    public static final int INVALID_CREDENTIALS = -8;
+
+    /** The client is in a transaction and cannot initiate a new one. */
+    public static final int IN_PROGRESS = -9;
+
+    /** When data connection is lost. */
+    public static final int DATA_CONNECTION_LOST = -10;
+
+    /** Cross-domain authentication required. */
+    public static final int CROSS_DOMAIN_AUTHENTICATION = -11;
+
+    /** When the server is not reachable. */
+    public static final int SERVER_UNREACHABLE = -12;
+
+    public static String toString(int errorCode) {
+        switch (errorCode) {
+            case NO_ERROR:
+                return "NO_ERROR";
+            case SOCKET_ERROR:
+                return "SOCKET_ERROR";
+            case SERVER_ERROR:
+                return "SERVER_ERROR";
+            case TRANSACTION_TERMINTED:
+                return "TRANSACTION_TERMINTED";
+            case CLIENT_ERROR:
+                return "CLIENT_ERROR";
+            case TIME_OUT:
+                return "TIME_OUT";
+            case INVALID_REMOTE_URI:
+                return "INVALID_REMOTE_URI";
+            case PEER_NOT_REACHABLE:
+                return "PEER_NOT_REACHABLE";
+            case INVALID_CREDENTIALS:
+                return "INVALID_CREDENTIALS";
+            case IN_PROGRESS:
+                return "IN_PROGRESS";
+            case DATA_CONNECTION_LOST:
+                return "DATA_CONNECTION_LOST";
+            case CROSS_DOMAIN_AUTHENTICATION:
+                return "CROSS_DOMAIN_AUTHENTICATION";
+            case SERVER_UNREACHABLE:
+                return "SERVER_UNREACHABLE";
+            default:
+                return "UNKNOWN";
+        }
+    }
+
+    private SipErrorCode() {
+    }
+}
diff --git a/android/net/sip/SipException.java b/android/net/sip/SipException.java
new file mode 100644
index 0000000..0339395
--- /dev/null
+++ b/android/net/sip/SipException.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2010 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.sip;
+
+/**
+ * Indicates a general SIP-related exception.
+ */
+public class SipException extends Exception {
+    public SipException() {
+    }
+
+    public SipException(String message) {
+        super(message);
+    }
+
+    public SipException(String message, Throwable cause) {
+        // we want to eliminate the dependency on javax.sip.SipException
+        super(message, ((cause instanceof javax.sip.SipException)
+                && (cause.getCause() != null))
+                ? cause.getCause()
+                : cause);
+    }
+}
diff --git a/android/net/sip/SipManager.java b/android/net/sip/SipManager.java
new file mode 100644
index 0000000..0cb1feb
--- /dev/null
+++ b/android/net/sip/SipManager.java
@@ -0,0 +1,661 @@
+/*
+ * Copyright (C) 2010 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.sip;
+
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.telephony.Rlog;
+
+import java.text.ParseException;
+
+/**
+ * Provides APIs for SIP tasks, such as initiating SIP connections, and provides access to related
+ * SIP services. This class is the starting point for any SIP actions. You can acquire an instance
+ * of it with {@link #newInstance newInstance()}.</p>
+ * <p>The APIs in this class allows you to:</p>
+ * <ul>
+ * <li>Create a {@link SipSession} to get ready for making calls or listen for incoming calls. See
+ * {@link #createSipSession createSipSession()} and {@link #getSessionFor getSessionFor()}.</li>
+ * <li>Initiate and receive generic SIP calls or audio-only SIP calls. Generic SIP calls may
+ * be video, audio, or other, and are initiated with {@link #open open()}. Audio-only SIP calls
+ * should be handled with a {@link SipAudioCall}, which you can acquire with {@link
+ * #makeAudioCall makeAudioCall()} and {@link #takeAudioCall takeAudioCall()}.</li>
+ * <li>Register and unregister with a SIP service provider, with
+ *      {@link #register register()} and {@link #unregister unregister()}.</li>
+ * <li>Verify session connectivity, with {@link #isOpened isOpened()} and
+ *      {@link #isRegistered isRegistered()}.</li>
+ * </ul>
+ * <p class="note"><strong>Note:</strong> Not all Android-powered devices support VOIP calls using
+ * SIP. You should always call {@link android.net.sip.SipManager#isVoipSupported
+ * isVoipSupported()} to verify that the device supports VOIP calling and {@link
+ * android.net.sip.SipManager#isApiSupported isApiSupported()} to verify that the device supports
+ * the SIP APIs. Your application must also request the {@link
+ * android.Manifest.permission#INTERNET} and {@link android.Manifest.permission#USE_SIP}
+ * permissions.</p>
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about using SIP, read the
+ * <a href="{@docRoot}guide/topics/network/sip.html">Session Initiation Protocol</a>
+ * developer guide.</p>
+ * </div>
+ */
+public class SipManager {
+    /**
+     * The result code to be sent back with the incoming call
+     * {@link PendingIntent}.
+     * @see #open(SipProfile, PendingIntent, SipRegistrationListener)
+     */
+    public static final int INCOMING_CALL_RESULT_CODE = 101;
+
+    /**
+     * Key to retrieve the call ID from an incoming call intent.
+     * @see #open(SipProfile, PendingIntent, SipRegistrationListener)
+     */
+    public static final String EXTRA_CALL_ID = "android:sipCallID";
+
+    /**
+     * Key to retrieve the offered session description from an incoming call
+     * intent.
+     * @see #open(SipProfile, PendingIntent, SipRegistrationListener)
+     */
+    public static final String EXTRA_OFFER_SD = "android:sipOfferSD";
+
+    /**
+     * Action to broadcast when SipService is up.
+     * Internal use only.
+     * @hide
+     */
+    public static final String ACTION_SIP_SERVICE_UP =
+            "android.net.sip.SIP_SERVICE_UP";
+    /**
+     * Action string for the incoming call intent for the Phone app.
+     * Internal use only.
+     * @hide
+     */
+    public static final String ACTION_SIP_INCOMING_CALL =
+            "com.android.phone.SIP_INCOMING_CALL";
+    /**
+     * Action string for the add-phone intent.
+     * Internal use only.
+     * @hide
+     */
+    public static final String ACTION_SIP_ADD_PHONE =
+            "com.android.phone.SIP_ADD_PHONE";
+    /**
+     * Action string for the remove-phone intent.
+     * Internal use only.
+     * @hide
+     */
+    public static final String ACTION_SIP_REMOVE_PHONE =
+            "com.android.phone.SIP_REMOVE_PHONE";
+
+    /**
+     * Action string for the SIP call option configuration changed intent.
+     * This is used to communicate  change to the SIP call option, triggering re-registration of
+     * the SIP phone accounts.
+     * Internal use only.
+     * @hide
+     */
+    public static final String ACTION_SIP_CALL_OPTION_CHANGED =
+            "com.android.phone.SIP_CALL_OPTION_CHANGED";
+
+    /**
+     * Part of the ACTION_SIP_ADD_PHONE and ACTION_SIP_REMOVE_PHONE intents.
+     * Internal use only.
+     * @hide
+     */
+    public static final String EXTRA_LOCAL_URI = "android:localSipUri";
+
+    private static final String TAG = "SipManager";
+
+    private ISipService mSipService;
+    private Context mContext;
+
+    /**
+     * Creates a manager instance. Returns null if SIP API is not supported.
+     *
+     * @param context application context for creating the manager object
+     * @return the manager instance or null if SIP API is not supported
+     */
+    public static SipManager newInstance(Context context) {
+        return (isApiSupported(context) ? new SipManager(context) : null);
+    }
+
+    /**
+     * Returns true if the SIP API is supported by the system.
+     */
+    public static boolean isApiSupported(Context context) {
+        return context.getPackageManager().hasSystemFeature(
+                PackageManager.FEATURE_SIP);
+    }
+
+    /**
+     * Returns true if the system supports SIP-based VOIP API.
+     */
+    public static boolean isVoipSupported(Context context) {
+        return context.getPackageManager().hasSystemFeature(
+                PackageManager.FEATURE_SIP_VOIP) && isApiSupported(context);
+    }
+
+    /**
+     * Returns true if SIP is only available on WIFI.
+     */
+    public static boolean isSipWifiOnly(Context context) {
+        return context.getResources().getBoolean(
+                com.android.internal.R.bool.config_sip_wifi_only);
+    }
+
+    private SipManager(Context context) {
+        mContext = context;
+        createSipService();
+    }
+
+    private void createSipService() {
+        if (mSipService == null) {
+            IBinder b = ServiceManager.getService(Context.SIP_SERVICE);
+            mSipService = ISipService.Stub.asInterface(b);
+        }
+    }
+
+    private void checkSipServiceConnection() throws SipException {
+        createSipService();
+        if (mSipService == null) {
+            throw new SipException("SipService is dead and is restarting...", new Exception());
+        }
+    }
+
+    /**
+     * Opens the profile for making generic SIP calls. The caller may make subsequent calls
+     * through {@link #makeAudioCall}. If one also wants to receive calls on the
+     * profile, use
+     * {@link #open(SipProfile, PendingIntent, SipRegistrationListener)}
+     * instead.
+     *
+     * @param localProfile the SIP profile to make calls from
+     * @throws SipException if the profile contains incorrect settings or
+     *      calling the SIP service results in an error
+     */
+    public void open(SipProfile localProfile) throws SipException {
+        try {
+            checkSipServiceConnection();
+            mSipService.open(localProfile, mContext.getOpPackageName());
+        } catch (RemoteException e) {
+            throw new SipException("open()", e);
+        }
+    }
+
+    /**
+     * Opens the profile for making calls and/or receiving generic SIP calls. The caller may
+     * make subsequent calls through {@link #makeAudioCall}. If the
+     * auto-registration option is enabled in the profile, the SIP service
+     * will register the profile to the corresponding SIP provider periodically
+     * in order to receive calls from the provider. When the SIP service
+     * receives a new call, it will send out an intent with the provided action
+     * string. The intent contains a call ID extra and an offer session
+     * description string extra. Use {@link #getCallId} and
+     * {@link #getOfferSessionDescription} to retrieve those extras.
+     *
+     * @param localProfile the SIP profile to receive incoming calls for
+     * @param incomingCallPendingIntent When an incoming call is received, the
+     *      SIP service will call
+     *      {@link PendingIntent#send(Context, int, Intent)} to send back the
+     *      intent to the caller with {@link #INCOMING_CALL_RESULT_CODE} as the
+     *      result code and the intent to fill in the call ID and session
+     *      description information. It cannot be null.
+     * @param listener to listen to registration events; can be null
+     * @see #getCallId
+     * @see #getOfferSessionDescription
+     * @see #takeAudioCall
+     * @throws NullPointerException if {@code incomingCallPendingIntent} is null
+     * @throws SipException if the profile contains incorrect settings or
+     *      calling the SIP service results in an error
+     * @see #isIncomingCallIntent
+     * @see #getCallId
+     * @see #getOfferSessionDescription
+     */
+    public void open(SipProfile localProfile,
+            PendingIntent incomingCallPendingIntent,
+            SipRegistrationListener listener) throws SipException {
+        if (incomingCallPendingIntent == null) {
+            throw new NullPointerException(
+                    "incomingCallPendingIntent cannot be null");
+        }
+        try {
+            checkSipServiceConnection();
+            mSipService.open3(localProfile, incomingCallPendingIntent,
+                    createRelay(listener, localProfile.getUriString()),
+                    mContext.getOpPackageName());
+        } catch (RemoteException e) {
+            throw new SipException("open()", e);
+        }
+    }
+
+    /**
+     * Sets the listener to listen to registration events. No effect if the
+     * profile has not been opened to receive calls (see
+     * {@link #open(SipProfile, PendingIntent, SipRegistrationListener)}).
+     *
+     * @param localProfileUri the URI of the profile
+     * @param listener to listen to registration events; can be null
+     * @throws SipException if calling the SIP service results in an error
+     */
+    public void setRegistrationListener(String localProfileUri,
+            SipRegistrationListener listener) throws SipException {
+        try {
+            checkSipServiceConnection();
+            mSipService.setRegistrationListener(
+                    localProfileUri, createRelay(listener, localProfileUri),
+                    mContext.getOpPackageName());
+        } catch (RemoteException e) {
+            throw new SipException("setRegistrationListener()", e);
+        }
+    }
+
+    /**
+     * Closes the specified profile to not make/receive calls. All the resources
+     * that were allocated to the profile are also released.
+     *
+     * @param localProfileUri the URI of the profile to close
+     * @throws SipException if calling the SIP service results in an error
+     */
+    public void close(String localProfileUri) throws SipException {
+        try {
+            checkSipServiceConnection();
+            mSipService.close(localProfileUri, mContext.getOpPackageName());
+        } catch (RemoteException e) {
+            throw new SipException("close()", e);
+        }
+    }
+
+    /**
+     * Checks if the specified profile is opened in the SIP service for
+     * making and/or receiving calls.
+     *
+     * @param localProfileUri the URI of the profile in question
+     * @return true if the profile is enabled to receive calls
+     * @throws SipException if calling the SIP service results in an error
+     */
+    public boolean isOpened(String localProfileUri) throws SipException {
+        try {
+            checkSipServiceConnection();
+            return mSipService.isOpened(localProfileUri, mContext.getOpPackageName());
+        } catch (RemoteException e) {
+            throw new SipException("isOpened()", e);
+        }
+    }
+
+    /**
+     * Checks if the SIP service has successfully registered the profile to the
+     * SIP provider (specified in the profile) for receiving calls. Returning
+     * true from this method also implies the profile is opened
+     * ({@link #isOpened}).
+     *
+     * @param localProfileUri the URI of the profile in question
+     * @return true if the profile is registered to the SIP provider; false if
+     *        the profile has not been opened in the SIP service or the SIP
+     *        service has not yet successfully registered the profile to the SIP
+     *        provider
+     * @throws SipException if calling the SIP service results in an error
+     */
+    public boolean isRegistered(String localProfileUri) throws SipException {
+        try {
+            checkSipServiceConnection();
+            return mSipService.isRegistered(localProfileUri, mContext.getOpPackageName());
+        } catch (RemoteException e) {
+            throw new SipException("isRegistered()", e);
+        }
+    }
+
+    /**
+     * Creates a {@link SipAudioCall} to make a call. The attempt will be timed
+     * out if the call is not established within {@code timeout} seconds and
+     * {@link SipAudioCall.Listener#onError onError(SipAudioCall, SipErrorCode.TIME_OUT, String)}
+     * will be called.
+     *
+     * @param localProfile the SIP profile to make the call from
+     * @param peerProfile the SIP profile to make the call to
+     * @param listener to listen to the call events from {@link SipAudioCall};
+     *      can be null
+     * @param timeout the timeout value in seconds. Default value (defined by
+     *        SIP protocol) is used if {@code timeout} is zero or negative.
+     * @return a {@link SipAudioCall} object
+     * @throws SipException if calling the SIP service results in an error or
+     *      VOIP API is not supported by the device
+     * @see SipAudioCall.Listener#onError
+     * @see #isVoipSupported
+     */
+    public SipAudioCall makeAudioCall(SipProfile localProfile,
+            SipProfile peerProfile, SipAudioCall.Listener listener, int timeout)
+            throws SipException {
+        if (!isVoipSupported(mContext)) {
+            throw new SipException("VOIP API is not supported");
+        }
+        SipAudioCall call = new SipAudioCall(mContext, localProfile);
+        call.setListener(listener);
+        SipSession s = createSipSession(localProfile, null);
+        call.makeCall(peerProfile, s, timeout);
+        return call;
+    }
+
+    /**
+     * Creates a {@link SipAudioCall} to make an audio call. The attempt will be
+     * timed out if the call is not established within {@code timeout} seconds
+     * and
+     * {@link SipAudioCall.Listener#onError onError(SipAudioCall, SipErrorCode.TIME_OUT, String)}
+     * will be called.
+     *
+     * @param localProfileUri URI of the SIP profile to make the call from
+     * @param peerProfileUri URI of the SIP profile to make the call to
+     * @param listener to listen to the call events from {@link SipAudioCall};
+     *      can be null
+     * @param timeout the timeout value in seconds. Default value (defined by
+     *        SIP protocol) is used if {@code timeout} is zero or negative.
+     * @return a {@link SipAudioCall} object
+     * @throws SipException if calling the SIP service results in an error or
+     *      VOIP API is not supported by the device
+     * @see SipAudioCall.Listener#onError
+     * @see #isVoipSupported
+     */
+    public SipAudioCall makeAudioCall(String localProfileUri,
+            String peerProfileUri, SipAudioCall.Listener listener, int timeout)
+            throws SipException {
+        if (!isVoipSupported(mContext)) {
+            throw new SipException("VOIP API is not supported");
+        }
+        try {
+            return makeAudioCall(
+                    new SipProfile.Builder(localProfileUri).build(),
+                    new SipProfile.Builder(peerProfileUri).build(), listener,
+                    timeout);
+        } catch (ParseException e) {
+            throw new SipException("build SipProfile", e);
+        }
+    }
+
+    /**
+     * Creates a {@link SipAudioCall} to take an incoming call. Before the call
+     * is returned, the listener will receive a
+     * {@link SipAudioCall.Listener#onRinging}
+     * callback.
+     *
+     * @param incomingCallIntent the incoming call broadcast intent
+     * @param listener to listen to the call events from {@link SipAudioCall};
+     *      can be null
+     * @return a {@link SipAudioCall} object
+     * @throws SipException if calling the SIP service results in an error
+     */
+    public SipAudioCall takeAudioCall(Intent incomingCallIntent,
+            SipAudioCall.Listener listener) throws SipException {
+        if (incomingCallIntent == null) {
+            throw new SipException("Cannot retrieve session with null intent");
+        }
+
+        String callId = getCallId(incomingCallIntent);
+        if (callId == null) {
+            throw new SipException("Call ID missing in incoming call intent");
+        }
+
+        String offerSd = getOfferSessionDescription(incomingCallIntent);
+        if (offerSd == null) {
+            throw new SipException("Session description missing in incoming "
+                    + "call intent");
+        }
+
+        try {
+            checkSipServiceConnection();
+            ISipSession session = mSipService.getPendingSession(callId,
+                    mContext.getOpPackageName());
+            if (session == null) {
+                throw new SipException("No pending session for the call");
+            }
+            SipAudioCall call = new SipAudioCall(
+                    mContext, session.getLocalProfile());
+            call.attachCall(new SipSession(session), offerSd);
+            call.setListener(listener);
+            return call;
+        } catch (Throwable t) {
+            throw new SipException("takeAudioCall()", t);
+        }
+    }
+
+    /**
+     * Checks if the intent is an incoming call broadcast intent.
+     *
+     * @param intent the intent in question
+     * @return true if the intent is an incoming call broadcast intent
+     */
+    public static boolean isIncomingCallIntent(Intent intent) {
+        if (intent == null) return false;
+        String callId = getCallId(intent);
+        String offerSd = getOfferSessionDescription(intent);
+        return ((callId != null) && (offerSd != null));
+    }
+
+    /**
+     * Gets the call ID from the specified incoming call broadcast intent.
+     *
+     * @param incomingCallIntent the incoming call broadcast intent
+     * @return the call ID or null if the intent does not contain it
+     */
+    public static String getCallId(Intent incomingCallIntent) {
+        return incomingCallIntent.getStringExtra(EXTRA_CALL_ID);
+    }
+
+    /**
+     * Gets the offer session description from the specified incoming call
+     * broadcast intent.
+     *
+     * @param incomingCallIntent the incoming call broadcast intent
+     * @return the offer session description or null if the intent does not
+     *      have it
+     */
+    public static String getOfferSessionDescription(Intent incomingCallIntent) {
+        return incomingCallIntent.getStringExtra(EXTRA_OFFER_SD);
+    }
+
+    /**
+     * Creates an incoming call broadcast intent.
+     *
+     * @param callId the call ID of the incoming call
+     * @param sessionDescription the session description of the incoming call
+     * @return the incoming call intent
+     * @hide
+     */
+    public static Intent createIncomingCallBroadcast(String callId,
+            String sessionDescription) {
+        Intent intent = new Intent();
+        intent.putExtra(EXTRA_CALL_ID, callId);
+        intent.putExtra(EXTRA_OFFER_SD, sessionDescription);
+        return intent;
+    }
+
+    /**
+     * Manually registers the profile to the corresponding SIP provider for
+     * receiving calls.
+     * {@link #open(SipProfile, PendingIntent, SipRegistrationListener)} is
+     * still needed to be called at least once in order for the SIP service to
+     * notify the caller with the {@link android.app.PendingIntent} when an incoming call is
+     * received.
+     *
+     * @param localProfile the SIP profile to register with
+     * @param expiryTime registration expiration time (in seconds)
+     * @param listener to listen to the registration events
+     * @throws SipException if calling the SIP service results in an error
+     */
+    public void register(SipProfile localProfile, int expiryTime,
+            SipRegistrationListener listener) throws SipException {
+        try {
+            checkSipServiceConnection();
+            ISipSession session = mSipService.createSession(localProfile,
+                    createRelay(listener, localProfile.getUriString()),
+                    mContext.getOpPackageName());
+            if (session == null) {
+                throw new SipException(
+                        "SipService.createSession() returns null");
+            }
+            session.register(expiryTime);
+        } catch (RemoteException e) {
+            throw new SipException("register()", e);
+        }
+    }
+
+    /**
+     * Manually unregisters the profile from the corresponding SIP provider for
+     * stop receiving further calls. This may interference with the auto
+     * registration process in the SIP service if the auto-registration option
+     * in the profile is enabled.
+     *
+     * @param localProfile the SIP profile to register with
+     * @param listener to listen to the registration events
+     * @throws SipException if calling the SIP service results in an error
+     */
+    public void unregister(SipProfile localProfile,
+            SipRegistrationListener listener) throws SipException {
+        try {
+            checkSipServiceConnection();
+            ISipSession session = mSipService.createSession(localProfile,
+                    createRelay(listener, localProfile.getUriString()),
+                    mContext.getOpPackageName());
+            if (session == null) {
+                throw new SipException(
+                        "SipService.createSession() returns null");
+            }
+            session.unregister();
+        } catch (RemoteException e) {
+            throw new SipException("unregister()", e);
+        }
+    }
+
+    /**
+     * Gets the {@link SipSession} that handles the incoming call. For audio
+     * calls, consider to use {@link SipAudioCall} to handle the incoming call.
+     * See {@link #takeAudioCall}. Note that the method may be called only once
+     * for the same intent. For subsequent calls on the same intent, the method
+     * returns null.
+     *
+     * @param incomingCallIntent the incoming call broadcast intent
+     * @return the session object that handles the incoming call
+     */
+    public SipSession getSessionFor(Intent incomingCallIntent)
+            throws SipException {
+        try {
+            checkSipServiceConnection();
+            String callId = getCallId(incomingCallIntent);
+            ISipSession s = mSipService.getPendingSession(callId,
+                    mContext.getOpPackageName());
+            return ((s == null) ? null : new SipSession(s));
+        } catch (RemoteException e) {
+            throw new SipException("getSessionFor()", e);
+        }
+    }
+
+    private static ISipSessionListener createRelay(
+            SipRegistrationListener listener, String uri) {
+        return ((listener == null) ? null : new ListenerRelay(listener, uri));
+    }
+
+    /**
+     * Creates a {@link SipSession} with the specified profile. Use other
+     * methods, if applicable, instead of interacting with {@link SipSession}
+     * directly.
+     *
+     * @param localProfile the SIP profile the session is associated with
+     * @param listener to listen to SIP session events
+     */
+    public SipSession createSipSession(SipProfile localProfile,
+            SipSession.Listener listener) throws SipException {
+        try {
+            checkSipServiceConnection();
+            ISipSession s = mSipService.createSession(localProfile, null,
+                    mContext.getOpPackageName());
+            if (s == null) {
+                throw new SipException(
+                        "Failed to create SipSession; network unavailable?");
+            }
+            return new SipSession(s, listener);
+        } catch (RemoteException e) {
+            throw new SipException("createSipSession()", e);
+        }
+    }
+
+    /**
+     * Gets the list of profiles hosted by the SIP service. The user information
+     * (username, password and display name) are crossed out.
+     * @hide
+     */
+    public SipProfile[] getListOfProfiles() throws SipException {
+        try {
+            checkSipServiceConnection();
+            return mSipService.getListOfProfiles(mContext.getOpPackageName());
+        } catch (RemoteException e) {
+            return new SipProfile[0];
+        }
+    }
+
+    private static class ListenerRelay extends SipSessionAdapter {
+        private SipRegistrationListener mListener;
+        private String mUri;
+
+        // listener must not be null
+        public ListenerRelay(SipRegistrationListener listener, String uri) {
+            mListener = listener;
+            mUri = uri;
+        }
+
+        private String getUri(ISipSession session) {
+            try {
+                return ((session == null)
+                        ? mUri
+                        : session.getLocalProfile().getUriString());
+            } catch (Throwable e) {
+                // SipService died? SIP stack died?
+                Rlog.e(TAG, "getUri(): ", e);
+                return null;
+            }
+        }
+
+        @Override
+        public void onRegistering(ISipSession session) {
+            mListener.onRegistering(getUri(session));
+        }
+
+        @Override
+        public void onRegistrationDone(ISipSession session, int duration) {
+            long expiryTime = duration;
+            if (duration > 0) expiryTime += System.currentTimeMillis();
+            mListener.onRegistrationDone(getUri(session), expiryTime);
+        }
+
+        @Override
+        public void onRegistrationFailed(ISipSession session, int errorCode,
+                String message) {
+            mListener.onRegistrationFailed(getUri(session), errorCode, message);
+        }
+
+        @Override
+        public void onRegistrationTimeout(ISipSession session) {
+            mListener.onRegistrationFailed(getUri(session),
+                    SipErrorCode.TIME_OUT, "registration timed out");
+        }
+    }
+}
diff --git a/android/net/sip/SipProfile.java b/android/net/sip/SipProfile.java
new file mode 100644
index 0000000..0ef754c
--- /dev/null
+++ b/android/net/sip/SipProfile.java
@@ -0,0 +1,502 @@
+/*
+ * Copyright (C) 2010 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.sip;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import java.io.ObjectStreamException;
+import java.io.Serializable;
+import java.text.ParseException;
+import javax.sip.InvalidArgumentException;
+import javax.sip.ListeningPoint;
+import javax.sip.PeerUnavailableException;
+import javax.sip.SipFactory;
+import javax.sip.address.Address;
+import javax.sip.address.AddressFactory;
+import javax.sip.address.SipURI;
+import javax.sip.address.URI;
+
+/**
+ * Defines a SIP profile, including a SIP account, domain and server information.
+ * <p>You can create a {@link SipProfile} using {@link
+ * SipProfile.Builder}. You can also retrieve one from a {@link SipSession}, using {@link
+ * SipSession#getLocalProfile} and {@link SipSession#getPeerProfile}.</p>
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about using SIP, read the
+ * <a href="{@docRoot}guide/topics/network/sip.html">Session Initiation Protocol</a>
+ * developer guide.</p>
+ * </div>
+ */
+public class SipProfile implements Parcelable, Serializable, Cloneable {
+    private static final long serialVersionUID = 1L;
+    private static final int DEFAULT_PORT = 5060;
+    private static final String TCP = "TCP";
+    private static final String UDP = "UDP";
+    private Address mAddress;
+    private String mProxyAddress;
+    private String mPassword;
+    private String mDomain;
+    private String mProtocol = UDP;
+    private String mProfileName;
+    private String mAuthUserName;
+    private int mPort = DEFAULT_PORT;
+    private boolean mSendKeepAlive = false;
+    private boolean mAutoRegistration = true;
+    private transient int mCallingUid = 0;
+
+    public static final Parcelable.Creator<SipProfile> CREATOR =
+            new Parcelable.Creator<SipProfile>() {
+                public SipProfile createFromParcel(Parcel in) {
+                    return new SipProfile(in);
+                }
+
+                public SipProfile[] newArray(int size) {
+                    return new SipProfile[size];
+                }
+            };
+
+    /**
+     * Helper class for creating a {@link SipProfile}.
+     */
+    public static class Builder {
+        private AddressFactory mAddressFactory;
+        private SipProfile mProfile = new SipProfile();
+        private SipURI mUri;
+        private String mDisplayName;
+        private String mProxyAddress;
+
+        {
+            try {
+                mAddressFactory =
+                        SipFactory.getInstance().createAddressFactory();
+            } catch (PeerUnavailableException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        /**
+         * Creates a builder based on the given profile.
+         */
+        public Builder(SipProfile profile) {
+            if (profile == null) throw new NullPointerException();
+            try {
+                mProfile = (SipProfile) profile.clone();
+            } catch (CloneNotSupportedException e) {
+                throw new RuntimeException("should not occur", e);
+            }
+            mProfile.mAddress = null;
+            mUri = profile.getUri();
+            mUri.setUserPassword(profile.getPassword());
+            mDisplayName = profile.getDisplayName();
+            mProxyAddress = profile.getProxyAddress();
+            mProfile.mPort = profile.getPort();
+        }
+
+        /**
+         * Constructor.
+         *
+         * @param uriString the URI string as "sip:<user_name>@<domain>"
+         * @throws ParseException if the string is not a valid URI
+         */
+        public Builder(String uriString) throws ParseException {
+            if (uriString == null) {
+                throw new NullPointerException("uriString cannot be null");
+            }
+            URI uri = mAddressFactory.createURI(fix(uriString));
+            if (uri instanceof SipURI) {
+                mUri = (SipURI) uri;
+            } else {
+                throw new ParseException(uriString + " is not a SIP URI", 0);
+            }
+            mProfile.mDomain = mUri.getHost();
+        }
+
+        /**
+         * Constructor.
+         *
+         * @param username username of the SIP account
+         * @param serverDomain the SIP server domain; if the network address
+         *      is different from the domain, use {@link #setOutboundProxy} to
+         *      set server address
+         * @throws ParseException if the parameters are not valid
+         */
+        public Builder(String username, String serverDomain)
+                throws ParseException {
+            if ((username == null) || (serverDomain == null)) {
+                throw new NullPointerException(
+                        "username and serverDomain cannot be null");
+            }
+            mUri = mAddressFactory.createSipURI(username, serverDomain);
+            mProfile.mDomain = serverDomain;
+        }
+
+        private String fix(String uriString) {
+            return (uriString.trim().toLowerCase().startsWith("sip:")
+                    ? uriString
+                    : "sip:" + uriString);
+        }
+
+        /**
+         * Sets the username used for authentication.
+         *
+         * @param name authentication username of the profile
+         * @return this builder object
+         */
+        public Builder setAuthUserName(String name) {
+            mProfile.mAuthUserName = name;
+            return this;
+        }
+
+        /**
+         * Sets the name of the profile. This name is given by user.
+         *
+         * @param name name of the profile
+         * @return this builder object
+         */
+        public Builder setProfileName(String name) {
+            mProfile.mProfileName = name;
+            return this;
+        }
+
+        /**
+         * Sets the password of the SIP account
+         *
+         * @param password password of the SIP account
+         * @return this builder object
+         */
+        public Builder setPassword(String password) {
+            mUri.setUserPassword(password);
+            return this;
+        }
+
+        /**
+         * Sets the port number of the server. By default, it is 5060.
+         *
+         * @param port port number of the server
+         * @return this builder object
+         * @throws IllegalArgumentException if the port number is out of range
+         */
+        public Builder setPort(int port) throws IllegalArgumentException {
+            if ((port > 65535) || (port < 1000)) {
+                throw new IllegalArgumentException("incorrect port arugment: " + port);
+            }
+            mProfile.mPort = port;
+            return this;
+        }
+
+        /**
+         * Sets the protocol used to connect to the SIP server. Currently,
+         * only "UDP" and "TCP" are supported.
+         *
+         * @param protocol the protocol string
+         * @return this builder object
+         * @throws IllegalArgumentException if the protocol is not recognized
+         */
+        public Builder setProtocol(String protocol)
+                throws IllegalArgumentException {
+            if (protocol == null) {
+                throw new NullPointerException("protocol cannot be null");
+            }
+            protocol = protocol.toUpperCase();
+            if (!protocol.equals(UDP) && !protocol.equals(TCP)) {
+                throw new IllegalArgumentException(
+                        "unsupported protocol: " + protocol);
+            }
+            mProfile.mProtocol = protocol;
+            return this;
+        }
+
+        /**
+         * Sets the outbound proxy of the SIP server.
+         *
+         * @param outboundProxy the network address of the outbound proxy
+         * @return this builder object
+         */
+        public Builder setOutboundProxy(String outboundProxy) {
+            mProxyAddress = outboundProxy;
+            return this;
+        }
+
+        /**
+         * Sets the display name of the user.
+         *
+         * @param displayName display name of the user
+         * @return this builder object
+         */
+        public Builder setDisplayName(String displayName) {
+            mDisplayName = displayName;
+            return this;
+        }
+
+        /**
+         * Sets the send keep-alive flag.
+         *
+         * @param flag true if sending keep-alive message is required,
+         *      false otherwise
+         * @return this builder object
+         */
+        public Builder setSendKeepAlive(boolean flag) {
+            mProfile.mSendKeepAlive = flag;
+            return this;
+        }
+
+
+        /**
+         * Sets the auto. registration flag.
+         *
+         * @param flag true if the profile will be registered automatically,
+         *      false otherwise
+         * @return this builder object
+         */
+        public Builder setAutoRegistration(boolean flag) {
+            mProfile.mAutoRegistration = flag;
+            return this;
+        }
+
+        /**
+         * Builds and returns the SIP profile object.
+         *
+         * @return the profile object created
+         */
+        public SipProfile build() {
+            // remove password from URI
+            mProfile.mPassword = mUri.getUserPassword();
+            mUri.setUserPassword(null);
+            try {
+                if (!TextUtils.isEmpty(mProxyAddress)) {
+                    SipURI uri = (SipURI)
+                            mAddressFactory.createURI(fix(mProxyAddress));
+                    mProfile.mProxyAddress = uri.getHost();
+                } else {
+                    if (!mProfile.mProtocol.equals(UDP)) {
+                        mUri.setTransportParam(mProfile.mProtocol);
+                    }
+                    if (mProfile.mPort != DEFAULT_PORT) {
+                        mUri.setPort(mProfile.mPort);
+                    }
+                }
+                mProfile.mAddress = mAddressFactory.createAddress(
+                        mDisplayName, mUri);
+            } catch (InvalidArgumentException e) {
+                throw new RuntimeException(e);
+            } catch (ParseException e) {
+                // must not occur
+                throw new RuntimeException(e);
+            }
+            return mProfile;
+        }
+    }
+
+    private SipProfile() {
+    }
+
+    private SipProfile(Parcel in) {
+        mAddress = (Address) in.readSerializable();
+        mProxyAddress = in.readString();
+        mPassword = in.readString();
+        mDomain = in.readString();
+        mProtocol = in.readString();
+        mProfileName = in.readString();
+        mSendKeepAlive = (in.readInt() == 0) ? false : true;
+        mAutoRegistration = (in.readInt() == 0) ? false : true;
+        mCallingUid = in.readInt();
+        mPort = in.readInt();
+        mAuthUserName = in.readString();
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeSerializable(mAddress);
+        out.writeString(mProxyAddress);
+        out.writeString(mPassword);
+        out.writeString(mDomain);
+        out.writeString(mProtocol);
+        out.writeString(mProfileName);
+        out.writeInt(mSendKeepAlive ? 1 : 0);
+        out.writeInt(mAutoRegistration ? 1 : 0);
+        out.writeInt(mCallingUid);
+        out.writeInt(mPort);
+        out.writeString(mAuthUserName);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * Gets the SIP URI of this profile.
+     *
+     * @return the SIP URI of this profile
+     * @hide
+     */
+    public SipURI getUri() {
+        return (SipURI) mAddress.getURI();
+    }
+
+    /**
+     * Gets the SIP URI string of this profile.
+     *
+     * @return the SIP URI string of this profile
+     */
+    public String getUriString() {
+        // We need to return the sip uri domain instead of
+        // the SIP URI with transport, port information if
+        // the outbound proxy address exists.
+        if (!TextUtils.isEmpty(mProxyAddress)) {
+            return "sip:" + getUserName() + "@" + mDomain;
+        }
+        return getUri().toString();
+    }
+
+    /**
+     * Gets the SIP address of this profile.
+     *
+     * @return the SIP address of this profile
+     * @hide
+     */
+    public Address getSipAddress() {
+        return mAddress;
+    }
+
+    /**
+     * Gets the display name of the user.
+     *
+     * @return the display name of the user
+     */
+    public String getDisplayName() {
+        return mAddress.getDisplayName();
+    }
+
+    /**
+     * Gets the username.
+     *
+     * @return the username
+     */
+    public String getUserName() {
+        return getUri().getUser();
+    }
+
+    /**
+     * Gets the username for authentication. If it is null, then the username
+     * is used in authentication instead.
+     *
+     * @return the authentication username
+     * @see #getUserName
+     */
+    public String getAuthUserName() {
+        return mAuthUserName;
+    }
+
+    /**
+     * Gets the password.
+     *
+     * @return the password
+     */
+    public String getPassword() {
+        return mPassword;
+    }
+
+    /**
+     * Gets the SIP domain.
+     *
+     * @return the SIP domain
+     */
+    public String getSipDomain() {
+        return mDomain;
+    }
+
+    /**
+     * Gets the port number of the SIP server.
+     *
+     * @return the port number of the SIP server
+     */
+    public int getPort() {
+        return mPort;
+    }
+
+    /**
+     * Gets the protocol used to connect to the server.
+     *
+     * @return the protocol
+     */
+    public String getProtocol() {
+        return mProtocol;
+    }
+
+    /**
+     * Gets the network address of the server outbound proxy.
+     *
+     * @return the network address of the server outbound proxy
+     */
+    public String getProxyAddress() {
+        return mProxyAddress;
+    }
+
+    /**
+     * Gets the (user-defined) name of the profile.
+     *
+     * @return name of the profile
+     */
+    public String getProfileName() {
+        return mProfileName;
+    }
+
+    /**
+     * Gets the flag of 'Sending keep-alive'.
+     *
+     * @return the flag of sending SIP keep-alive messages.
+     */
+    public boolean getSendKeepAlive() {
+        return mSendKeepAlive;
+    }
+
+    /**
+     * Gets the flag of 'Auto Registration'.
+     *
+     * @return the flag of registering the profile automatically.
+     */
+    public boolean getAutoRegistration() {
+        return mAutoRegistration;
+    }
+
+    /**
+     * Sets the calling process's Uid in the sip service.
+     * @hide
+     */
+    public void setCallingUid(int uid) {
+        mCallingUid = uid;
+    }
+
+    /**
+     * Gets the calling process's Uid in the sip settings.
+     * @hide
+     */
+    public int getCallingUid() {
+        return mCallingUid;
+    }
+
+    private Object readResolve() throws ObjectStreamException {
+        // For compatibility.
+        if (mPort == 0) mPort = DEFAULT_PORT;
+        return this;
+    }
+}
diff --git a/android/net/sip/SipRegistrationListener.java b/android/net/sip/SipRegistrationListener.java
new file mode 100644
index 0000000..9968cc7
--- /dev/null
+++ b/android/net/sip/SipRegistrationListener.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2010 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.sip;
+
+/**
+ * Listener for SIP registration events.
+ */
+public interface SipRegistrationListener {
+    /**
+     * Called when a registration request is sent.
+     *
+     * @param localProfileUri the URI string of the SIP profile to register with
+     */
+    void onRegistering(String localProfileUri);
+
+    /**
+     * Called when the registration succeeded.
+     *
+     * @param localProfileUri the URI string of the SIP profile to register with
+     * @param expiryTime duration in seconds before the registration expires
+     */
+    void onRegistrationDone(String localProfileUri, long expiryTime);
+
+    /**
+     * Called when the registration failed.
+     *
+     * @param localProfileUri the URI string of the SIP profile to register with
+     * @param errorCode error code of this error
+     * @param errorMessage error message
+     * @see SipErrorCode
+     */
+    void onRegistrationFailed(String localProfileUri, int errorCode,
+            String errorMessage);
+}
diff --git a/android/net/sip/SipSession.java b/android/net/sip/SipSession.java
new file mode 100644
index 0000000..edbc66f
--- /dev/null
+++ b/android/net/sip/SipSession.java
@@ -0,0 +1,574 @@
+/*
+ * Copyright (C) 2010 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.sip;
+
+import android.os.RemoteException;
+import android.telephony.Rlog;
+
+/**
+ * Represents a SIP session that is associated with a SIP dialog or a standalone
+ * transaction not within a dialog.
+ * <p>You can get a {@link SipSession} from {@link SipManager} with {@link
+ * SipManager#createSipSession createSipSession()} (when initiating calls) or {@link
+ * SipManager#getSessionFor getSessionFor()} (when receiving calls).</p>
+ */
+public final class SipSession {
+    private static final String TAG = "SipSession";
+
+    /**
+     * Defines SIP session states, such as "registering", "outgoing call", and "in call".
+     */
+    public static class State {
+        /** When session is ready to initiate a call or transaction. */
+        public static final int READY_TO_CALL = 0;
+
+        /** When the registration request is sent out. */
+        public static final int REGISTERING = 1;
+
+        /** When the unregistration request is sent out. */
+        public static final int DEREGISTERING = 2;
+
+        /** When an INVITE request is received. */
+        public static final int INCOMING_CALL = 3;
+
+        /** When an OK response is sent for the INVITE request received. */
+        public static final int INCOMING_CALL_ANSWERING = 4;
+
+        /** When an INVITE request is sent. */
+        public static final int OUTGOING_CALL = 5;
+
+        /** When a RINGING response is received for the INVITE request sent. */
+        public static final int OUTGOING_CALL_RING_BACK = 6;
+
+        /** When a CANCEL request is sent for the INVITE request sent. */
+        public static final int OUTGOING_CALL_CANCELING = 7;
+
+        /** When a call is established. */
+        public static final int IN_CALL = 8;
+
+        /** When an OPTIONS request is sent. */
+        public static final int PINGING = 9;
+
+        /** When ending a call. @hide */
+        public static final int ENDING_CALL = 10;
+
+        /** Not defined. */
+        public static final int NOT_DEFINED = 101;
+
+        /**
+         * Converts the state to string.
+         */
+        public static String toString(int state) {
+            switch (state) {
+                case READY_TO_CALL:
+                    return "READY_TO_CALL";
+                case REGISTERING:
+                    return "REGISTERING";
+                case DEREGISTERING:
+                    return "DEREGISTERING";
+                case INCOMING_CALL:
+                    return "INCOMING_CALL";
+                case INCOMING_CALL_ANSWERING:
+                    return "INCOMING_CALL_ANSWERING";
+                case OUTGOING_CALL:
+                    return "OUTGOING_CALL";
+                case OUTGOING_CALL_RING_BACK:
+                    return "OUTGOING_CALL_RING_BACK";
+                case OUTGOING_CALL_CANCELING:
+                    return "OUTGOING_CALL_CANCELING";
+                case IN_CALL:
+                    return "IN_CALL";
+                case PINGING:
+                    return "PINGING";
+                default:
+                    return "NOT_DEFINED";
+            }
+        }
+
+        private State() {
+        }
+    }
+
+    /**
+     * Listener for events relating to a SIP session, such as when a session is being registered
+     * ("on registering") or a call is outgoing ("on calling").
+     * <p>Many of these events are also received by {@link SipAudioCall.Listener}.</p>
+     */
+    public static class Listener {
+        /**
+         * Called when an INVITE request is sent to initiate a new call.
+         *
+         * @param session the session object that carries out the transaction
+         */
+        public void onCalling(SipSession session) {
+        }
+
+        /**
+         * Called when an INVITE request is received.
+         *
+         * @param session the session object that carries out the transaction
+         * @param caller the SIP profile of the caller
+         * @param sessionDescription the caller's session description
+         */
+        public void onRinging(SipSession session, SipProfile caller,
+                String sessionDescription) {
+        }
+
+        /**
+         * Called when a RINGING response is received for the INVITE request sent
+         *
+         * @param session the session object that carries out the transaction
+         */
+        public void onRingingBack(SipSession session) {
+        }
+
+        /**
+         * Called when the session is established.
+         *
+         * @param session the session object that is associated with the dialog
+         * @param sessionDescription the peer's session description
+         */
+        public void onCallEstablished(SipSession session,
+                String sessionDescription) {
+        }
+
+        /**
+         * Called when the session is terminated.
+         *
+         * @param session the session object that is associated with the dialog
+         */
+        public void onCallEnded(SipSession session) {
+        }
+
+        /**
+         * Called when the peer is busy during session initialization.
+         *
+         * @param session the session object that carries out the transaction
+         */
+        public void onCallBusy(SipSession session) {
+        }
+
+        /**
+         * Called when the call is being transferred to a new one.
+         *
+         * @hide
+         * @param newSession the new session that the call will be transferred to
+         * @param sessionDescription the new peer's session description
+         */
+        public void onCallTransferring(SipSession newSession,
+                String sessionDescription) {
+        }
+
+        /**
+         * Called when an error occurs during session initialization and
+         * termination.
+         *
+         * @param session the session object that carries out the transaction
+         * @param errorCode error code defined in {@link SipErrorCode}
+         * @param errorMessage error message
+         */
+        public void onError(SipSession session, int errorCode,
+                String errorMessage) {
+        }
+
+        /**
+         * Called when an error occurs during session modification negotiation.
+         *
+         * @param session the session object that carries out the transaction
+         * @param errorCode error code defined in {@link SipErrorCode}
+         * @param errorMessage error message
+         */
+        public void onCallChangeFailed(SipSession session, int errorCode,
+                String errorMessage) {
+        }
+
+        /**
+         * Called when a registration request is sent.
+         *
+         * @param session the session object that carries out the transaction
+         */
+        public void onRegistering(SipSession session) {
+        }
+
+        /**
+         * Called when registration is successfully done.
+         *
+         * @param session the session object that carries out the transaction
+         * @param duration duration in second before the registration expires
+         */
+        public void onRegistrationDone(SipSession session, int duration) {
+        }
+
+        /**
+         * Called when the registration fails.
+         *
+         * @param session the session object that carries out the transaction
+         * @param errorCode error code defined in {@link SipErrorCode}
+         * @param errorMessage error message
+         */
+        public void onRegistrationFailed(SipSession session, int errorCode,
+                String errorMessage) {
+        }
+
+        /**
+         * Called when the registration gets timed out.
+         *
+         * @param session the session object that carries out the transaction
+         */
+        public void onRegistrationTimeout(SipSession session) {
+        }
+    }
+
+    private final ISipSession mSession;
+    private Listener mListener;
+
+    SipSession(ISipSession realSession) {
+        mSession = realSession;
+        if (realSession != null) {
+            try {
+                realSession.setListener(createListener());
+            } catch (RemoteException e) {
+                loge("SipSession.setListener:", e);
+            }
+        }
+    }
+
+    SipSession(ISipSession realSession, Listener listener) {
+        this(realSession);
+        setListener(listener);
+    }
+
+    /**
+     * Gets the IP address of the local host on which this SIP session runs.
+     *
+     * @return the IP address of the local host
+     */
+    public String getLocalIp() {
+        try {
+            return mSession.getLocalIp();
+        } catch (RemoteException e) {
+            loge("getLocalIp:", e);
+            return "127.0.0.1";
+        }
+    }
+
+    /**
+     * Gets the SIP profile that this session is associated with.
+     *
+     * @return the SIP profile that this session is associated with
+     */
+    public SipProfile getLocalProfile() {
+        try {
+            return mSession.getLocalProfile();
+        } catch (RemoteException e) {
+            loge("getLocalProfile:", e);
+            return null;
+        }
+    }
+
+    /**
+     * Gets the SIP profile that this session is connected to. Only available
+     * when the session is associated with a SIP dialog.
+     *
+     * @return the SIP profile that this session is connected to
+     */
+    public SipProfile getPeerProfile() {
+        try {
+            return mSession.getPeerProfile();
+        } catch (RemoteException e) {
+            loge("getPeerProfile:", e);
+            return null;
+        }
+    }
+
+    /**
+     * Gets the session state. The value returned must be one of the states in
+     * {@link State}.
+     *
+     * @return the session state
+     */
+    public int getState() {
+        try {
+            return mSession.getState();
+        } catch (RemoteException e) {
+            loge("getState:", e);
+            return State.NOT_DEFINED;
+        }
+    }
+
+    /**
+     * Checks if the session is in a call.
+     *
+     * @return true if the session is in a call
+     */
+    public boolean isInCall() {
+        try {
+            return mSession.isInCall();
+        } catch (RemoteException e) {
+            loge("isInCall:", e);
+            return false;
+        }
+    }
+
+    /**
+     * Gets the call ID of the session.
+     *
+     * @return the call ID
+     */
+    public String getCallId() {
+        try {
+            return mSession.getCallId();
+        } catch (RemoteException e) {
+            loge("getCallId:", e);
+            return null;
+        }
+    }
+
+
+    /**
+     * Sets the listener to listen to the session events. A {@code SipSession}
+     * can only hold one listener at a time. Subsequent calls to this method
+     * override the previous listener.
+     *
+     * @param listener to listen to the session events of this object
+     */
+    public void setListener(Listener listener) {
+        mListener = listener;
+    }
+
+
+    /**
+     * Performs registration to the server specified by the associated local
+     * profile. The session listener is called back upon success or failure of
+     * registration. The method is only valid to call when the session state is
+     * in {@link State#READY_TO_CALL}.
+     *
+     * @param duration duration in second before the registration expires
+     * @see Listener
+     */
+    public void register(int duration) {
+        try {
+            mSession.register(duration);
+        } catch (RemoteException e) {
+            loge("register:", e);
+        }
+    }
+
+    /**
+     * Performs unregistration to the server specified by the associated local
+     * profile. Unregistration is technically the same as registration with zero
+     * expiration duration. The session listener is called back upon success or
+     * failure of unregistration. The method is only valid to call when the
+     * session state is in {@link State#READY_TO_CALL}.
+     *
+     * @see Listener
+     */
+    public void unregister() {
+        try {
+            mSession.unregister();
+        } catch (RemoteException e) {
+            loge("unregister:", e);
+        }
+    }
+
+    /**
+     * Initiates a call to the specified profile. The session listener is called
+     * back upon defined session events. The method is only valid to call when
+     * the session state is in {@link State#READY_TO_CALL}.
+     *
+     * @param callee the SIP profile to make the call to
+     * @param sessionDescription the session description of this call
+     * @param timeout the session will be timed out if the call is not
+     *        established within {@code timeout} seconds. Default value (defined
+     *        by SIP protocol) is used if {@code timeout} is zero or negative.
+     * @see Listener
+     */
+    public void makeCall(SipProfile callee, String sessionDescription,
+            int timeout) {
+        try {
+            mSession.makeCall(callee, sessionDescription, timeout);
+        } catch (RemoteException e) {
+            loge("makeCall:", e);
+        }
+    }
+
+    /**
+     * Answers an incoming call with the specified session description. The
+     * method is only valid to call when the session state is in
+     * {@link State#INCOMING_CALL}.
+     *
+     * @param sessionDescription the session description to answer this call
+     * @param timeout the session will be timed out if the call is not
+     *        established within {@code timeout} seconds. Default value (defined
+     *        by SIP protocol) is used if {@code timeout} is zero or negative.
+     */
+    public void answerCall(String sessionDescription, int timeout) {
+        try {
+            mSession.answerCall(sessionDescription, timeout);
+        } catch (RemoteException e) {
+            loge("answerCall:", e);
+        }
+    }
+
+    /**
+     * Ends an established call, terminates an outgoing call or rejects an
+     * incoming call. The method is only valid to call when the session state is
+     * in {@link State#IN_CALL},
+     * {@link State#INCOMING_CALL},
+     * {@link State#OUTGOING_CALL} or
+     * {@link State#OUTGOING_CALL_RING_BACK}.
+     */
+    public void endCall() {
+        try {
+            mSession.endCall();
+        } catch (RemoteException e) {
+            loge("endCall:", e);
+        }
+    }
+
+    /**
+     * Changes the session description during a call. The method is only valid
+     * to call when the session state is in {@link State#IN_CALL}.
+     *
+     * @param sessionDescription the new session description
+     * @param timeout the session will be timed out if the call is not
+     *        established within {@code timeout} seconds. Default value (defined
+     *        by SIP protocol) is used if {@code timeout} is zero or negative.
+     */
+    public void changeCall(String sessionDescription, int timeout) {
+        try {
+            mSession.changeCall(sessionDescription, timeout);
+        } catch (RemoteException e) {
+            loge("changeCall:", e);
+        }
+    }
+
+    ISipSession getRealSession() {
+        return mSession;
+    }
+
+    private ISipSessionListener createListener() {
+        return new ISipSessionListener.Stub() {
+            @Override
+            public void onCalling(ISipSession session) {
+                if (mListener != null) {
+                    mListener.onCalling(SipSession.this);
+                }
+            }
+
+            @Override
+            public void onRinging(ISipSession session, SipProfile caller,
+                    String sessionDescription) {
+                if (mListener != null) {
+                    mListener.onRinging(SipSession.this, caller,
+                            sessionDescription);
+                }
+            }
+
+            @Override
+            public void onRingingBack(ISipSession session) {
+                if (mListener != null) {
+                    mListener.onRingingBack(SipSession.this);
+                }
+            }
+
+            @Override
+            public void onCallEstablished(ISipSession session,
+                    String sessionDescription) {
+                if (mListener != null) {
+                    mListener.onCallEstablished(SipSession.this,
+                            sessionDescription);
+                }
+            }
+
+            @Override
+            public void onCallEnded(ISipSession session) {
+                if (mListener != null) {
+                    mListener.onCallEnded(SipSession.this);
+                }
+            }
+
+            @Override
+            public void onCallBusy(ISipSession session) {
+                if (mListener != null) {
+                    mListener.onCallBusy(SipSession.this);
+                }
+            }
+
+            @Override
+            public void onCallTransferring(ISipSession session,
+                    String sessionDescription) {
+                if (mListener != null) {
+                    mListener.onCallTransferring(
+                            new SipSession(session, SipSession.this.mListener),
+                            sessionDescription);
+
+                }
+            }
+
+            @Override
+            public void onCallChangeFailed(ISipSession session, int errorCode,
+                    String message) {
+                if (mListener != null) {
+                    mListener.onCallChangeFailed(SipSession.this, errorCode,
+                            message);
+                }
+            }
+
+            @Override
+            public void onError(ISipSession session, int errorCode, String message) {
+                if (mListener != null) {
+                    mListener.onError(SipSession.this, errorCode, message);
+                }
+            }
+
+            @Override
+            public void onRegistering(ISipSession session) {
+                if (mListener != null) {
+                    mListener.onRegistering(SipSession.this);
+                }
+            }
+
+            @Override
+            public void onRegistrationDone(ISipSession session, int duration) {
+                if (mListener != null) {
+                    mListener.onRegistrationDone(SipSession.this, duration);
+                }
+            }
+
+            @Override
+            public void onRegistrationFailed(ISipSession session, int errorCode,
+                    String message) {
+                if (mListener != null) {
+                    mListener.onRegistrationFailed(SipSession.this, errorCode,
+                            message);
+                }
+            }
+
+            @Override
+            public void onRegistrationTimeout(ISipSession session) {
+                if (mListener != null) {
+                    mListener.onRegistrationTimeout(SipSession.this);
+                }
+            }
+        };
+    }
+
+    private void loge(String s, Throwable t) {
+        Rlog.e(TAG, s, t);
+    }
+}
diff --git a/android/net/sip/SipSessionAdapter.java b/android/net/sip/SipSessionAdapter.java
new file mode 100644
index 0000000..f538983
--- /dev/null
+++ b/android/net/sip/SipSessionAdapter.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright (C) 2010 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.sip;
+
+/**
+ * Adapter class for {@link ISipSessionListener}. Default implementation of all
+ * callback methods is no-op.
+ * @hide
+ */
+public class SipSessionAdapter extends ISipSessionListener.Stub {
+    public void onCalling(ISipSession session) {
+    }
+
+    public void onRinging(ISipSession session, SipProfile caller,
+            String sessionDescription) {
+    }
+
+    public void onRingingBack(ISipSession session) {
+    }
+
+    public void onCallEstablished(ISipSession session,
+            String sessionDescription) {
+    }
+
+    public void onCallEnded(ISipSession session) {
+    }
+
+    public void onCallBusy(ISipSession session) {
+    }
+
+    public void onCallTransferring(ISipSession session,
+            String sessionDescription) {
+    }
+
+    public void onCallChangeFailed(ISipSession session, int errorCode,
+            String message) {
+    }
+
+    public void onError(ISipSession session, int errorCode, String message) {
+    }
+
+    public void onRegistering(ISipSession session) {
+    }
+
+    public void onRegistrationDone(ISipSession session, int duration) {
+    }
+
+    public void onRegistrationFailed(ISipSession session, int errorCode,
+            String message) {
+    }
+
+    public void onRegistrationTimeout(ISipSession session) {
+    }
+}
diff --git a/android/net/util/DnsUtils.java b/android/net/util/DnsUtils.java
new file mode 100644
index 0000000..e6abd50
--- /dev/null
+++ b/android/net/util/DnsUtils.java
@@ -0,0 +1,376 @@
+/*
+ * Copyright (C) 2019 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.util;
+
+import static android.system.OsConstants.AF_INET;
+import static android.system.OsConstants.AF_INET6;
+import static android.system.OsConstants.IPPROTO_UDP;
+import static android.system.OsConstants.SOCK_DGRAM;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.InetAddresses;
+import android.net.Network;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+
+import com.android.internal.util.BitUtils;
+
+import libcore.io.IoUtils;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * @hide
+ */
+public class DnsUtils {
+    private static final String TAG = "DnsUtils";
+    private static final int CHAR_BIT = 8;
+    public static final int IPV6_ADDR_SCOPE_NODELOCAL = 0x01;
+    public static final int IPV6_ADDR_SCOPE_LINKLOCAL = 0x02;
+    public static final int IPV6_ADDR_SCOPE_SITELOCAL = 0x05;
+    public static final int IPV6_ADDR_SCOPE_GLOBAL = 0x0e;
+    private static final Comparator<SortableAddress> sRfc6724Comparator = new Rfc6724Comparator();
+
+    /**
+     * Comparator to sort SortableAddress in Rfc6724 style.
+     */
+    public static class Rfc6724Comparator implements Comparator<SortableAddress> {
+        // This function matches the behaviour of _rfc6724_compare in the native resolver.
+        @Override
+        public int compare(SortableAddress span1, SortableAddress span2) {
+            // Rule 1: Avoid unusable destinations.
+            if (span1.hasSrcAddr != span2.hasSrcAddr) {
+                return span2.hasSrcAddr - span1.hasSrcAddr;
+            }
+
+            // Rule 2: Prefer matching scope.
+            if (span1.scopeMatch != span2.scopeMatch) {
+                return span2.scopeMatch - span1.scopeMatch;
+            }
+
+            // TODO: Implement rule 3: Avoid deprecated addresses.
+            // TODO: Implement rule 4: Prefer home addresses.
+
+            // Rule 5: Prefer matching label.
+            if (span1.labelMatch != span2.labelMatch) {
+                return span2.labelMatch - span1.labelMatch;
+            }
+
+            // Rule 6: Prefer higher precedence.
+            if (span1.precedence != span2.precedence) {
+                return span2.precedence - span1.precedence;
+            }
+
+            // TODO: Implement rule 7: Prefer native transport.
+
+            // Rule 8: Prefer smaller scope.
+            if (span1.scope != span2.scope) {
+                return span1.scope - span2.scope;
+            }
+
+            // Rule 9: Use longest matching prefix. IPv6 only.
+            if (span1.prefixMatchLen != span2.prefixMatchLen) {
+                return span2.prefixMatchLen - span1.prefixMatchLen;
+            }
+
+            // Rule 10: Leave the order unchanged. Collections.sort is a stable sort.
+            return 0;
+        }
+    }
+
+    /**
+     * Class used to sort with RFC 6724
+     */
+    public static class SortableAddress {
+        public final int label;
+        public final int labelMatch;
+        public final int scope;
+        public final int scopeMatch;
+        public final int precedence;
+        public final int prefixMatchLen;
+        public final int hasSrcAddr;
+        public final InetAddress address;
+
+        public SortableAddress(@NonNull InetAddress addr, @Nullable InetAddress srcAddr) {
+            address = addr;
+            hasSrcAddr = (srcAddr != null) ? 1 : 0;
+            label = findLabel(addr);
+            scope = findScope(addr);
+            precedence = findPrecedence(addr);
+            labelMatch = ((srcAddr != null) && (label == findLabel(srcAddr))) ? 1 : 0;
+            scopeMatch = ((srcAddr != null) && (scope == findScope(srcAddr))) ? 1 : 0;
+            if (isIpv6Address(addr) && isIpv6Address(srcAddr)) {
+                prefixMatchLen = compareIpv6PrefixMatchLen(srcAddr, addr);
+            } else {
+                prefixMatchLen = 0;
+            }
+        }
+    }
+
+    /**
+     * Sort the given address list in RFC6724 order.
+     * Will leave the list unchanged if an error occurs.
+     *
+     * This function matches the behaviour of _rfc6724_sort in the native resolver.
+     */
+    public static @NonNull List<InetAddress> rfc6724Sort(@Nullable Network network,
+            @NonNull List<InetAddress> answers) {
+        List<SortableAddress> sortableAnswerList = new ArrayList<>();
+        answers.forEach(addr -> sortableAnswerList.add(
+                new SortableAddress(addr, findSrcAddress(network, addr))));
+
+        Collections.sort(sortableAnswerList, sRfc6724Comparator);
+
+        final List<InetAddress> sortedAnswers = new ArrayList<>();
+        sortableAnswerList.forEach(ans -> sortedAnswers.add(ans.address));
+
+        return sortedAnswers;
+    }
+
+    private static @Nullable InetAddress findSrcAddress(@Nullable Network network,
+            @NonNull InetAddress addr) {
+        final int domain;
+        if (isIpv4Address(addr)) {
+            domain = AF_INET;
+        } else if (isIpv6Address(addr)) {
+            domain = AF_INET6;
+        } else {
+            return null;
+        }
+        final FileDescriptor socket;
+        try {
+            socket = Os.socket(domain, SOCK_DGRAM, IPPROTO_UDP);
+        } catch (ErrnoException e) {
+            Log.e(TAG, "findSrcAddress:" + e.toString());
+            return null;
+        }
+        try {
+            if (network != null) network.bindSocket(socket);
+            Os.connect(socket, new InetSocketAddress(addr, 0));
+            return ((InetSocketAddress) Os.getsockname(socket)).getAddress();
+        } catch (IOException | ErrnoException e) {
+            return null;
+        } finally {
+            IoUtils.closeQuietly(socket);
+        }
+    }
+
+    /**
+     * Get the label for a given IPv4/IPv6 address.
+     * RFC 6724, section 2.1.
+     *
+     * Note that Java will return an IPv4-mapped address as an IPv4 address.
+     */
+    private static int findLabel(@NonNull InetAddress addr) {
+        if (isIpv4Address(addr)) {
+            return 4;
+        } else if (isIpv6Address(addr)) {
+            if (addr.isLoopbackAddress()) {
+                return 0;
+            } else if (isIpv6Address6To4(addr)) {
+                return 2;
+            } else if (isIpv6AddressTeredo(addr)) {
+                return 5;
+            } else if (isIpv6AddressULA(addr)) {
+                return 13;
+            } else if (((Inet6Address) addr).isIPv4CompatibleAddress()) {
+                return 3;
+            } else if (addr.isSiteLocalAddress()) {
+                return 11;
+            } else if (isIpv6Address6Bone(addr)) {
+                return 12;
+            } else {
+                // All other IPv6 addresses, including global unicast addresses.
+                return 1;
+            }
+        } else {
+            // This should never happen.
+            return 1;
+        }
+    }
+
+    private static boolean isIpv6Address(@Nullable InetAddress addr) {
+        return addr instanceof Inet6Address;
+    }
+
+    private static boolean isIpv4Address(@Nullable InetAddress addr) {
+        return addr instanceof Inet4Address;
+    }
+
+    private static boolean isIpv6Address6To4(@NonNull InetAddress addr) {
+        if (!isIpv6Address(addr)) return false;
+        final byte[] byteAddr = addr.getAddress();
+        return byteAddr[0] == 0x20 && byteAddr[1] == 0x02;
+    }
+
+    private static boolean isIpv6AddressTeredo(@NonNull InetAddress addr) {
+        if (!isIpv6Address(addr)) return false;
+        final byte[] byteAddr = addr.getAddress();
+        return byteAddr[0] == 0x20 && byteAddr[1] == 0x01 && byteAddr[2] == 0x00
+                && byteAddr[3] == 0x00;
+    }
+
+    private static boolean isIpv6AddressULA(@NonNull InetAddress addr) {
+        return isIpv6Address(addr) && (addr.getAddress()[0] & 0xfe) == 0xfc;
+    }
+
+    private static boolean isIpv6Address6Bone(@NonNull InetAddress addr) {
+        if (!isIpv6Address(addr)) return false;
+        final byte[] byteAddr = addr.getAddress();
+        return byteAddr[0] == 0x3f && byteAddr[1] == (byte) 0xfe;
+    }
+
+    private static int getIpv6MulticastScope(@NonNull InetAddress addr) {
+        return !isIpv6Address(addr) ? 0 : (addr.getAddress()[1] & 0x0f);
+    }
+
+    private static int findScope(@NonNull InetAddress addr) {
+        if (isIpv6Address(addr)) {
+            if (addr.isMulticastAddress()) {
+                return getIpv6MulticastScope(addr);
+            } else if (addr.isLoopbackAddress() || addr.isLinkLocalAddress()) {
+                /**
+                 * RFC 4291 section 2.5.3 says loopback is to be treated as having
+                 * link-local scope.
+                 */
+                return IPV6_ADDR_SCOPE_LINKLOCAL;
+            } else if (addr.isSiteLocalAddress()) {
+                return IPV6_ADDR_SCOPE_SITELOCAL;
+            } else {
+                return IPV6_ADDR_SCOPE_GLOBAL;
+            }
+        } else if (isIpv4Address(addr)) {
+            if (addr.isLoopbackAddress() || addr.isLinkLocalAddress()) {
+                return IPV6_ADDR_SCOPE_LINKLOCAL;
+            } else {
+                /**
+                 * RFC 6724 section 3.2. Other IPv4 addresses, including private addresses
+                 * and shared addresses (100.64.0.0/10), are assigned global scope.
+                 */
+                return IPV6_ADDR_SCOPE_GLOBAL;
+            }
+        } else {
+            /**
+             * This should never happen.
+             * Return a scope with low priority as a last resort.
+             */
+            return IPV6_ADDR_SCOPE_NODELOCAL;
+        }
+    }
+
+    /**
+     * Get the precedence for a given IPv4/IPv6 address.
+     * RFC 6724, section 2.1.
+     *
+     * Note that Java will return an IPv4-mapped address as an IPv4 address.
+     */
+    private static int findPrecedence(@NonNull InetAddress addr) {
+        if (isIpv4Address(addr)) {
+            return 35;
+        } else if (isIpv6Address(addr)) {
+            if (addr.isLoopbackAddress()) {
+                return 50;
+            } else if (isIpv6Address6To4(addr)) {
+                return 30;
+            } else if (isIpv6AddressTeredo(addr)) {
+                return 5;
+            } else if (isIpv6AddressULA(addr)) {
+                return 3;
+            } else if (((Inet6Address) addr).isIPv4CompatibleAddress() || addr.isSiteLocalAddress()
+                    || isIpv6Address6Bone(addr)) {
+                return 1;
+            } else {
+                // All other IPv6 addresses, including global unicast addresses.
+                return 40;
+            }
+        } else {
+            return 1;
+        }
+    }
+
+    /**
+     * Find number of matching initial bits between the two addresses.
+     */
+    private static int compareIpv6PrefixMatchLen(@NonNull InetAddress srcAddr,
+            @NonNull InetAddress dstAddr) {
+        final byte[] srcByte = srcAddr.getAddress();
+        final byte[] dstByte = dstAddr.getAddress();
+
+        // This should never happen.
+        if (srcByte.length != dstByte.length) return 0;
+
+        for (int i = 0; i < dstByte.length; ++i) {
+            if (srcByte[i] == dstByte[i]) {
+                continue;
+            }
+            int x = BitUtils.uint8(srcByte[i]) ^ BitUtils.uint8(dstByte[i]);
+            return i * CHAR_BIT + (Integer.numberOfLeadingZeros(x) - 24);  // Java ints are 32 bits
+        }
+        return dstByte.length * CHAR_BIT;
+    }
+
+    /**
+     * Check if given network has Ipv4 capability
+     * This function matches the behaviour of have_ipv4 in the native resolver.
+     */
+    public static boolean haveIpv4(@Nullable Network network) {
+        final SocketAddress addrIpv4 =
+                new InetSocketAddress(InetAddresses.parseNumericAddress("8.8.8.8"), 0);
+        return checkConnectivity(network, AF_INET, addrIpv4);
+    }
+
+    /**
+     * Check if given network has Ipv6 capability
+     * This function matches the behaviour of have_ipv6 in the native resolver.
+     */
+    public static boolean haveIpv6(@Nullable Network network) {
+        final SocketAddress addrIpv6 =
+                new InetSocketAddress(InetAddresses.parseNumericAddress("2000::"), 0);
+        return checkConnectivity(network, AF_INET6, addrIpv6);
+    }
+
+    private static boolean checkConnectivity(@Nullable Network network,
+            int domain, @NonNull SocketAddress addr) {
+        final FileDescriptor socket;
+        try {
+            socket = Os.socket(domain, SOCK_DGRAM, IPPROTO_UDP);
+        } catch (ErrnoException e) {
+            return false;
+        }
+        try {
+            if (network != null) network.bindSocket(socket);
+            Os.connect(socket, addr);
+        } catch (IOException | ErrnoException e) {
+            return false;
+        } finally {
+            IoUtils.closeQuietly(socket);
+        }
+        return true;
+    }
+}
diff --git a/android/net/util/InterfaceParams.java b/android/net/util/InterfaceParams.java
new file mode 100644
index 0000000..f6bb873
--- /dev/null
+++ b/android/net/util/InterfaceParams.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.util;
+
+import static com.android.internal.util.Preconditions.checkArgument;
+
+import android.net.MacAddress;
+import android.text.TextUtils;
+
+import java.net.NetworkInterface;
+import java.net.SocketException;
+
+
+/**
+ * Encapsulate the interface parameters common to IpClient/IpServer components.
+ *
+ * Basically all java.net.NetworkInterface methods throw Exceptions. IpClient
+ * and IpServer (sub)components need most or all of this information at some
+ * point during their lifecycles, so pass only this simplified object around
+ * which can be created once when IpClient/IpServer are told to start.
+ *
+ * @hide
+ */
+public class InterfaceParams {
+    public final String name;
+    public final int index;
+    public final MacAddress macAddr;
+    public final int defaultMtu;
+
+    // TODO: move the below to NetworkStackConstants when this class is moved to the NetworkStack.
+    private static final int ETHER_MTU = 1500;
+    private static final int IPV6_MIN_MTU = 1280;
+
+
+    public static InterfaceParams getByName(String name) {
+        final NetworkInterface netif = getNetworkInterfaceByName(name);
+        if (netif == null) return null;
+
+        // Not all interfaces have MAC addresses, e.g. rmnet_data0.
+        final MacAddress macAddr = getMacAddress(netif);
+
+        try {
+            return new InterfaceParams(name, netif.getIndex(), macAddr, netif.getMTU());
+        } catch (IllegalArgumentException|SocketException e) {
+            return null;
+        }
+    }
+
+    public InterfaceParams(String name, int index, MacAddress macAddr) {
+        this(name, index, macAddr, ETHER_MTU);
+    }
+
+    public InterfaceParams(String name, int index, MacAddress macAddr, int defaultMtu) {
+        checkArgument((!TextUtils.isEmpty(name)), "impossible interface name");
+        checkArgument((index > 0), "invalid interface index");
+        this.name = name;
+        this.index = index;
+        this.macAddr = (macAddr != null) ? macAddr : MacAddress.fromBytes(new byte[] {
+                0x02, 0x00, 0x00, 0x00, 0x00, 0x00 });
+        this.defaultMtu = (defaultMtu > IPV6_MIN_MTU) ? defaultMtu : IPV6_MIN_MTU;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("%s/%d/%s/%d", name, index, macAddr, defaultMtu);
+    }
+
+    private static NetworkInterface getNetworkInterfaceByName(String name) {
+        try {
+            return NetworkInterface.getByName(name);
+        } catch (NullPointerException|SocketException e) {
+            return null;
+        }
+    }
+
+    private static MacAddress getMacAddress(NetworkInterface netif) {
+        try {
+            return MacAddress.fromBytes(netif.getHardwareAddress());
+        } catch (IllegalArgumentException|NullPointerException|SocketException e) {
+            return null;
+        }
+    }
+}
diff --git a/android/net/util/InterfaceSet.java b/android/net/util/InterfaceSet.java
new file mode 100644
index 0000000..9f26fa1
--- /dev/null
+++ b/android/net/util/InterfaceSet.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2018 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.util;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.StringJoiner;
+
+
+/**
+ * @hide
+ */
+public class InterfaceSet {
+    public final Set<String> ifnames;
+
+    public InterfaceSet(String... names) {
+        final Set<String> nameSet = new HashSet<>();
+        for (String name : names) {
+            if (name != null) nameSet.add(name);
+        }
+        ifnames = Collections.unmodifiableSet(nameSet);
+    }
+
+    @Override
+    public String toString() {
+        final StringJoiner sj = new StringJoiner(",", "[", "]");
+        for (String ifname : ifnames) sj.add(ifname);
+        return sj.toString();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        return obj != null
+                && obj instanceof InterfaceSet
+                && ifnames.equals(((InterfaceSet)obj).ifnames);
+    }
+}
diff --git a/android/net/util/IpUtils.java b/android/net/util/IpUtils.java
new file mode 100644
index 0000000..e037c40
--- /dev/null
+++ b/android/net/util/IpUtils.java
@@ -0,0 +1,151 @@
+/*
+ * Copyright (C) 2015 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.util;
+
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.nio.BufferOverflowException;
+import java.nio.BufferUnderflowException;
+import java.nio.ByteBuffer;
+import java.nio.ShortBuffer;
+
+import static android.system.OsConstants.IPPROTO_TCP;
+import static android.system.OsConstants.IPPROTO_UDP;
+
+/**
+ * @hide
+ */
+public class IpUtils {
+    /**
+     * Converts a signed short value to an unsigned int value.  Needed
+     * because Java does not have unsigned types.
+     */
+    private static int intAbs(short v) {
+        return v & 0xFFFF;
+    }
+
+    /**
+     * Performs an IP checksum (used in IP header and across UDP
+     * payload) on the specified portion of a ByteBuffer.  The seed
+     * allows the checksum to commence with a specified value.
+     */
+    private static int checksum(ByteBuffer buf, int seed, int start, int end) {
+        int sum = seed;
+        final int bufPosition = buf.position();
+
+        // set position of original ByteBuffer, so that the ShortBuffer
+        // will be correctly initialized
+        buf.position(start);
+        ShortBuffer shortBuf = buf.asShortBuffer();
+
+        // re-set ByteBuffer position
+        buf.position(bufPosition);
+
+        final int numShorts = (end - start) / 2;
+        for (int i = 0; i < numShorts; i++) {
+            sum += intAbs(shortBuf.get(i));
+        }
+        start += numShorts * 2;
+
+        // see if a singleton byte remains
+        if (end != start) {
+            short b = buf.get(start);
+
+            // make it unsigned
+            if (b < 0) {
+                b += 256;
+            }
+
+            sum += b * 256;
+        }
+
+        sum = ((sum >> 16) & 0xFFFF) + (sum & 0xFFFF);
+        sum = ((sum + ((sum >> 16) & 0xFFFF)) & 0xFFFF);
+        int negated = ~sum;
+        return intAbs((short) negated);
+    }
+
+    private static int pseudoChecksumIPv4(
+            ByteBuffer buf, int headerOffset, int protocol, int transportLen) {
+        int partial = protocol + transportLen;
+        partial += intAbs(buf.getShort(headerOffset + 12));
+        partial += intAbs(buf.getShort(headerOffset + 14));
+        partial += intAbs(buf.getShort(headerOffset + 16));
+        partial += intAbs(buf.getShort(headerOffset + 18));
+        return partial;
+    }
+
+    private static int pseudoChecksumIPv6(
+            ByteBuffer buf, int headerOffset, int protocol, int transportLen) {
+        int partial = protocol + transportLen;
+        for (int offset = 8; offset < 40; offset += 2) {
+            partial += intAbs(buf.getShort(headerOffset + offset));
+        }
+        return partial;
+    }
+
+    private static byte ipversion(ByteBuffer buf, int headerOffset) {
+        return (byte) ((buf.get(headerOffset) & (byte) 0xf0) >> 4);
+   }
+
+    public static short ipChecksum(ByteBuffer buf, int headerOffset) {
+        byte ihl = (byte) (buf.get(headerOffset) & 0x0f);
+        return (short) checksum(buf, 0, headerOffset, headerOffset + ihl * 4);
+    }
+
+    private static short transportChecksum(ByteBuffer buf, int protocol,
+            int ipOffset, int transportOffset, int transportLen) {
+        if (transportLen < 0) {
+            throw new IllegalArgumentException("Transport length < 0: " + transportLen);
+        }
+        int sum;
+        byte ver = ipversion(buf, ipOffset);
+        if (ver == 4) {
+            sum = pseudoChecksumIPv4(buf, ipOffset, protocol, transportLen);
+        } else if (ver == 6) {
+            sum = pseudoChecksumIPv6(buf, ipOffset, protocol, transportLen);
+        } else {
+            throw new UnsupportedOperationException("Checksum must be IPv4 or IPv6");
+        }
+
+        sum = checksum(buf, sum, transportOffset, transportOffset + transportLen);
+        if (protocol == IPPROTO_UDP && sum == 0) {
+            sum = (short) 0xffff;
+        }
+        return (short) sum;
+    }
+
+    public static short udpChecksum(ByteBuffer buf, int ipOffset, int transportOffset) {
+        int transportLen = intAbs(buf.getShort(transportOffset + 4));
+        return transportChecksum(buf, IPPROTO_UDP, ipOffset, transportOffset, transportLen);
+    }
+
+    public static short tcpChecksum(ByteBuffer buf, int ipOffset, int transportOffset,
+            int transportLen) {
+        return transportChecksum(buf, IPPROTO_TCP, ipOffset, transportOffset, transportLen);
+    }
+
+    public static String addressAndPortToString(InetAddress address, int port) {
+        return String.format(
+                (address instanceof Inet6Address) ? "[%s]:%d" : "%s:%d",
+                address.getHostAddress(), port);
+    }
+
+    public static boolean isValidUdpOrTcpPort(int port) {
+        return port > 0 && port < 65536;
+    }
+}
diff --git a/android/net/util/KeepalivePacketDataUtil.java b/android/net/util/KeepalivePacketDataUtil.java
new file mode 100644
index 0000000..9a51729
--- /dev/null
+++ b/android/net/util/KeepalivePacketDataUtil.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2019 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.util;
+
+import android.annotation.NonNull;
+import android.net.NattKeepalivePacketData;
+import android.net.NattKeepalivePacketDataParcelable;
+
+/** @hide */
+public final class KeepalivePacketDataUtil {
+     /**
+     * Convert this NattKeepalivePacketData to a NattKeepalivePacketDataParcelable.
+     */
+    @NonNull
+    public static NattKeepalivePacketDataParcelable toStableParcelable(
+            NattKeepalivePacketData pkt) {
+        final NattKeepalivePacketDataParcelable parcel = new NattKeepalivePacketDataParcelable();
+
+        parcel.srcAddress = pkt.srcAddress.getAddress();
+        parcel.srcPort = pkt.srcPort;
+        parcel.dstAddress = pkt.dstAddress.getAddress();
+        parcel.dstPort = pkt.dstPort;
+        return parcel;
+    }
+}
diff --git a/android/net/util/KeepaliveUtils.java b/android/net/util/KeepaliveUtils.java
new file mode 100644
index 0000000..bfc4563
--- /dev/null
+++ b/android/net/util/KeepaliveUtils.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2019 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.util;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.res.Resources;
+import android.net.NetworkCapabilities;
+import android.text.TextUtils;
+import android.util.AndroidRuntimeException;
+
+import com.android.internal.R;
+
+/**
+ * Collection of utilities for socket keepalive offload.
+ *
+ * @hide
+ */
+public final class KeepaliveUtils {
+
+    public static final String TAG = "KeepaliveUtils";
+
+    public static class KeepaliveDeviceConfigurationException extends AndroidRuntimeException {
+        public KeepaliveDeviceConfigurationException(final String msg) {
+            super(msg);
+        }
+    }
+
+    /**
+     * Read supported keepalive count for each transport type from overlay resource. This should be
+     * used to create a local variable store of resource customization, and use it as the input for
+     * {@link getSupportedKeepalivesForNetworkCapabilities}.
+     *
+     * @param context The context to read resource from.
+     * @return An array of supported keepalive count for each transport type.
+     */
+    @NonNull
+    public static int[] getSupportedKeepalives(@NonNull Context context) {
+        String[] res = null;
+        try {
+            res = context.getResources().getStringArray(
+                    R.array.config_networkSupportedKeepaliveCount);
+        } catch (Resources.NotFoundException unused) {
+        }
+        if (res == null) throw new KeepaliveDeviceConfigurationException("invalid resource");
+
+        final int[] ret = new int[NetworkCapabilities.MAX_TRANSPORT + 1];
+        for (final String row : res) {
+            if (TextUtils.isEmpty(row)) {
+                throw new KeepaliveDeviceConfigurationException("Empty string");
+            }
+            final String[] arr = row.split(",");
+            if (arr.length != 2) {
+                throw new KeepaliveDeviceConfigurationException("Invalid parameter length");
+            }
+
+            int transport;
+            int supported;
+            try {
+                transport = Integer.parseInt(arr[0]);
+                supported = Integer.parseInt(arr[1]);
+            } catch (NumberFormatException e) {
+                throw new KeepaliveDeviceConfigurationException("Invalid number format");
+            }
+
+            if (!NetworkCapabilities.isValidTransport(transport)) {
+                throw new KeepaliveDeviceConfigurationException("Invalid transport " + transport);
+            }
+
+            if (supported < 0) {
+                throw new KeepaliveDeviceConfigurationException(
+                        "Invalid supported count " + supported + " for "
+                                + NetworkCapabilities.transportNameOf(transport));
+            }
+            ret[transport] = supported;
+        }
+        return ret;
+    }
+
+    /**
+     * Get supported keepalive count for the given {@link NetworkCapabilities}.
+     *
+     * @param supportedKeepalives An array of supported keepalive count for each transport type.
+     * @param nc The {@link NetworkCapabilities} of the network the socket keepalive is on.
+     *
+     * @return Supported keepalive count for the given {@link NetworkCapabilities}.
+     */
+    public static int getSupportedKeepalivesForNetworkCapabilities(
+            @NonNull int[] supportedKeepalives, @NonNull NetworkCapabilities nc) {
+        final int[] transports = nc.getTransportTypes();
+        if (transports.length == 0) return 0;
+        int supportedCount = supportedKeepalives[transports[0]];
+        // Iterate through transports and return minimum supported value.
+        for (final int transport : transports) {
+            if (supportedCount > supportedKeepalives[transport]) {
+                supportedCount = supportedKeepalives[transport];
+            }
+        }
+        return supportedCount;
+    }
+}
diff --git a/android/net/util/MultinetworkPolicyTracker.java b/android/net/util/MultinetworkPolicyTracker.java
new file mode 100644
index 0000000..30c5cd9
--- /dev/null
+++ b/android/net/util/MultinetworkPolicyTracker.java
@@ -0,0 +1,195 @@
+/*
+ * 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.util;
+
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.database.ContentObserver;
+import android.net.ConnectivityManager;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.Message;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.util.Slog;
+
+import java.util.Arrays;
+import java.util.List;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.R;
+
+import static android.provider.Settings.Global.NETWORK_AVOID_BAD_WIFI;
+import static android.provider.Settings.Global.NETWORK_METERED_MULTIPATH_PREFERENCE;
+
+/**
+ * A class to encapsulate management of the "Smart Networking" capability of
+ * avoiding bad Wi-Fi when, for example upstream connectivity is lost or
+ * certain critical link failures occur.
+ *
+ * This enables the device to switch to another form of connectivity, like
+ * mobile, if it's available and working.
+ *
+ * The Runnable |avoidBadWifiCallback|, if given, is posted to the supplied
+ * Handler' whenever the computed "avoid bad wifi" value changes.
+ *
+ * Disabling this reverts the device to a level of networking sophistication
+ * circa 2012-13 by disabling disparate code paths each of which contribute to
+ * maintaining continuous, working Internet connectivity.
+ *
+ * @hide
+ */
+public class MultinetworkPolicyTracker {
+    private static String TAG = MultinetworkPolicyTracker.class.getSimpleName();
+
+    private final Context mContext;
+    private final Handler mHandler;
+    private final Runnable mReevaluateRunnable;
+    private final List<Uri> mSettingsUris;
+    private final ContentResolver mResolver;
+    private final SettingObserver mSettingObserver;
+    private final BroadcastReceiver mBroadcastReceiver;
+
+    private volatile boolean mAvoidBadWifi = true;
+    private volatile int mMeteredMultipathPreference;
+
+    public MultinetworkPolicyTracker(Context ctx, Handler handler) {
+        this(ctx, handler, null);
+    }
+
+    public MultinetworkPolicyTracker(Context ctx, Handler handler, Runnable avoidBadWifiCallback) {
+        mContext = ctx;
+        mHandler = handler;
+        mReevaluateRunnable = () -> {
+            if (updateAvoidBadWifi() && avoidBadWifiCallback != null) {
+                avoidBadWifiCallback.run();
+            }
+            updateMeteredMultipathPreference();
+        };
+        mSettingsUris = Arrays.asList(
+            Settings.Global.getUriFor(NETWORK_AVOID_BAD_WIFI),
+            Settings.Global.getUriFor(NETWORK_METERED_MULTIPATH_PREFERENCE));
+        mResolver = mContext.getContentResolver();
+        mSettingObserver = new SettingObserver();
+        mBroadcastReceiver = new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                reevaluate();
+            }
+        };
+
+        updateAvoidBadWifi();
+        updateMeteredMultipathPreference();
+    }
+
+    public void start() {
+        for (Uri uri : mSettingsUris) {
+            mResolver.registerContentObserver(uri, false, mSettingObserver);
+        }
+
+        final IntentFilter intentFilter = new IntentFilter();
+        intentFilter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
+        mContext.registerReceiverAsUser(
+                mBroadcastReceiver, UserHandle.ALL, intentFilter, null, null);
+
+        reevaluate();
+    }
+
+    public void shutdown() {
+        mResolver.unregisterContentObserver(mSettingObserver);
+
+        mContext.unregisterReceiver(mBroadcastReceiver);
+    }
+
+    public boolean getAvoidBadWifi() {
+        return mAvoidBadWifi;
+    }
+
+    // TODO: move this to MultipathPolicyTracker.
+    public int getMeteredMultipathPreference() {
+        return mMeteredMultipathPreference;
+    }
+
+    /**
+     * Whether the device or carrier configuration disables avoiding bad wifi by default.
+     */
+    public boolean configRestrictsAvoidBadWifi() {
+        return (mContext.getResources().getInteger(R.integer.config_networkAvoidBadWifi) == 0);
+    }
+
+    /**
+     * Whether we should display a notification when wifi becomes unvalidated.
+     */
+    public boolean shouldNotifyWifiUnvalidated() {
+        return configRestrictsAvoidBadWifi() && getAvoidBadWifiSetting() == null;
+    }
+
+    public String getAvoidBadWifiSetting() {
+        return Settings.Global.getString(mResolver, NETWORK_AVOID_BAD_WIFI);
+    }
+
+    @VisibleForTesting
+    public void reevaluate() {
+        mHandler.post(mReevaluateRunnable);
+    }
+
+    public boolean updateAvoidBadWifi() {
+        final boolean settingAvoidBadWifi = "1".equals(getAvoidBadWifiSetting());
+        final boolean prev = mAvoidBadWifi;
+        mAvoidBadWifi = settingAvoidBadWifi || !configRestrictsAvoidBadWifi();
+        return mAvoidBadWifi != prev;
+    }
+
+    /**
+     * The default (device and carrier-dependent) value for metered multipath preference.
+     */
+    public int configMeteredMultipathPreference() {
+        return mContext.getResources().getInteger(
+                R.integer.config_networkMeteredMultipathPreference);
+    }
+
+    public void updateMeteredMultipathPreference() {
+        String setting = Settings.Global.getString(mResolver, NETWORK_METERED_MULTIPATH_PREFERENCE);
+        try {
+            mMeteredMultipathPreference = Integer.parseInt(setting);
+        } catch (NumberFormatException e) {
+            mMeteredMultipathPreference = configMeteredMultipathPreference();
+        }
+    }
+
+    private class SettingObserver extends ContentObserver {
+        public SettingObserver() {
+            super(null);
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            Slog.wtf(TAG, "Should never be reached.");
+        }
+
+        @Override
+        public void onChange(boolean selfChange, Uri uri) {
+            if (!mSettingsUris.contains(uri)) {
+                Slog.wtf(TAG, "Unexpected settings observation: " + uri);
+            }
+            reevaluate();
+        }
+    }
+}
diff --git a/android/net/util/NetdService.java b/android/net/util/NetdService.java
new file mode 100644
index 0000000..d4cd5bd
--- /dev/null
+++ b/android/net/util/NetdService.java
@@ -0,0 +1,135 @@
+/*
+ * 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.util;
+
+import android.content.Context;
+import android.net.INetd;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.os.SystemClock;
+import android.util.Log;
+
+
+/**
+ * @hide
+ */
+public class NetdService {
+    private static final String TAG = NetdService.class.getSimpleName();
+    private static final long BASE_TIMEOUT_MS = 100;
+    private static final long MAX_TIMEOUT_MS = 1000;
+
+
+    /**
+     * Return an INetd instance, or null if not available.
+     *
+     * It is the caller's responsibility to check for a null return value
+     * and to handle RemoteException errors from invocations on the returned
+     * interface if, for example, netd dies and is restarted.
+     *
+     * Returned instances of INetd should not be cached.
+     *
+     * @return an INetd instance or null.
+     */
+    public static INetd getInstance() {
+        // NOTE: ServiceManager does no caching for the netd service,
+        // because netd is not one of the defined common services.
+        final INetd netdInstance = INetd.Stub.asInterface(
+                ServiceManager.getService(Context.NETD_SERVICE));
+        if (netdInstance == null) {
+            Log.w(TAG, "WARNING: returning null INetd instance.");
+        }
+        return netdInstance;
+    }
+
+    /**
+     * Blocks for a specified time until an INetd instance is available.
+     *
+     * It is the caller's responsibility to handle RemoteException errors
+     * from invocations on the returned interface if, for example, netd
+     * dies after this interface was returned.
+     *
+     * Returned instances of INetd should not be cached.
+     *
+     * Special values of maxTimeoutMs include: 0, meaning try to obtain an
+     * INetd instance only once, and -1 (or any value less than 0), meaning
+     * try to obtain an INetd instance indefinitely.
+     *
+     * @param maxTimeoutMs the maximum time to spend getting an INetd instance
+     * @return an INetd instance or null if no instance is available
+     * within |maxTimeoutMs| milliseconds.
+     */
+    public static INetd get(long maxTimeoutMs) {
+        if (maxTimeoutMs == 0) return getInstance();
+
+        final long stop = (maxTimeoutMs > 0)
+                ? SystemClock.elapsedRealtime() + maxTimeoutMs
+                : Long.MAX_VALUE;
+
+        long timeoutMs = 0;
+        while (true) {
+            final INetd netdInstance = getInstance();
+            if (netdInstance != null) {
+                return netdInstance;
+            }
+
+            final long remaining = stop - SystemClock.elapsedRealtime();
+            if (remaining <= 0) break;
+
+            // No netdInstance was received; sleep and retry.
+            timeoutMs = Math.min(timeoutMs + BASE_TIMEOUT_MS, MAX_TIMEOUT_MS);
+            timeoutMs = Math.min(timeoutMs, remaining);
+            try {
+                Thread.sleep(timeoutMs);
+            } catch (InterruptedException e) {}
+        }
+        return null;
+    }
+
+    /**
+     * Blocks until an INetd instance is available.
+     *
+     * It is the caller's responsibility to handle RemoteException errors
+     * from invocations on the returned interface if, for example, netd
+     * dies after this interface was returned.
+     *
+     * Returned instances of INetd should not be cached.
+     *
+     * @return an INetd instance.
+     */
+    public static INetd get() {
+        return get(-1);
+    }
+
+    public static interface NetdCommand {
+        void run(INetd netd) throws RemoteException;
+    }
+
+    /**
+     * Blocks until an INetd instance is availabe, and retries until either
+     * the command succeeds or a runtime exception is thrown.
+     */
+    public static void run(NetdCommand cmd) {
+        while (true) {
+            try {
+                cmd.run(get());
+                return;
+            } catch (RemoteException re) {
+                Log.e(TAG, "error communicating with netd: " + re);
+            }
+        }
+    }
+}
diff --git a/android/net/util/NetworkConstants.java b/android/net/util/NetworkConstants.java
new file mode 100644
index 0000000..ea5ce65
--- /dev/null
+++ b/android/net/util/NetworkConstants.java
@@ -0,0 +1,92 @@
+/*
+ * 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.util;
+
+/**
+ * Networking protocol constants.
+ *
+ * Includes:
+ *     - constants that describe packet layout
+ *     - various helper functions
+ *
+ * @hide
+ */
+public final class NetworkConstants {
+    private NetworkConstants() { throw new RuntimeException("no instance permitted"); }
+
+    public static final byte FF = asByte(0xff);
+    public static final byte[] ETHER_ADDR_BROADCAST = {
+        FF, FF, FF, FF, FF, FF
+    };
+
+    public static final int ETHER_MTU = 1500;
+
+    /**
+     * IPv4 constants.
+     *
+     * See also:
+     *     - https://tools.ietf.org/html/rfc791
+     */
+    public static final int IPV4_ADDR_BITS = 32;
+
+    /**
+     * IPv6 constants.
+     *
+     * See also:
+     *     - https://tools.ietf.org/html/rfc2460
+     */
+    public static final int IPV6_ADDR_BITS = 128;
+    public static final int IPV6_ADDR_LEN = 16;
+    public static final int IPV6_MIN_MTU = 1280;
+    public static final int RFC7421_PREFIX_LENGTH = 64;
+
+    /**
+     * ICMP common (v4/v6) constants.
+     *
+     * See also:
+     *     - https://tools.ietf.org/html/rfc792
+     *     - https://tools.ietf.org/html/rfc4443
+     */
+    public static final int ICMP_HEADER_TYPE_OFFSET = 0;
+    public static final int ICMP_HEADER_CODE_OFFSET = 1;
+    public static final int ICMP_HEADER_CHECKSUM_OFFSET = 2;
+    public static final int ICMP_ECHO_IDENTIFIER_OFFSET = 4;
+    public static final int ICMP_ECHO_SEQUENCE_NUMBER_OFFSET = 6;
+    public static final int ICMP_ECHO_DATA_OFFSET = 8;
+
+    /**
+     * ICMPv4 constants.
+     *
+     * See also:
+     *     - https://tools.ietf.org/html/rfc792
+     */
+    public static final int ICMPV4_ECHO_REQUEST_TYPE = 8;
+    public static final int ICMPV6_ECHO_REQUEST_TYPE = 128;
+
+    /**
+     * DNS constants.
+     *
+     * See also:
+     *     - https://tools.ietf.org/html/rfc1035
+     */
+    public static final int DNS_SERVER_PORT = 53;
+
+    /**
+     * Utility functions.
+     */
+    public static byte asByte(int i) { return (byte) i; }
+}
diff --git a/android/net/util/PrefixUtils.java b/android/net/util/PrefixUtils.java
new file mode 100644
index 0000000..f60694a
--- /dev/null
+++ b/android/net/util/PrefixUtils.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.util;
+
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.net.LinkProperties;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+
+/**
+ * @hide
+ */
+public class PrefixUtils {
+    private static final IpPrefix[] MIN_NON_FORWARDABLE_PREFIXES = {
+            pfx("127.0.0.0/8"),     // IPv4 loopback
+            pfx("169.254.0.0/16"),  // IPv4 link-local, RFC3927#section-8
+            pfx("::/3"),
+            pfx("fe80::/64"),       // IPv6 link-local
+            pfx("fc00::/7"),        // IPv6 ULA
+            pfx("ff02::/8"),        // IPv6 link-local multicast
+    };
+
+    public static final IpPrefix DEFAULT_WIFI_P2P_PREFIX = pfx("192.168.49.0/24");
+
+    public static Set<IpPrefix> getNonForwardablePrefixes() {
+        final HashSet<IpPrefix> prefixes = new HashSet<>();
+        addNonForwardablePrefixes(prefixes);
+        return prefixes;
+    }
+
+    public static void addNonForwardablePrefixes(Set<IpPrefix> prefixes) {
+        Collections.addAll(prefixes, MIN_NON_FORWARDABLE_PREFIXES);
+    }
+
+    public static Set<IpPrefix> localPrefixesFrom(LinkProperties lp) {
+        final HashSet<IpPrefix> localPrefixes = new HashSet<>();
+        if (lp == null) return localPrefixes;
+
+        for (LinkAddress addr : lp.getAllLinkAddresses()) {
+            if (addr.getAddress().isLinkLocalAddress()) continue;
+            localPrefixes.add(asIpPrefix(addr));
+        }
+        // TODO: Add directly-connected routes as well (ones from which we did
+        // not also form a LinkAddress)?
+
+        return localPrefixes;
+    }
+
+    public static IpPrefix asIpPrefix(LinkAddress addr) {
+        return new IpPrefix(addr.getAddress(), addr.getPrefixLength());
+    }
+
+    public static IpPrefix ipAddressAsPrefix(InetAddress ip) {
+        final int bitLength = (ip instanceof Inet4Address)
+                ? NetworkConstants.IPV4_ADDR_BITS
+                : NetworkConstants.IPV6_ADDR_BITS;
+        return new IpPrefix(ip, bitLength);
+    }
+
+    private static IpPrefix pfx(String prefixStr) {
+        return new IpPrefix(prefixStr);
+    }
+}
diff --git a/android/net/util/SharedLog.java b/android/net/util/SharedLog.java
new file mode 100644
index 0000000..2cdb2b0
--- /dev/null
+++ b/android/net/util/SharedLog.java
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.util;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.text.TextUtils;
+import android.util.LocalLog;
+import android.util.Log;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.StringJoiner;
+
+
+/**
+ * Class to centralize logging functionality for tethering.
+ *
+ * All access to class methods other than dump() must be on the same thread.
+ *
+ * TODO: this is a copy of SharedLog in the NetworkStack. Remove after Tethering is migrated.
+ * @hide
+ */
+public class SharedLog {
+    private static final int DEFAULT_MAX_RECORDS = 500;
+    private static final String COMPONENT_DELIMITER = ".";
+
+    private enum Category {
+        NONE,
+        ERROR,
+        MARK,
+        WARN,
+    };
+
+    private final LocalLog mLocalLog;
+    // The tag to use for output to the system log. This is not output to the
+    // LocalLog because that would be redundant.
+    private final String mTag;
+    // The component (or subcomponent) of a system that is sharing this log.
+    // This can grow in depth if components call forSubComponent() to obtain
+    // their SharedLog instance. The tag is not included in the component for
+    // brevity.
+    private final String mComponent;
+
+    public SharedLog(String tag) {
+        this(DEFAULT_MAX_RECORDS, tag);
+    }
+
+    public SharedLog(int maxRecords, String tag) {
+        this(new LocalLog(maxRecords), tag, tag);
+    }
+
+    private SharedLog(LocalLog localLog, String tag, String component) {
+        mLocalLog = localLog;
+        mTag = tag;
+        mComponent = component;
+    }
+
+    public String getTag() {
+        return mTag;
+    }
+
+    /**
+     * Create a SharedLog based on this log with an additional component prefix on each logged line.
+     */
+    public SharedLog forSubComponent(String component) {
+        if (!isRootLogInstance()) {
+            component = mComponent + COMPONENT_DELIMITER + component;
+        }
+        return new SharedLog(mLocalLog, mTag, component);
+    }
+
+    /**
+     * Dump the contents of this log.
+     *
+     * <p>This method may be called on any thread.
+     */
+    public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+        mLocalLog.readOnlyLocalLog().dump(fd, writer, args);
+    }
+
+    //////
+    // Methods that both log an entry and emit it to the system log.
+    //////
+
+    /**
+     * Log an error due to an exception. This does not include the exception stacktrace.
+     *
+     * <p>The log entry will be also added to the system log.
+     * @see #e(String, Throwable)
+     */
+    public void e(Exception e) {
+        Log.e(mTag, record(Category.ERROR, e.toString()));
+    }
+
+    /**
+     * Log an error message.
+     *
+     * <p>The log entry will be also added to the system log.
+     */
+    public void e(String msg) {
+        Log.e(mTag, record(Category.ERROR, msg));
+    }
+
+    /**
+     * Log an error due to an exception, with the exception stacktrace if provided.
+     *
+     * <p>The error and exception message appear in the shared log, but the stacktrace is only
+     * logged in general log output (logcat). The log entry will be also added to the system log.
+     */
+    public void e(@NonNull String msg, @Nullable Throwable exception) {
+        if (exception == null) {
+            e(msg);
+            return;
+        }
+        Log.e(mTag, record(Category.ERROR, msg + ": " + exception.getMessage()), exception);
+    }
+
+    /**
+     * Log an informational message.
+     *
+     * <p>The log entry will be also added to the system log.
+     */
+    public void i(String msg) {
+        Log.i(mTag, record(Category.NONE, msg));
+    }
+
+    /**
+     * Log a warning message.
+     *
+     * <p>The log entry will be also added to the system log.
+     */
+    public void w(String msg) {
+        Log.w(mTag, record(Category.WARN, msg));
+    }
+
+    //////
+    // Methods that only log an entry (and do NOT emit to the system log).
+    //////
+
+    /**
+     * Log a general message to be only included in the in-memory log.
+     *
+     * <p>The log entry will *not* be added to the system log.
+     */
+    public void log(String msg) {
+        record(Category.NONE, msg);
+    }
+
+    /**
+     * Log a general, formatted message to be only included in the in-memory log.
+     *
+     * <p>The log entry will *not* be added to the system log.
+     * @see String#format(String, Object...)
+     */
+    public void logf(String fmt, Object... args) {
+        log(String.format(fmt, args));
+    }
+
+    /**
+     * Log a message with MARK level.
+     *
+     * <p>The log entry will *not* be added to the system log.
+     */
+    public void mark(String msg) {
+        record(Category.MARK, msg);
+    }
+
+    private String record(Category category, String msg) {
+        final String entry = logLine(category, msg);
+        mLocalLog.log(entry);
+        return entry;
+    }
+
+    private String logLine(Category category, String msg) {
+        final StringJoiner sj = new StringJoiner(" ");
+        if (!isRootLogInstance()) sj.add("[" + mComponent + "]");
+        if (category != Category.NONE) sj.add(category.toString());
+        return sj.add(msg).toString();
+    }
+
+    // Check whether this SharedLog instance is nominally the top level in
+    // a potential hierarchy of shared logs (the root of a tree),
+    // or is a subcomponent within the hierarchy.
+    private boolean isRootLogInstance() {
+        return TextUtils.isEmpty(mComponent) || mComponent.equals(mTag);
+    }
+}
diff --git a/android/net/util/SocketUtils.java b/android/net/util/SocketUtils.java
new file mode 100644
index 0000000..1364d8c
--- /dev/null
+++ b/android/net/util/SocketUtils.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2015 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.util;
+
+import static android.system.OsConstants.SOL_SOCKET;
+import static android.system.OsConstants.SO_BINDTODEVICE;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
+import android.net.NetworkUtils;
+import android.system.ErrnoException;
+import android.system.NetlinkSocketAddress;
+import android.system.Os;
+import android.system.PacketSocketAddress;
+
+import libcore.io.IoBridge;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.SocketAddress;
+
+/**
+ * Collection of utilities to interact with raw sockets.
+ * @hide
+ */
+@SystemApi
+@TestApi
+public final class SocketUtils {
+    /**
+     * Create a raw datagram socket that is bound to an interface.
+     *
+     * <p>Data sent through the socket will go directly to the underlying network, ignoring VPNs.
+     */
+    public static void bindSocketToInterface(@NonNull FileDescriptor socket, @NonNull String iface)
+            throws ErrnoException {
+        // SO_BINDTODEVICE actually takes a string. This works because the first member
+        // of struct ifreq is a NULL-terminated interface name.
+        // TODO: add a setsockoptString()
+        Os.setsockoptIfreq(socket, SOL_SOCKET, SO_BINDTODEVICE, iface);
+        NetworkUtils.protectFromVpn(socket);
+    }
+
+    /**
+     * Make a socket address to communicate with netlink.
+     */
+    @NonNull
+    public static SocketAddress makeNetlinkSocketAddress(int portId, int groupsMask) {
+        return new NetlinkSocketAddress(portId, groupsMask);
+    }
+
+    /**
+     * Make socket address that packet sockets can bind to.
+     */
+    @NonNull
+    public static SocketAddress makePacketSocketAddress(int protocol, int ifIndex) {
+        return new PacketSocketAddress((short) protocol, ifIndex);
+    }
+
+    /**
+     * Make a socket address that packet socket can send packets to.
+     */
+    @NonNull
+    public static SocketAddress makePacketSocketAddress(int ifIndex, @NonNull byte[] hwAddr) {
+        return new PacketSocketAddress(ifIndex, hwAddr);
+    }
+
+    /**
+     * @see IoBridge#closeAndSignalBlockedThreads(FileDescriptor)
+     */
+    public static void closeSocket(@Nullable FileDescriptor fd) throws IOException {
+        IoBridge.closeAndSignalBlockedThreads(fd);
+    }
+
+    private SocketUtils() {}
+}
diff --git a/android/net/util/VersionedBroadcastListener.java b/android/net/util/VersionedBroadcastListener.java
new file mode 100644
index 0000000..107c404
--- /dev/null
+++ b/android/net/util/VersionedBroadcastListener.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.util;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.os.Handler;
+import android.util.Log;
+
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Consumer;
+
+
+/**
+ * A utility class that runs the provided callback on the provided handler when
+ * intents matching the provided filter arrive. Intents received by a stale
+ * receiver are safely ignored.
+ *
+ * Calls to startListening() and stopListening() must happen on the same thread.
+ *
+ * @hide
+ */
+public class VersionedBroadcastListener {
+    private static final boolean DBG = false;
+
+    public interface IntentCallback {
+        public void run(Intent intent);
+    }
+
+    private final String mTag;
+    private final Context mContext;
+    private final Handler mHandler;
+    private final IntentFilter mFilter;
+    private final Consumer<Intent> mCallback;
+    private final AtomicInteger mGenerationNumber;
+    private BroadcastReceiver mReceiver;
+
+    public VersionedBroadcastListener(String tag, Context ctx, Handler handler,
+            IntentFilter filter, Consumer<Intent> callback) {
+        mTag = tag;
+        mContext = ctx;
+        mHandler = handler;
+        mFilter = filter;
+        mCallback = callback;
+        mGenerationNumber = new AtomicInteger(0);
+    }
+
+    public void startListening() {
+        if (DBG) Log.d(mTag, "startListening");
+        if (mReceiver != null) return;
+
+        mReceiver = new Receiver(mTag, mGenerationNumber, mCallback);
+        mContext.registerReceiver(mReceiver, mFilter, null, mHandler);
+    }
+
+    public void stopListening() {
+        if (DBG) Log.d(mTag, "stopListening");
+        if (mReceiver == null) return;
+
+        mGenerationNumber.incrementAndGet();
+        mContext.unregisterReceiver(mReceiver);
+        mReceiver = null;
+    }
+
+    private static class Receiver extends BroadcastReceiver {
+        public final String tag;
+        public final AtomicInteger atomicGenerationNumber;
+        public final Consumer<Intent> callback;
+        // Used to verify this receiver is still current.
+        public final int generationNumber;
+
+        public Receiver(
+                String tag, AtomicInteger atomicGenerationNumber, Consumer<Intent> callback) {
+            this.tag = tag;
+            this.atomicGenerationNumber = atomicGenerationNumber;
+            this.callback = callback;
+            generationNumber = atomicGenerationNumber.incrementAndGet();
+        }
+
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            final int currentGenerationNumber = atomicGenerationNumber.get();
+
+            if (DBG) {
+                Log.d(tag, "receiver generationNumber=" + generationNumber +
+                        ", current generationNumber=" + currentGenerationNumber);
+            }
+            if (generationNumber != currentGenerationNumber) return;
+
+            callback.accept(intent);
+        }
+    }
+}
diff --git a/android/net/wifi/AnqpInformationElement.java b/android/net/wifi/AnqpInformationElement.java
new file mode 100644
index 0000000..47b7129
--- /dev/null
+++ b/android/net/wifi/AnqpInformationElement.java
@@ -0,0 +1,80 @@
+/*
+ * 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.wifi;
+
+/**
+ * This object contains the payload of an ANQP element.
+ * Vendor id is the vendor ID for the element, or 0 if it is an 802.11(u) element.
+ * Hotspot 2.0 uses the WFA Vendor ID which is 0x506f9a
+ * The payload contains the bytes of the payload, starting after the length octet(s).
+ * @hide
+ */
+public class AnqpInformationElement {
+    public static final int HOTSPOT20_VENDOR_ID = 0x506f9a;
+
+    public static final int ANQP_QUERY_LIST = 256;
+    public static final int ANQP_CAPABILITY_LIST = 257;
+    public static final int ANQP_VENUE_NAME = 258;
+    public static final int ANQP_EMERGENCY_NUMBER = 259;
+    public static final int ANQP_NWK_AUTH_TYPE = 260;
+    public static final int ANQP_ROAMING_CONSORTIUM = 261;
+    public static final int ANQP_IP_ADDR_AVAILABILITY = 262;
+    public static final int ANQP_NAI_REALM = 263;
+    public static final int ANQP_3GPP_NETWORK = 264;
+    public static final int ANQP_GEO_LOC = 265;
+    public static final int ANQP_CIVIC_LOC = 266;
+    public static final int ANQP_LOC_URI = 267;
+    public static final int ANQP_DOM_NAME = 268;
+    public static final int ANQP_EMERGENCY_ALERT = 269;
+    public static final int ANQP_TDLS_CAP = 270;
+    public static final int ANQP_EMERGENCY_NAI = 271;
+    public static final int ANQP_NEIGHBOR_REPORT = 272;
+    public static final int ANQP_VENDOR_SPEC = 56797;
+
+    public static final int HS_QUERY_LIST = 1;
+    public static final int HS_CAPABILITY_LIST = 2;
+    public static final int HS_FRIENDLY_NAME = 3;
+    public static final int HS_WAN_METRICS = 4;
+    public static final int HS_CONN_CAPABILITY = 5;
+    public static final int HS_NAI_HOME_REALM_QUERY = 6;
+    public static final int HS_OPERATING_CLASS = 7;
+    public static final int HS_OSU_PROVIDERS = 8;
+    public static final int HS_ICON_REQUEST = 10;
+    public static final int HS_ICON_FILE = 11;
+
+    private final int mVendorId;
+    private final int mElementId;
+    private final byte[] mPayload;
+
+    public AnqpInformationElement(int vendorId, int elementId, byte[] payload) {
+        mVendorId = vendorId;
+        mElementId = elementId;
+        mPayload = payload;
+    }
+
+    public int getVendorId() {
+        return mVendorId;
+    }
+
+    public int getElementId() {
+        return mElementId;
+    }
+
+    public byte[] getPayload() {
+        return mPayload;
+    }
+}
diff --git a/android/net/wifi/BatchedScanResult.java b/android/net/wifi/BatchedScanResult.java
new file mode 100644
index 0000000..ed8845d
--- /dev/null
+++ b/android/net/wifi/BatchedScanResult.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2008 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.wifi;
+
+import android.os.Parcelable;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Describes the Results of a batched set of wifi scans where the firmware performs many
+ * scans and stores the timestamped results without waking the main processor each time.
+ * @hide
+ * @removed
+ */
+@Deprecated
+@SystemApi
+public class BatchedScanResult implements Parcelable {
+    private static final String TAG = "BatchedScanResult";
+
+    /** Inidcates this scan was interrupted and may only have partial results. */
+    public boolean truncated;
+
+    /** The result of this particular scan. */
+    public final List<ScanResult> scanResults = new ArrayList<ScanResult>();
+
+
+    public BatchedScanResult() {
+    }
+
+    public BatchedScanResult(BatchedScanResult source) {
+        truncated = source.truncated;
+        for (ScanResult s : source.scanResults) scanResults.add(new ScanResult(s));
+    }
+
+    @Override
+    public String toString() {
+        StringBuffer sb = new StringBuffer();
+
+        sb.append("BatchedScanResult: ").
+                append("truncated: ").append(String.valueOf(truncated)).
+                append("scanResults: [");
+        for (ScanResult s : scanResults) {
+            sb.append(" <").append(s.toString()).append("> ");
+        }
+        sb.append(" ]");
+        return sb.toString();
+    }
+
+    /** Implement the Parcelable interface {@hide} */
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Implement the Parcelable interface {@hide} */
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(truncated ? 1 : 0);
+        dest.writeInt(scanResults.size());
+        for (ScanResult s : scanResults) {
+            s.writeToParcel(dest, flags);
+        }
+    }
+
+    /** Implement the Parcelable interface {@hide} */
+    public static final @android.annotation.NonNull Creator<BatchedScanResult> CREATOR =
+        new Creator<BatchedScanResult>() {
+            public BatchedScanResult createFromParcel(Parcel in) {
+                BatchedScanResult result = new BatchedScanResult();
+                result.truncated = (in.readInt() == 1);
+                int count = in.readInt();
+                while (count-- > 0) {
+                    result.scanResults.add(ScanResult.CREATOR.createFromParcel(in));
+                }
+                return result;
+            }
+
+            public BatchedScanResult[] newArray(int size) {
+                return new BatchedScanResult[size];
+            }
+        };
+}
diff --git a/android/net/wifi/EAPConstants.java b/android/net/wifi/EAPConstants.java
new file mode 100644
index 0000000..b5f7c94
--- /dev/null
+++ b/android/net/wifi/EAPConstants.java
@@ -0,0 +1,57 @@
+/**
+ * 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.wifi;
+
+/**
+ * Utility class containing EAP (Extensible Authentication Protocol) Related constants.
+ *
+ * @hide
+ */
+public final class EAPConstants {
+    // Constant definition for EAP types. Refer to
+    // http://www.iana.org/assignments/eap-numbers/eap-numbers.xhtml for more info.
+    public static final int EAP_MD5 = 4;
+    public static final int EAP_OTP = 5;
+    public static final int EAP_RSA = 9;
+    public static final int EAP_KEA = 11;
+    public static final int EAP_KEA_VALIDATE = 12;
+    public static final int EAP_TLS = 13;
+    public static final int EAP_LEAP = 17;
+    public static final int EAP_SIM = 18;
+    public static final int EAP_TTLS = 21;
+    public static final int EAP_AKA = 23;
+    public static final int EAP_3Com = 24;
+    public static final int EAP_MSCHAPv2 = 26;
+    public static final int EAP_PEAP = 29;
+    public static final int EAP_POTP = 32;
+    public static final int EAP_ActiontecWireless = 35;
+    public static final int EAP_HTTPDigest = 38;
+    public static final int EAP_SPEKE = 41;
+    public static final int EAP_MOBAC = 42;
+    public static final int EAP_FAST = 43;
+    public static final int EAP_ZLXEAP = 44;
+    public static final int EAP_Link = 45;
+    public static final int EAP_PAX = 46;
+    public static final int EAP_PSK = 47;
+    public static final int EAP_SAKE = 48;
+    public static final int EAP_IKEv2 = 49;
+    public static final int EAP_AKA_PRIME = 50;
+    public static final int EAP_GPSK = 51;
+    public static final int EAP_PWD = 52;
+    public static final int EAP_EKE = 53;
+    public static final int EAP_TEAP = 55;
+}
diff --git a/android/net/wifi/EasyConnectStatusCallback.java b/android/net/wifi/EasyConnectStatusCallback.java
new file mode 100644
index 0000000..b8c82fd
--- /dev/null
+++ b/android/net/wifi/EasyConnectStatusCallback.java
@@ -0,0 +1,179 @@
+/*
+ * Copyright (C) 2018 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.wifi;
+
+import android.annotation.IntDef;
+import android.annotation.SystemApi;
+import android.os.Handler;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * Easy Connect (DPP) Status Callback. Use this callback to get status updates (success, failure,
+ * progress) from the Easy Connect operation started with
+ * {@link WifiManager#startEasyConnectAsConfiguratorInitiator(String,
+ * int, int, Handler, EasyConnectStatusCallback)} or
+ * {@link WifiManager#startEasyConnectAsEnrolleeInitiator(String,
+ * Handler, EasyConnectStatusCallback)}
+ *
+ * @hide
+ */
+@SystemApi
+public abstract class EasyConnectStatusCallback {
+    /**
+     * Easy Connect Success event: Configuration sent (Configurator mode).
+     */
+    public static final int EASY_CONNECT_EVENT_SUCCESS_CONFIGURATION_SENT = 0;
+
+    /** @hide */
+    @IntDef(prefix = {"EASY_CONNECT_EVENT_SUCCESS_"}, value = {
+            EASY_CONNECT_EVENT_SUCCESS_CONFIGURATION_SENT,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface EasyConnectSuccessStatusCode {
+    }
+
+    /**
+     * Easy Connect Progress event: Initial authentication with peer succeeded.
+     */
+    public static final int EASY_CONNECT_EVENT_PROGRESS_AUTHENTICATION_SUCCESS = 0;
+
+    /**
+     * Easy Connect Progress event: Peer requires more time to process bootstrapping.
+     */
+    public static final int EASY_CONNECT_EVENT_PROGRESS_RESPONSE_PENDING = 1;
+
+    /** @hide */
+    @IntDef(prefix = {"EASY_CONNECT_EVENT_PROGRESS_"}, value = {
+            EASY_CONNECT_EVENT_PROGRESS_AUTHENTICATION_SUCCESS,
+            EASY_CONNECT_EVENT_PROGRESS_RESPONSE_PENDING,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface EasyConnectProgressStatusCode {
+    }
+
+    /**
+     * Easy Connect Failure event: Scanned QR code is either not a Easy Connect URI, or the Easy
+     * Connect URI has errors.
+     */
+    public static final int EASY_CONNECT_EVENT_FAILURE_INVALID_URI = -1;
+
+    /**
+     * Easy Connect Failure event: Bootstrapping/Authentication initialization process failure.
+     */
+    public static final int EASY_CONNECT_EVENT_FAILURE_AUTHENTICATION = -2;
+
+    /**
+     * Easy Connect Failure event: Both devices are implementing the same role and are incompatible.
+     */
+    public static final int EASY_CONNECT_EVENT_FAILURE_NOT_COMPATIBLE = -3;
+
+    /**
+     * Easy Connect Failure event: Configuration process has failed due to malformed message.
+     */
+    public static final int EASY_CONNECT_EVENT_FAILURE_CONFIGURATION = -4;
+
+    /**
+     * Easy Connect Failure event: Easy Connect request while in another Easy Connect exchange.
+     */
+    public static final int EASY_CONNECT_EVENT_FAILURE_BUSY = -5;
+
+    /**
+     * Easy Connect Failure event: No response from the peer.
+     */
+    public static final int EASY_CONNECT_EVENT_FAILURE_TIMEOUT = -6;
+
+    /**
+     * Easy Connect Failure event: General protocol failure.
+     */
+    public static final int EASY_CONNECT_EVENT_FAILURE_GENERIC = -7;
+
+    /**
+     * Easy Connect Failure event: Feature or option is not supported.
+     */
+    public static final int EASY_CONNECT_EVENT_FAILURE_NOT_SUPPORTED = -8;
+
+    /**
+     * Easy Connect Failure event: Invalid network provided to Easy Connect configurator.
+     * Network must either be WPA3-Personal (SAE) or WPA2-Personal (PSK).
+     */
+    public static final int EASY_CONNECT_EVENT_FAILURE_INVALID_NETWORK = -9;
+
+
+    /** @hide */
+    @IntDef(prefix = {"EASY_CONNECT_EVENT_FAILURE_"}, value = {
+            EASY_CONNECT_EVENT_FAILURE_INVALID_URI,
+            EASY_CONNECT_EVENT_FAILURE_AUTHENTICATION,
+            EASY_CONNECT_EVENT_FAILURE_NOT_COMPATIBLE,
+            EASY_CONNECT_EVENT_FAILURE_CONFIGURATION,
+            EASY_CONNECT_EVENT_FAILURE_BUSY,
+            EASY_CONNECT_EVENT_FAILURE_TIMEOUT,
+            EASY_CONNECT_EVENT_FAILURE_GENERIC,
+            EASY_CONNECT_EVENT_FAILURE_NOT_SUPPORTED,
+            EASY_CONNECT_EVENT_FAILURE_INVALID_NETWORK,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface EasyConnectFailureStatusCode {
+    }
+
+    /**
+     * Called when local Easy Connect Enrollee successfully receives a new Wi-Fi configuration from
+     * the
+     * peer Easy Connect configurator. This callback marks the successful end of the Easy Connect
+     * current Easy Connect
+     * session, and no further callbacks will be called. This callback is the successful outcome
+     * of a Easy Connect flow starting with
+     * {@link WifiManager#startEasyConnectAsEnrolleeInitiator(String,
+     * Handler,
+     * EasyConnectStatusCallback)}.
+     *
+     * @param newNetworkId New Wi-Fi configuration with a network ID received from the configurator
+     */
+    public abstract void onEnrolleeSuccess(int newNetworkId);
+
+    /**
+     * Called when a Easy Connect success event takes place, except for when configuration is
+     * received from
+     * an external Configurator. The callback onSuccessConfigReceived will be used in this case.
+     * This callback marks the successful end of the current Easy Connect session, and no further
+     * callbacks will be called. This callback is the successful outcome of a Easy Connect flow
+     * starting with
+     * {@link WifiManager#startEasyConnectAsConfiguratorInitiator(String, int, int, Handler,
+     * EasyConnectStatusCallback)}.
+     *
+     * @param code Easy Connect success status code.
+     */
+    public abstract void onConfiguratorSuccess(@EasyConnectSuccessStatusCode int code);
+
+    /**
+     * Called when a Easy Connect Failure event takes place. This callback marks the unsuccessful
+     * end of the
+     * current Easy Connect session, and no further callbacks will be called.
+     *
+     * @param code Easy Connect failure status code.
+     */
+    public abstract void onFailure(@EasyConnectFailureStatusCode int code);
+
+    /**
+     * Called when Easy Connect events that indicate progress take place. Can be used by UI elements
+     * to show progress.
+     *
+     * @param code Easy Connect progress status code.
+     */
+    public abstract void onProgress(@EasyConnectProgressStatusCode int code);
+}
diff --git a/android/net/wifi/ParcelUtil.java b/android/net/wifi/ParcelUtil.java
new file mode 100644
index 0000000..a26877d
--- /dev/null
+++ b/android/net/wifi/ParcelUtil.java
@@ -0,0 +1,165 @@
+/**
+ * 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.wifi;
+
+import android.os.Parcel;
+
+import java.io.ByteArrayInputStream;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.PKCS8EncodedKeySpec;
+
+/**
+ * Provides utilities for writing/reading a non-Parcelable objects to/from
+ * a Parcel object.
+ *
+ * @hide
+ */
+public class ParcelUtil {
+    /**
+     * Write a PrivateKey object |key| to the specified Parcel |dest|.
+     *
+     * Below is the data format:
+     * |algorithm|     -> String of algorithm name
+     * |endcodedKey|  -> byte[] of key data
+     *
+     * For a null PrivateKey object, a null string will be written to |algorithm| and
+     * |encodedKey| will be skipped. Since a PrivateKey can only be constructed with
+     * a valid algorithm String.
+     *
+     * @param dest Parcel object to write to
+     * @param key PrivateKey object to read from.
+     */
+    public static void writePrivateKey(Parcel dest, PrivateKey key) {
+        if (key == null) {
+            dest.writeString(null);
+            return;
+        }
+
+        dest.writeString(key.getAlgorithm());
+        dest.writeByteArray(key.getEncoded());
+    }
+
+    /**
+     * Read/create a PrivateKey object from a specified Parcel object |in|.
+     *
+     * Refer to the function above for the expected data format.
+     *
+     * @param in Parcel object to read from
+     * @return a PrivateKey object or null
+     */
+    public static PrivateKey readPrivateKey(Parcel in) {
+        String algorithm = in.readString();
+        if (algorithm == null) {
+            return null;
+        }
+
+        byte[] userKeyBytes = in.createByteArray();
+        try {
+            KeyFactory keyFactory = KeyFactory.getInstance(algorithm);
+            return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(userKeyBytes));
+       } catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
+            return null;
+       }
+    }
+
+    /**
+     * Write a X509Certificate object |cert| to a Parcel object |dest|.
+     * The data being written to the Parcel is just a byte[] of the encoded certificate data.
+     *
+     * @param dest Parcel object to write to
+     * @param cert X509Certificate object to read from
+     */
+    public static void writeCertificate(Parcel dest, X509Certificate cert) {
+        byte[] certBytes = null;
+        if (cert != null) {
+            try {
+                certBytes = cert.getEncoded();
+            } catch (CertificateEncodingException e) {
+                /* empty, write null. */
+            }
+        }
+        dest.writeByteArray(certBytes);
+    }
+
+    /**
+     * Read/create a X509Certificate object from a specified Parcel object |in|.
+     *
+     * @param in Parcel object to read from
+     * @return a X509Certficate object or null
+     */
+    public static X509Certificate readCertificate(Parcel in) {
+        byte[] certBytes = in.createByteArray();
+        if (certBytes == null) {
+            return null;
+        }
+
+        try {
+            CertificateFactory cFactory = CertificateFactory.getInstance("X.509");
+            return (X509Certificate) cFactory
+                    .generateCertificate(new ByteArrayInputStream(certBytes));
+        } catch (CertificateException e) {
+            return null;
+        }
+    }
+
+    /**
+     * Write an array of X509Certificate objects |certs| to a Parcel object |dest|.
+     * The data being written to the Parcel are consist of an integer indicating
+     * the size of the array and the certificates data.  Certificates data will be
+     * skipped for a null array or size of 0 array.
+     *
+     * @param dest Parcel object to write to
+     * @param certs array of X509Certificate objects to read from
+     */
+    public static void writeCertificates(Parcel dest, X509Certificate[] certs) {
+        if (certs == null || certs.length == 0) {
+            dest.writeInt(0);
+            return;
+        }
+
+        dest.writeInt(certs.length);
+        for (int i = 0; i < certs.length; i++) {
+            writeCertificate(dest, certs[i]);
+        }
+    }
+
+    /**
+     * Read/create an array of X509Certificate objects from a specified Parcel object |in|.
+     *
+     * @param in Parcel object to read from
+     * @return X509Certficate[] or null
+     */
+    public static X509Certificate[] readCertificates(Parcel in) {
+        int length = in.readInt();
+        if (length == 0) {
+            return null;
+        }
+
+        X509Certificate[] certs = new X509Certificate[length];
+        for (int i = 0; i < length; i++) {
+            certs[i] = readCertificate(in);
+        }
+        return certs;
+    }
+}
diff --git a/android/net/wifi/PasspointManagementObjectDefinition.java b/android/net/wifi/PasspointManagementObjectDefinition.java
new file mode 100644
index 0000000..70577b9
--- /dev/null
+++ b/android/net/wifi/PasspointManagementObjectDefinition.java
@@ -0,0 +1,81 @@
+/*
+ * 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.wifi;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * This object describes a partial tree structure in the Hotspot 2.0 release 2 management object.
+ * The object is used during subscription remediation to modify parts of an existing PPS MO
+ * tree (Hotspot 2.0 specification section 9.1).
+ * @hide
+ */
+public class PasspointManagementObjectDefinition implements Parcelable {
+    private final String mBaseUri;
+    private final String mUrn;
+    private final String mMoTree;
+
+    public PasspointManagementObjectDefinition(String baseUri, String urn, String moTree) {
+        mBaseUri = baseUri;
+        mUrn = urn;
+        mMoTree = moTree;
+    }
+
+    public String getBaseUri() {
+        return mBaseUri;
+    }
+
+    public String getUrn() {
+        return mUrn;
+    }
+
+    public String getMoTree() {
+        return mMoTree;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeString(mBaseUri);
+        dest.writeString(mUrn);
+        dest.writeString(mMoTree);
+    }
+
+    /**
+     * Implement the Parcelable interface {@hide}
+     */
+    public static final @android.annotation.NonNull Creator<PasspointManagementObjectDefinition> CREATOR =
+            new Creator<PasspointManagementObjectDefinition>() {
+                public PasspointManagementObjectDefinition createFromParcel(Parcel in) {
+                    return new PasspointManagementObjectDefinition(
+                            in.readString(),    /* base URI */
+                            in.readString(),    /* URN */
+                            in.readString()     /* Tree XML */
+                    );
+                }
+
+                public PasspointManagementObjectDefinition[] newArray(int size) {
+                    return new PasspointManagementObjectDefinition[size];
+                }
+            };
+}
+
diff --git a/android/net/wifi/RssiPacketCountInfo.java b/android/net/wifi/RssiPacketCountInfo.java
new file mode 100644
index 0000000..4301165
--- /dev/null
+++ b/android/net/wifi/RssiPacketCountInfo.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2012 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.wifi;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Bundle of RSSI and packet count information, for WiFi watchdog
+ *
+ * @see WifiWatchdogStateMachine
+ *
+ * @hide
+ */
+public class RssiPacketCountInfo implements Parcelable {
+
+    public int rssi;
+
+    public int txgood;
+
+    public int txbad;
+
+    public int rxgood;
+
+    public RssiPacketCountInfo() {
+        rssi = txgood = txbad = rxgood = 0;
+    }
+
+    private RssiPacketCountInfo(Parcel in) {
+        rssi = in.readInt();
+        txgood = in.readInt();
+        txbad = in.readInt();
+        rxgood = in.readInt();
+    }
+
+    @Override
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeInt(rssi);
+        out.writeInt(txgood);
+        out.writeInt(txbad);
+        out.writeInt(rxgood);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public static final @android.annotation.NonNull Parcelable.Creator<RssiPacketCountInfo> CREATOR =
+            new Parcelable.Creator<RssiPacketCountInfo>() {
+        @Override
+        public RssiPacketCountInfo createFromParcel(Parcel in) {
+            return new RssiPacketCountInfo(in);
+        }
+
+        @Override
+        public RssiPacketCountInfo[] newArray(int size) {
+            return new RssiPacketCountInfo[size];
+        }
+    };
+}
diff --git a/android/net/wifi/RttManager.java b/android/net/wifi/RttManager.java
new file mode 100644
index 0000000..6ce2121
--- /dev/null
+++ b/android/net/wifi/RttManager.java
@@ -0,0 +1,1231 @@
+package android.net.wifi;
+
+import android.Manifest;
+import android.annotation.NonNull;
+import android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
+import android.annotation.SystemService;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.net.wifi.rtt.RangingRequest;
+import android.net.wifi.rtt.RangingResult;
+import android.net.wifi.rtt.RangingResultCallback;
+import android.net.wifi.rtt.WifiRttManager;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Protocol;
+
+import java.util.List;
+
+/** @hide */
+@SystemApi
+@Deprecated
+@SystemService(Context.WIFI_RTT_SERVICE)
+public class RttManager {
+
+    private static final boolean DBG = false;
+    private static final String TAG = "RttManager";
+
+    /** @deprecated It is Not supported anymore. */
+    @Deprecated
+    public static final int RTT_TYPE_UNSPECIFIED        = 0;
+
+    public static final int RTT_TYPE_ONE_SIDED          = 1;
+    public static final int RTT_TYPE_TWO_SIDED          = 2;
+
+    /** @deprecated It is not supported anymore. */
+    @Deprecated
+    public static final int RTT_TYPE_11_V               = 2;
+
+    /** @deprecated It is not supported anymore. */
+    @Deprecated
+    public static final int RTT_TYPE_11_MC              = 4;
+
+    /** @deprecated It is not supported anymore. */
+    @Deprecated
+    public static final int RTT_PEER_TYPE_UNSPECIFIED    = 0;
+
+    public static final int RTT_PEER_TYPE_AP             = 1;
+    public static final int RTT_PEER_TYPE_STA            = 2;       /* requires NAN */
+    public static final int RTT_PEER_P2P_GO              = 3;
+    public static final int RTT_PEER_P2P_CLIENT          = 4;
+    public static final int RTT_PEER_NAN                 = 5;
+
+    /**
+     * @deprecated It is not supported anymore.
+     * Use {@link android.net.wifi.RttManager#RTT_BW_20_SUPPORT} API.
+     */
+    @Deprecated
+    public static final int RTT_CHANNEL_WIDTH_20      = 0;
+
+    /**
+     * @deprecated It is not supported anymore.
+     * Use {@link android.net.wifi.RttManager#RTT_BW_40_SUPPORT} API.
+     */
+    @Deprecated
+    public static final int RTT_CHANNEL_WIDTH_40      = 1;
+
+    /**
+     * @deprecated It is not supported anymore.
+     * Use {@link android.net.wifi.RttManager#RTT_BW_80_SUPPORT} API.
+     */
+    @Deprecated
+    public static final int RTT_CHANNEL_WIDTH_80      = 2;
+
+    /**@deprecated It is not supported anymore.
+     * Use {@link android.net.wifi.RttManager#RTT_BW_160_SUPPORT} API.
+     */
+    @Deprecated
+    public static final int RTT_CHANNEL_WIDTH_160     = 3;
+
+    /**@deprecated not supported anymore*/
+    @Deprecated
+    public static final int RTT_CHANNEL_WIDTH_80P80   = 4;
+
+    /**@deprecated It is not supported anymore.
+     * Use {@link android.net.wifi.RttManager#RTT_BW_5_SUPPORT} API.
+     */
+    @Deprecated
+    public static final int RTT_CHANNEL_WIDTH_5       = 5;
+
+    /**@deprecated It is not supported anymore.
+     * Use {@link android.net.wifi.RttManager#RTT_BW_10_SUPPORT} API.
+     */
+    @Deprecated
+    public static final int RTT_CHANNEL_WIDTH_10      = 6;
+
+    /** @deprecated channel info must be specified. */
+    @Deprecated
+    public static final int RTT_CHANNEL_WIDTH_UNSPECIFIED = -1;
+
+    public static final int RTT_STATUS_SUCCESS                  = 0;
+    /** General failure*/
+    public static final int RTT_STATUS_FAILURE                  = 1;
+    /** Destination does not respond to RTT request*/
+    public static final int RTT_STATUS_FAIL_NO_RSP              = 2;
+    /** RTT request is rejected by the destination. Double side RTT only*/
+    public static final int RTT_STATUS_FAIL_REJECTED            = 3;
+    /** */
+    public static final int RTT_STATUS_FAIL_NOT_SCHEDULED_YET   = 4;
+    /** Timing measurement timeout*/
+    public static final int RTT_STATUS_FAIL_TM_TIMEOUT          = 5;
+    /** Destination is on a different channel from the RTT Request*/
+    public static final int RTT_STATUS_FAIL_AP_ON_DIFF_CHANNEL  = 6;
+    /** This type of Ranging is not support by Hardware*/
+    public static final int RTT_STATUS_FAIL_NO_CAPABILITY       = 7;
+    /** Request abort fro uncertain reason*/
+    public static final int RTT_STATUS_ABORTED                  = 8;
+    /** The T1-T4 or TOD/TOA Timestamp is illegal*/
+    public static final int RTT_STATUS_FAIL_INVALID_TS          = 9;
+    /** 11mc protocol level failed, eg, unrecognized FTMR/FTM frame*/
+    public static final int RTT_STATUS_FAIL_PROTOCOL            = 10;
+    /** Request can not be scheduled by hardware*/
+    public static final int RTT_STATUS_FAIL_SCHEDULE            = 11;
+    /** destination is busy now, you can try after a specified time from destination*/
+    public static final int RTT_STATUS_FAIL_BUSY_TRY_LATER      = 12;
+    /** Bad Request argument*/
+    public static final int RTT_STATUS_INVALID_REQ              = 13;
+    /** Wifi is not enabled*/
+    public static final int RTT_STATUS_NO_WIFI                  = 14;
+    /** Responder overrides param info, cannot range with new params 2-side RTT only*/
+    public static final int RTT_STATUS_FAIL_FTM_PARAM_OVERRIDE  = 15;
+
+    public static final int REASON_UNSPECIFIED              = -1;
+    public static final int REASON_NOT_AVAILABLE            = -2;
+    public static final int REASON_INVALID_LISTENER         = -3;
+    public static final int REASON_INVALID_REQUEST          = -4;
+    /** Do not have required permission */
+    public static final int REASON_PERMISSION_DENIED        = -5;
+    /** Ranging failed because responder role is enabled in STA mode.*/
+    public static final int
+            REASON_INITIATOR_NOT_ALLOWED_WHEN_RESPONDER_ON  = -6;
+
+    public static final String DESCRIPTION_KEY  = "android.net.wifi.RttManager.Description";
+
+    /**
+     * RTT BW supported bit mask, used as RTT param bandWidth too
+     */
+    public static final int RTT_BW_5_SUPPORT   = 0x01;
+    public static final int RTT_BW_10_SUPPORT  = 0x02;
+    public static final int RTT_BW_20_SUPPORT  = 0x04;
+    public static final int RTT_BW_40_SUPPORT  = 0x08;
+    public static final int RTT_BW_80_SUPPORT  = 0x10;
+    public static final int RTT_BW_160_SUPPORT = 0x20;
+
+    /**
+     * RTT Preamble Support bit mask
+     */
+    public static final int PREAMBLE_LEGACY  = 0x01;
+    public static final int PREAMBLE_HT      = 0x02;
+    public static final int PREAMBLE_VHT     = 0x04;
+
+    /** @deprecated Use the new {@link android.net.wifi.RttManager.RttCapabilities} API */
+    @Deprecated
+    public class Capabilities {
+        public int supportedType;
+        public int supportedPeerType;
+    }
+
+    /** @deprecated Use the new {@link android.net.wifi.RttManager#getRttCapabilities()} API.*/
+    @Deprecated
+    @SuppressLint("Doclava125")
+    public Capabilities getCapabilities() {
+        throw new UnsupportedOperationException(
+                "getCapabilities is not supported in the adaptation layer");
+    }
+
+    /**
+     * This class describe the RTT capability of the Hardware
+     */
+    @Deprecated
+    public static class RttCapabilities implements Parcelable {
+        /** @deprecated It is not supported*/
+        @Deprecated
+        public boolean supportedType;
+        /** @deprecated It is not supported*/
+        @Deprecated
+        public boolean supportedPeerType;
+        //1-sided rtt measurement is supported
+        public boolean oneSidedRttSupported;
+        //11mc 2-sided rtt measurement is supported
+        public boolean twoSided11McRttSupported;
+        //location configuration information supported
+        public boolean lciSupported;
+        //location civic records supported
+        public boolean lcrSupported;
+        //preamble supported, see bit mask definition above
+        public int preambleSupported;
+        //RTT bandwidth supported
+        public int bwSupported;
+        // Whether STA responder role is supported.
+        public boolean responderSupported;
+
+        /** Whether the secure RTT protocol is supported. */
+        public boolean secureRttSupported;
+
+        /** Draft 11mc version supported, including major and minor version. e.g, draft 4.3 is 43 */
+        public int mcVersion;
+
+        @Override
+        public String toString() {
+            StringBuffer sb = new StringBuffer();
+            sb.append("oneSidedRtt ").
+            append(oneSidedRttSupported ? "is Supported. " : "is not supported. ").
+            append("twoSided11McRtt ").
+            append(twoSided11McRttSupported ? "is Supported. " : "is not supported. ").
+            append("lci ").
+            append(lciSupported ? "is Supported. " : "is not supported. ").
+            append("lcr ").
+            append(lcrSupported ? "is Supported. " : "is not supported. ");
+
+            if ((preambleSupported & PREAMBLE_LEGACY) != 0) {
+                sb.append("Legacy ");
+            }
+
+            if ((preambleSupported & PREAMBLE_HT) != 0) {
+                sb.append("HT ");
+            }
+
+            if ((preambleSupported & PREAMBLE_VHT) != 0) {
+                sb.append("VHT ");
+            }
+
+            sb.append("is supported. ");
+
+            if ((bwSupported & RTT_BW_5_SUPPORT) != 0) {
+                sb.append("5 MHz ");
+            }
+
+            if ((bwSupported & RTT_BW_10_SUPPORT) != 0) {
+                sb.append("10 MHz ");
+            }
+
+            if ((bwSupported & RTT_BW_20_SUPPORT) != 0) {
+                sb.append("20 MHz ");
+            }
+
+            if ((bwSupported & RTT_BW_40_SUPPORT) != 0) {
+                sb.append("40 MHz ");
+            }
+
+            if ((bwSupported & RTT_BW_80_SUPPORT) != 0) {
+                sb.append("80 MHz ");
+            }
+
+            if ((bwSupported & RTT_BW_160_SUPPORT) != 0) {
+                sb.append("160 MHz ");
+            }
+
+            sb.append("is supported.");
+
+            sb.append(" STA responder role is ")
+                    .append(responderSupported ? "supported" : "not supported");
+            sb.append(" Secure RTT protocol is ")
+                    .append(secureRttSupported ? "supported" : "not supported");
+            sb.append(" 11mc version is " + mcVersion);
+
+            return sb.toString();
+        }
+        /** Implement the Parcelable interface {@hide} */
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        /** Implement the Parcelable interface {@hide} */
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeInt(oneSidedRttSupported ? 1 : 0);
+            dest.writeInt(twoSided11McRttSupported ? 1 : 0);
+            dest.writeInt(lciSupported ? 1 : 0);
+            dest.writeInt(lcrSupported ? 1 : 0);
+            dest.writeInt(preambleSupported);
+            dest.writeInt(bwSupported);
+            dest.writeInt(responderSupported ? 1 : 0);
+            dest.writeInt(secureRttSupported ? 1 : 0);
+            dest.writeInt(mcVersion);
+        }
+
+        /** Implement the Parcelable interface {@hide} */
+        public static final @android.annotation.NonNull Creator<RttCapabilities> CREATOR =
+            new Creator<RttCapabilities>() {
+            @Override
+            public RttCapabilities createFromParcel(Parcel in) {
+                RttCapabilities capabilities = new RttCapabilities();
+                capabilities.oneSidedRttSupported = (in.readInt() == 1);
+                capabilities.twoSided11McRttSupported = (in.readInt() == 1);
+                capabilities.lciSupported = (in.readInt() == 1);
+                capabilities.lcrSupported = (in.readInt() == 1);
+                capabilities.preambleSupported = in.readInt();
+                capabilities.bwSupported = in.readInt();
+                capabilities.responderSupported = (in.readInt() == 1);
+                capabilities.secureRttSupported = (in.readInt() == 1);
+                capabilities.mcVersion = in.readInt();
+                return capabilities;
+            }
+                /** Implement the Parcelable interface {@hide} */
+                @Override
+                public RttCapabilities[] newArray(int size) {
+                    return new RttCapabilities[size];
+                }
+             };
+    }
+
+    /**
+     * This method is deprecated. Please use the {@link WifiRttManager} API.
+     */
+    @RequiresPermission(Manifest.permission.LOCATION_HARDWARE)
+    public RttCapabilities getRttCapabilities() {
+        return mRttCapabilities;
+    }
+
+    /** specifies parameters for RTT request */
+    @Deprecated
+    public static class RttParams {
+        /**
+         * type of destination device being ranged
+         * currently only support RTT_PEER_TYPE_AP
+         * Range:RTT_PEER_TYPE_xxxx Default value:RTT_PEER_TYPE_AP
+         */
+        public int deviceType;
+
+        /**
+         * type of RTT measurement method. Need check scan result and RttCapabilities first
+         * Range: RTT_TYPE_ONE_SIDED or RTT_TYPE_TWO_SIDED
+         * Default value: RTT_TYPE_ONE_SIDED
+         */
+        public int requestType;
+
+        /**
+         * Whether the secure RTT protocol needs to be used for ranging this peer device.
+         */
+        public boolean secure;
+
+        /**
+         * mac address of the device being ranged
+         * Default value: null
+         */
+        public String bssid;
+
+        /**
+         * The primary control channel over which the client is
+         * communicating with the AP.Same as ScanResult.frequency
+         * Default value: 0
+         */
+        public int frequency;
+
+        /**
+         * channel width of the destination AP. Same as ScanResult.channelWidth
+         * Default value: 0
+         */
+        public int channelWidth;
+
+        /**
+         * Not used if the AP bandwidth is 20 MHz
+         * If the AP use 40, 80 or 160 MHz, this is the center frequency
+         * if the AP use 80 + 80 MHz, this is the center frequency of the first segment
+         * same as ScanResult.centerFreq0
+         * Default value: 0
+         */
+         public int centerFreq0;
+
+         /**
+          * Only used if the AP bandwidth is 80 + 80 MHz
+          * if the AP use 80 + 80 MHz, this is the center frequency of the second segment
+          * same as ScanResult.centerFreq1
+          * Default value: 0
+          */
+          public int centerFreq1;
+
+        /**
+         * number of samples to be taken
+         * @deprecated Use the new {@link android.net.wifi.RttManager.RttParams#numSamplesPerBurst}
+         */
+        @Deprecated
+        public int num_samples;
+
+        /**
+         * number of retries if a sample fails
+         * @deprecated
+         * Use {@link android.net.wifi.RttManager.RttParams#numRetriesPerMeasurementFrame} API.
+         */
+        @Deprecated
+        public int num_retries;
+
+        /** Number of burst in exp , 2^x. 0 means single shot measurement, range 0-15
+         * Currently only single shot is supported
+         * Default value: 0
+         */
+        public int numberBurst;
+
+        /**
+         * valid only if numberBurst > 1, interval between burst(100ms).
+         * Range : 0-31, 0--means no specific
+         * Default value: 0
+         */
+        public int interval;
+
+        /**
+         * number of samples to be taken in one burst
+         * Range: 1-31
+         * Default value: 8
+         */
+        public int numSamplesPerBurst;
+
+        /** number of retries for each measurement frame if a sample fails
+         *  Only used by single side RTT,
+         *  Range 0 - 3 Default value: 0
+         */
+        public int numRetriesPerMeasurementFrame;
+
+        /**
+         * number of retries for FTMR frame (control frame) if it fails.
+         * Only used by 80211MC double side RTT
+         * Range: 0-3  Default Value : 0
+         */
+        public int numRetriesPerFTMR;
+
+        /**
+         * Request LCI information, only available when choose double side RTT measurement
+         * need check RttCapabilties first.
+         * Default value: false
+         * */
+        public boolean LCIRequest;
+
+        /**
+         * Request LCR information, only available when choose double side RTT measurement
+         * need check RttCapabilties first.
+         * Default value: false
+         * */
+        public boolean LCRRequest;
+
+        /**
+         * Timeout for each burst, (250 * 2^x) us,
+         * Range 1-11 and 15. 15 means no control Default value: 15
+         * */
+        public int burstTimeout;
+
+        /** preamble used for RTT measurement
+         *  Range: PREAMBLE_LEGACY, PREAMBLE_HT, PREAMBLE_VHT
+         *  Default value: PREAMBLE_HT
+         */
+        public int preamble;
+
+        /** bandWidth used for RTT measurement.User need verify the highest BW the destination
+         * support (from scan result etc) before set this value. Wider channels result usually give
+         * better accuracy. However, the frame loss can increase too.
+         * should be one of RTT_BW_5_SUPPORT to RTT_BW_160_SUPPORT. However, need check
+         * RttCapabilities firstto verify HW support this bandwidth.
+         * Default value:RTT_BW_20_SUPPORT
+         */
+        public int bandwidth;
+
+        public RttParams() {
+            //provide initial value for RttParams
+            deviceType = RTT_PEER_TYPE_AP;
+            requestType = RTT_TYPE_ONE_SIDED;
+            numberBurst = 0;
+            numSamplesPerBurst = 8;
+            numRetriesPerMeasurementFrame  = 0;
+            numRetriesPerFTMR = 0;
+            burstTimeout = 15;
+            preamble = PREAMBLE_HT;
+            bandwidth = RTT_BW_20_SUPPORT;
+        }
+
+        /**
+         * {@hide}
+         */
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            sb.append("deviceType=" + deviceType);
+            sb.append(", requestType=" + requestType);
+            sb.append(", secure=" + secure);
+            sb.append(", bssid=" + bssid);
+            sb.append(", frequency=" + frequency);
+            sb.append(", channelWidth=" + channelWidth);
+            sb.append(", centerFreq0=" + centerFreq0);
+            sb.append(", centerFreq1=" + centerFreq1);
+            sb.append(", num_samples=" + num_samples);
+            sb.append(", num_retries=" + num_retries);
+            sb.append(", numberBurst=" + numberBurst);
+            sb.append(", interval=" + interval);
+            sb.append(", numSamplesPerBurst=" + numSamplesPerBurst);
+            sb.append(", numRetriesPerMeasurementFrame=" + numRetriesPerMeasurementFrame);
+            sb.append(", numRetriesPerFTMR=" + numRetriesPerFTMR);
+            sb.append(", LCIRequest=" + LCIRequest);
+            sb.append(", LCRRequest=" + LCRRequest);
+            sb.append(", burstTimeout=" + burstTimeout);
+            sb.append(", preamble=" + preamble);
+            sb.append(", bandwidth=" + bandwidth);
+            return sb.toString();
+        }
+    }
+
+    /** pseudo-private class used to parcel arguments */
+    @Deprecated
+    public static class ParcelableRttParams implements Parcelable {
+
+        @NonNull
+        public RttParams mParams[];
+
+        /**
+         * @hide
+         */
+        @VisibleForTesting
+        public ParcelableRttParams(RttParams[] params) {
+            mParams = (params == null ? new RttParams[0] : params);
+        }
+
+        /** Implement the Parcelable interface {@hide} */
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        /** Implement the Parcelable interface {@hide} */
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeInt(mParams.length);
+
+            for (RttParams params : mParams) {
+                dest.writeInt(params.deviceType);
+                dest.writeInt(params.requestType);
+                dest.writeByte(params.secure ? (byte) 1 : 0);
+                dest.writeString(params.bssid);
+                dest.writeInt(params.channelWidth);
+                dest.writeInt(params.frequency);
+                dest.writeInt(params.centerFreq0);
+                dest.writeInt(params.centerFreq1);
+                dest.writeInt(params.numberBurst);
+                dest.writeInt(params.interval);
+                dest.writeInt(params.numSamplesPerBurst);
+                dest.writeInt(params.numRetriesPerMeasurementFrame);
+                dest.writeInt(params.numRetriesPerFTMR);
+                dest.writeInt(params.LCIRequest ? 1 : 0);
+                dest.writeInt(params.LCRRequest ? 1 : 0);
+                dest.writeInt(params.burstTimeout);
+                dest.writeInt(params.preamble);
+                dest.writeInt(params.bandwidth);
+            }
+        }
+
+        /** Implement the Parcelable interface {@hide} */
+        public static final @android.annotation.NonNull Creator<ParcelableRttParams> CREATOR =
+                new Creator<ParcelableRttParams>() {
+                    @Override
+                    public ParcelableRttParams createFromParcel(Parcel in) {
+
+                        int num = in.readInt();
+                        RttParams params[] = new RttParams[num];
+                        for (int i = 0; i < num; i++) {
+                            params[i] = new RttParams();
+                            params[i].deviceType = in.readInt();
+                            params[i].requestType = in.readInt();
+                            params[i].secure = (in.readByte() != 0);
+                            params[i].bssid = in.readString();
+                            params[i].channelWidth = in.readInt();
+                            params[i].frequency = in.readInt();
+                            params[i].centerFreq0 = in.readInt();
+                            params[i].centerFreq1 = in.readInt();
+                            params[i].numberBurst = in.readInt();
+                            params[i].interval = in.readInt();
+                            params[i].numSamplesPerBurst = in.readInt();
+                            params[i].numRetriesPerMeasurementFrame = in.readInt();
+                            params[i].numRetriesPerFTMR = in.readInt();
+                            params[i].LCIRequest = (in.readInt() == 1);
+                            params[i].LCRRequest = (in.readInt() == 1);
+                            params[i].burstTimeout = in.readInt();
+                            params[i].preamble = in.readInt();
+                            params[i].bandwidth = in.readInt();
+                        }
+
+                        ParcelableRttParams parcelableParams = new ParcelableRttParams(params);
+                        return parcelableParams;
+                    }
+
+                    @Override
+                    public ParcelableRttParams[] newArray(int size) {
+                        return new ParcelableRttParams[size];
+                    }
+                };
+    }
+
+    @Deprecated
+    public static class WifiInformationElement {
+        /** Information Element ID 0xFF means element is invalid. */
+        public byte id;
+        public byte[] data;
+    }
+    /** specifies RTT results */
+    @Deprecated
+    public static class RttResult {
+        /** mac address of the device being ranged. */
+        public String bssid;
+
+        /** # of burst for this measurement. */
+        public int burstNumber;
+
+        /** total number of measurement frames attempted in this measurement. */
+        public int measurementFrameNumber;
+
+        /** total successful number of measurement frames in this measurement. */
+        public int successMeasurementFrameNumber;
+
+        /**
+         * Maximum number of frames per burst supported by peer. Two side RTT only
+         * Valid only if less than request
+         */
+        public int frameNumberPerBurstPeer;
+
+        /** status of the request */
+        public int status;
+
+        /**
+         * type of the request used
+         * @deprecated Use {@link android.net.wifi.RttManager.RttResult#measurementType}
+         */
+        @Deprecated
+        public int requestType;
+
+        /** RTT measurement method type used, should be one of RTT_TYPE_ONE_SIDED or
+         *  RTT_TYPE_TWO_SIDED.
+         */
+        public int measurementType;
+
+        /**
+         * only valid when status ==  RTT_STATUS_FAIL_BUSY_TRY_LATER
+         * please retry RTT measurement after this duration since peer indicate busy at ths moment
+         *  Unit S  Range:1-31
+         */
+        public int retryAfterDuration;
+
+        /** timestamp of completion, in microsecond since boot. */
+        public long ts;
+
+        /** average RSSI observed, unit of 0.5 dB. */
+        public int rssi;
+
+        /**
+         * RSSI spread (i.e. max - min)
+         * @deprecated Use {@link android.net.wifi.RttManager.RttResult#rssiSpread} API.
+         */
+        @Deprecated
+        public int rssi_spread;
+
+        /**RSSI spread (i.e. max - min), unit of 0.5 dB. */
+        public int rssiSpread;
+
+        /**
+         * average transmit rate
+         * @deprecated Use {@link android.net.wifi.RttManager.RttResult#txRate} API.
+         */
+        @Deprecated
+        public int tx_rate;
+
+        /** average transmit rate. Unit (kbps). */
+        public int txRate;
+
+        /** average receiving rate Unit (kbps). */
+        public int rxRate;
+
+       /**
+        * average round trip time in nano second
+        * @deprecated  Use {@link android.net.wifi.RttManager.RttResult#rtt} API.
+        */
+        @Deprecated
+        public long rtt_ns;
+
+        /** average round trip time in picoseconds. */
+        public long rtt;
+
+        /**
+         * standard deviation observed in round trip time
+         * @deprecated Use {@link android.net.wifi.RttManager.RttResult#rttStandardDeviation} API.
+         */
+        @Deprecated
+        public long rtt_sd_ns;
+
+        /** standard deviation of RTT in picoseconds. */
+        public long rttStandardDeviation;
+
+        /**
+         * spread (i.e. max - min) round trip time
+         * @deprecated Use {@link android.net.wifi.RttManager.RttResult#rttSpread} API.
+         */
+        @Deprecated
+        public long rtt_spread_ns;
+
+        /** spread (i.e. max - min) RTT in picoseconds. */
+        public long rttSpread;
+
+        /**
+         * average distance in centimeter, computed based on rtt_ns
+         * @deprecated use {@link android.net.wifi.RttManager.RttResult#distance} API.
+         */
+        @Deprecated
+        public int distance_cm;
+
+        /** average distance in cm, computed based on rtt. */
+        public int distance;
+
+        /**
+         * standard deviation observed in distance
+         * @deprecated
+         * Use {@link .android.net.wifi.RttManager.RttResult#distanceStandardDeviation} API.
+         */
+        @Deprecated
+        public int distance_sd_cm;
+
+        /** standard deviation observed in distance in cm. */
+        public int distanceStandardDeviation;
+
+        /**
+         * spread (i.e. max - min) distance
+         * @deprecated Use {@link android.net.wifi.RttManager.RttResult#distanceSpread} API.
+         */
+        @Deprecated
+        public int distance_spread_cm;
+
+        /** spread (i.e. max - min) distance in cm. */
+        public int distanceSpread;
+
+        /** the duration of this measurement burst, unit ms. */
+        public int burstDuration;
+
+        /** Burst number supported by peer after negotiation, 2side RTT only*/
+        public int negotiatedBurstNum;
+
+        /** LCI information Element, only available for double side RTT. */
+        public WifiInformationElement LCI;
+
+        /** LCR information Element, only available to double side RTT. */
+        public WifiInformationElement LCR;
+
+        /**
+         * Whether the secure RTT protocol was used for ranging.
+         */
+        public boolean secure;
+    }
+
+
+    /** pseudo-private class used to parcel results. */
+    @Deprecated
+    public static class ParcelableRttResults implements Parcelable {
+
+        public RttResult mResults[];
+
+        public ParcelableRttResults(RttResult[] results) {
+            mResults = results;
+        }
+
+        /**
+         * {@hide}
+         */
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            for (int i = 0; i < mResults.length; ++i) {
+                sb.append("[" + i + "]: ");
+                sb.append("bssid=" + mResults[i].bssid);
+                sb.append(", burstNumber=" + mResults[i].burstNumber);
+                sb.append(", measurementFrameNumber=" + mResults[i].measurementFrameNumber);
+                sb.append(", successMeasurementFrameNumber="
+                        + mResults[i].successMeasurementFrameNumber);
+                sb.append(", frameNumberPerBurstPeer=" + mResults[i].frameNumberPerBurstPeer);
+                sb.append(", status=" + mResults[i].status);
+                sb.append(", requestType=" + mResults[i].requestType);
+                sb.append(", measurementType=" + mResults[i].measurementType);
+                sb.append(", retryAfterDuration=" + mResults[i].retryAfterDuration);
+                sb.append(", ts=" + mResults[i].ts);
+                sb.append(", rssi=" + mResults[i].rssi);
+                sb.append(", rssi_spread=" + mResults[i].rssi_spread);
+                sb.append(", rssiSpread=" + mResults[i].rssiSpread);
+                sb.append(", tx_rate=" + mResults[i].tx_rate);
+                sb.append(", txRate=" + mResults[i].txRate);
+                sb.append(", rxRate=" + mResults[i].rxRate);
+                sb.append(", rtt_ns=" + mResults[i].rtt_ns);
+                sb.append(", rtt=" + mResults[i].rtt);
+                sb.append(", rtt_sd_ns=" + mResults[i].rtt_sd_ns);
+                sb.append(", rttStandardDeviation=" + mResults[i].rttStandardDeviation);
+                sb.append(", rtt_spread_ns=" + mResults[i].rtt_spread_ns);
+                sb.append(", rttSpread=" + mResults[i].rttSpread);
+                sb.append(", distance_cm=" + mResults[i].distance_cm);
+                sb.append(", distance=" + mResults[i].distance);
+                sb.append(", distance_sd_cm=" + mResults[i].distance_sd_cm);
+                sb.append(", distanceStandardDeviation=" + mResults[i].distanceStandardDeviation);
+                sb.append(", distance_spread_cm=" + mResults[i].distance_spread_cm);
+                sb.append(", distanceSpread=" + mResults[i].distanceSpread);
+                sb.append(", burstDuration=" + mResults[i].burstDuration);
+                sb.append(", negotiatedBurstNum=" + mResults[i].negotiatedBurstNum);
+                sb.append(", LCI=" + mResults[i].LCI);
+                sb.append(", LCR=" + mResults[i].LCR);
+                sb.append(", secure=" + mResults[i].secure);
+            }
+            return sb.toString();
+        }
+
+        /** Implement the Parcelable interface {@hide} */
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        /** Implement the Parcelable interface {@hide} */
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            if (mResults != null) {
+                dest.writeInt(mResults.length);
+                for (RttResult result : mResults) {
+                    dest.writeString(result.bssid);
+                    dest.writeInt(result.burstNumber);
+                    dest.writeInt(result.measurementFrameNumber);
+                    dest.writeInt(result.successMeasurementFrameNumber);
+                    dest.writeInt(result.frameNumberPerBurstPeer);
+                    dest.writeInt(result.status);
+                    dest.writeInt(result.measurementType);
+                    dest.writeInt(result.retryAfterDuration);
+                    dest.writeLong(result.ts);
+                    dest.writeInt(result.rssi);
+                    dest.writeInt(result.rssiSpread);
+                    dest.writeInt(result.txRate);
+                    dest.writeLong(result.rtt);
+                    dest.writeLong(result.rttStandardDeviation);
+                    dest.writeLong(result.rttSpread);
+                    dest.writeInt(result.distance);
+                    dest.writeInt(result.distanceStandardDeviation);
+                    dest.writeInt(result.distanceSpread);
+                    dest.writeInt(result.burstDuration);
+                    dest.writeInt(result.negotiatedBurstNum);
+                    dest.writeByte(result.LCI.id);
+                    if (result.LCI.id != (byte) 0xFF) {
+                        dest.writeByte((byte)result.LCI.data.length);
+                        dest.writeByteArray(result.LCI.data);
+                    }
+                    dest.writeByte(result.LCR.id);
+                    if (result.LCR.id != (byte) 0xFF) {
+                        dest.writeByte((byte) result.LCR.data.length);
+                        dest.writeByteArray(result.LCR.data);
+                    }
+                    dest.writeByte(result.secure ? (byte) 1 : 0);
+                }
+            } else {
+                dest.writeInt(0);
+            }
+        }
+
+        /** Implement the Parcelable interface {@hide} */
+        public static final @android.annotation.NonNull Creator<ParcelableRttResults> CREATOR =
+                new Creator<ParcelableRttResults>() {
+                    @Override
+                    public ParcelableRttResults createFromParcel(Parcel in) {
+
+                        int num = in.readInt();
+
+                        if (num == 0) {
+                            return new ParcelableRttResults(null);
+                        }
+
+                        RttResult results[] = new RttResult[num];
+                        for (int i = 0; i < num; i++) {
+                            results[i] = new RttResult();
+                            results[i].bssid = in.readString();
+                            results[i].burstNumber = in.readInt();
+                            results[i].measurementFrameNumber = in.readInt();
+                            results[i].successMeasurementFrameNumber = in.readInt();
+                            results[i].frameNumberPerBurstPeer = in.readInt();
+                            results[i].status = in.readInt();
+                            results[i].measurementType = in.readInt();
+                            results[i].retryAfterDuration = in.readInt();
+                            results[i].ts = in.readLong();
+                            results[i].rssi = in.readInt();
+                            results[i].rssiSpread = in.readInt();
+                            results[i].txRate = in.readInt();
+                            results[i].rtt = in.readLong();
+                            results[i].rttStandardDeviation = in.readLong();
+                            results[i].rttSpread = in.readLong();
+                            results[i].distance = in.readInt();
+                            results[i].distanceStandardDeviation = in.readInt();
+                            results[i].distanceSpread = in.readInt();
+                            results[i].burstDuration = in.readInt();
+                            results[i].negotiatedBurstNum = in.readInt();
+                            results[i].LCI = new WifiInformationElement();
+                            results[i].LCI.id = in.readByte();
+                            if (results[i].LCI.id != (byte) 0xFF) {
+                                byte length = in.readByte();
+                                results[i].LCI.data = new byte[length];
+                                in.readByteArray(results[i].LCI.data);
+                            }
+                            results[i].LCR = new WifiInformationElement();
+                            results[i].LCR.id = in.readByte();
+                            if (results[i].LCR.id != (byte) 0xFF) {
+                                byte length = in.readByte();
+                                results[i].LCR.data = new byte[length];
+                                in.readByteArray(results[i].LCR.data);
+                            }
+                            results[i].secure = (in.readByte() != 0);
+                        }
+
+                        ParcelableRttResults parcelableResults = new ParcelableRttResults(results);
+                        return parcelableResults;
+                    }
+
+                    @Override
+                    public ParcelableRttResults[] newArray(int size) {
+                        return new ParcelableRttResults[size];
+                    }
+                };
+    }
+
+    @Deprecated
+    public static interface RttListener {
+        public void onSuccess(RttResult[] results);
+        public void onFailure(int reason, String description);
+        public void onAborted();
+    }
+
+    /**
+     * Request to start an RTT ranging
+     * <p>
+     * This method is deprecated. Please use the
+     * {@link WifiRttManager#startRanging(RangingRequest, java.util.concurrent.Executor, RangingResultCallback)}
+     * API.
+     *
+     * @param params  -- RTT request Parameters
+     * @param listener -- Call back to inform RTT result
+     * @exception throw IllegalArgumentException when params are illegal
+     *            throw IllegalStateException when RttCapabilities do not exist
+     */
+    @RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE)
+    public void startRanging(RttParams[] params, RttListener listener) {
+        Log.i(TAG, "Send RTT request to RTT Service");
+
+        if (!mNewService.isAvailable()) {
+            listener.onFailure(REASON_NOT_AVAILABLE, "");
+            return;
+        }
+
+        RangingRequest.Builder builder = new RangingRequest.Builder();
+        for (RttParams rttParams : params) {
+            if (rttParams.deviceType != RTT_PEER_TYPE_AP) {
+                listener.onFailure(REASON_INVALID_REQUEST, "Only AP peers are supported");
+                return;
+            }
+
+            ScanResult reconstructed = new ScanResult();
+            reconstructed.BSSID = rttParams.bssid;
+            if (rttParams.requestType == RTT_TYPE_TWO_SIDED) {
+                reconstructed.setFlag(ScanResult.FLAG_80211mc_RESPONDER);
+            }
+            reconstructed.channelWidth = rttParams.channelWidth;
+            reconstructed.frequency = rttParams.frequency;
+            reconstructed.centerFreq0 = rttParams.centerFreq0;
+            reconstructed.centerFreq1 = rttParams.centerFreq1;
+            builder.addResponder(
+                    android.net.wifi.rtt.ResponderConfig.fromScanResult(reconstructed));
+        }
+        try {
+            mNewService.startRanging(builder.build(),
+                    mContext.getMainExecutor(),
+                    new RangingResultCallback() {
+                @Override
+                public void onRangingFailure(int code) {
+                    int localCode = REASON_UNSPECIFIED;
+                    if (code == STATUS_CODE_FAIL_RTT_NOT_AVAILABLE) {
+                        localCode = REASON_NOT_AVAILABLE;
+                    }
+                    listener.onFailure(localCode, "");
+                }
+
+                @Override
+                public void onRangingResults(List<RangingResult> results) {
+                    RttResult[] legacyResults = new RttResult[results.size()];
+                    int i = 0;
+                    for (RangingResult result : results) {
+                        legacyResults[i] = new RttResult();
+                        legacyResults[i].status = result.getStatus();
+                        legacyResults[i].bssid = result.getMacAddress().toString();
+                        if (result.getStatus() == RangingResult.STATUS_SUCCESS) {
+                            legacyResults[i].distance = result.getDistanceMm() / 10;
+                            legacyResults[i].distanceStandardDeviation =
+                                    result.getDistanceStdDevMm() / 10;
+                            legacyResults[i].rssi = result.getRssi() * -2;
+                            legacyResults[i].ts = result.getRangingTimestampMillis() * 1000;
+                            legacyResults[i].measurementFrameNumber =
+                                    result.getNumAttemptedMeasurements();
+                            legacyResults[i].successMeasurementFrameNumber =
+                                    result.getNumSuccessfulMeasurements();
+                        } else {
+                            // just in case legacy API needed some relatively real timestamp
+                            legacyResults[i].ts = SystemClock.elapsedRealtime() * 1000;
+                        }
+                        i++;
+                    }
+                    listener.onSuccess(legacyResults);
+                }
+            });
+        } catch (IllegalArgumentException e) {
+            Log.e(TAG, "startRanging: invalid arguments - " + e);
+            listener.onFailure(REASON_INVALID_REQUEST, e.getMessage());
+        } catch (SecurityException e) {
+            Log.e(TAG, "startRanging: security exception - " + e);
+            listener.onFailure(REASON_PERMISSION_DENIED, e.getMessage());
+        }
+    }
+
+    /**
+     * This method is deprecated and performs no function. Please use the {@link WifiRttManager}
+     * API.
+     */
+    @RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE)
+    public void stopRanging(RttListener listener) {
+        Log.e(TAG, "stopRanging: unsupported operation - nop");
+    }
+
+    /**
+     * Callbacks for responder operations.
+     * <p>
+     * A {@link ResponderCallback} is the handle to the calling client. {@link RttManager} will keep
+     * a reference to the callback for the entire period when responder is enabled. The same
+     * callback as used in enabling responder needs to be passed for disabling responder.
+     * The client can freely destroy or reuse the callback after {@link RttManager#disableResponder}
+     * is called.
+     */
+    @Deprecated
+    public abstract static class ResponderCallback {
+        /** Callback when responder is enabled. */
+        public abstract void onResponderEnabled(ResponderConfig config);
+        /** Callback when enabling responder failed. */
+        public abstract void onResponderEnableFailure(int reason);
+        // TODO: consider adding onResponderAborted once it's supported.
+    }
+
+    /**
+     * Enable Wi-Fi RTT responder mode on the device. The enabling result will be delivered via
+     * {@code callback}.
+     * <p>
+     * Note calling this method with the same callback when the responder is already enabled won't
+     * change the responder state, a cached {@link ResponderConfig} from the last enabling will be
+     * returned through the callback.
+     * <p>
+     * This method is deprecated and will throw an {@link UnsupportedOperationException}
+     * exception. Please use the {@link WifiRttManager} API to perform a Wi-Fi Aware peer-to-peer
+     * ranging.
+     *
+     * @param callback Callback for responder enabling/disabling result.
+     * @throws IllegalArgumentException If {@code callback} is null.
+     */
+    @RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE)
+    public void enableResponder(ResponderCallback callback) {
+        throw new UnsupportedOperationException(
+                "enableResponder is not supported in the adaptation layer");
+    }
+
+    /**
+     * Disable Wi-Fi RTT responder mode on the device. The {@code callback} needs to be the
+     * same one used in {@link #enableResponder(ResponderCallback)}.
+     * <p>
+     * Calling this method when responder isn't enabled won't have any effect. The callback can be
+     * reused for enabling responder after this method is called.
+     * <p>
+     * This method is deprecated and will throw an {@link UnsupportedOperationException}
+     * exception. Please use the {@link WifiRttManager} API to perform a Wi-Fi Aware peer-to-peer
+     * ranging.
+     *
+     * @param callback The same callback used for enabling responder.
+     * @throws IllegalArgumentException If {@code callback} is null.
+     */
+    @RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE)
+    public void disableResponder(ResponderCallback callback) {
+        throw new UnsupportedOperationException(
+                "disableResponder is not supported in the adaptation layer");
+    }
+
+    /**
+     * Configuration used for RTT responder mode. The configuration information can be used by a
+     * peer device to range the responder.
+     *
+     * @see ScanResult
+     */
+    @Deprecated
+    public static class ResponderConfig implements Parcelable {
+
+        // TODO: make all fields final once we can get mac address from responder HAL APIs.
+        /**
+         * Wi-Fi mac address used for responder mode.
+         */
+        public String macAddress = "";
+
+        /**
+         * The primary 20 MHz frequency (in MHz) of the channel where responder is enabled.
+         * @see ScanResult#frequency
+         */
+        public int frequency;
+
+        /**
+         * Center frequency of the channel where responder is enabled on. Only in use when channel
+         * width is at least 40MHz.
+         * @see ScanResult#centerFreq0
+         */
+        public int centerFreq0;
+
+        /**
+         * Center frequency of the second segment when channel width is 80 + 80 MHz.
+         * @see ScanResult#centerFreq1
+         */
+        public int centerFreq1;
+
+        /**
+         * Width of the channel where responder is enabled on.
+         * @see ScanResult#channelWidth
+         */
+        public int channelWidth;
+
+        /**
+         * Preamble supported by responder.
+         */
+        public int preamble;
+
+        @Override
+        public String toString() {
+            StringBuilder builder = new StringBuilder();
+            builder.append("macAddress = ").append(macAddress)
+                    .append(" frequency = ").append(frequency)
+                    .append(" centerFreq0 = ").append(centerFreq0)
+                    .append(" centerFreq1 = ").append(centerFreq1)
+                    .append(" channelWidth = ").append(channelWidth)
+                    .append(" preamble = ").append(preamble);
+            return builder.toString();
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeString(macAddress);
+            dest.writeInt(frequency);
+            dest.writeInt(centerFreq0);
+            dest.writeInt(centerFreq1);
+            dest.writeInt(channelWidth);
+            dest.writeInt(preamble);
+        }
+
+        /** Implement {@link Parcelable} interface */
+        public static final @android.annotation.NonNull Parcelable.Creator<ResponderConfig> CREATOR =
+                new Parcelable.Creator<ResponderConfig>() {
+            @Override
+            public ResponderConfig createFromParcel(Parcel in) {
+                ResponderConfig config = new ResponderConfig();
+                config.macAddress = in.readString();
+                config.frequency = in.readInt();
+                config.centerFreq0 = in.readInt();
+                config.centerFreq1 = in.readInt();
+                config.channelWidth = in.readInt();
+                config.preamble = in.readInt();
+                return config;
+            }
+
+            @Override
+            public ResponderConfig[] newArray(int size) {
+                return new ResponderConfig[size];
+            }
+        };
+
+    }
+
+    /* private methods */
+    public static final int BASE = Protocol.BASE_WIFI_RTT_MANAGER;
+
+    public static final int CMD_OP_START_RANGING        = BASE + 0;
+    public static final int CMD_OP_STOP_RANGING         = BASE + 1;
+    public static final int CMD_OP_FAILED               = BASE + 2;
+    public static final int CMD_OP_SUCCEEDED            = BASE + 3;
+    public static final int CMD_OP_ABORTED              = BASE + 4;
+    public static final int CMD_OP_ENABLE_RESPONDER     = BASE + 5;
+    public static final int CMD_OP_DISABLE_RESPONDER    = BASE + 6;
+    public static final int
+            CMD_OP_ENALBE_RESPONDER_SUCCEEDED           = BASE + 7;
+    public static final int
+            CMD_OP_ENALBE_RESPONDER_FAILED              = BASE + 8;
+    /** @hide */
+    public static final int CMD_OP_REG_BINDER           = BASE + 9;
+
+    private final WifiRttManager mNewService;
+    private final Context mContext;
+    private RttCapabilities mRttCapabilities;
+
+    /**
+     * Create a new WifiScanner instance.
+     * Applications will almost always want to use
+     * {@link android.content.Context#getSystemService Context.getSystemService()} to retrieve
+     * the standard {@link android.content.Context#WIFI_RTT_SERVICE Context.WIFI_RTT_SERVICE}.
+     * @param service the new WifiRttManager service
+     *
+     * @hide
+     */
+    public RttManager(Context context, WifiRttManager service) {
+        mNewService = service;
+        mContext = context;
+
+        boolean rttSupported = context.getPackageManager().hasSystemFeature(
+                PackageManager.FEATURE_WIFI_RTT);
+
+        mRttCapabilities = new RttCapabilities();
+        mRttCapabilities.oneSidedRttSupported = rttSupported;
+        mRttCapabilities.twoSided11McRttSupported = rttSupported;
+        mRttCapabilities.lciSupported = false;
+        mRttCapabilities.lcrSupported = false;
+        mRttCapabilities.preambleSupported = PREAMBLE_HT | PREAMBLE_VHT;
+        mRttCapabilities.bwSupported = RTT_BW_40_SUPPORT | RTT_BW_80_SUPPORT;
+        mRttCapabilities.responderSupported = false;
+        mRttCapabilities.secureRttSupported = false;
+    }
+}
+
diff --git a/android/net/wifi/ScanResult.java b/android/net/wifi/ScanResult.java
new file mode 100644
index 0000000..c0c0361
--- /dev/null
+++ b/android/net/wifi/ScanResult.java
@@ -0,0 +1,847 @@
+/*
+ * Copyright (C) 2008 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.wifi;
+
+import android.annotation.SystemApi;
+import android.annotation.UnsupportedAppUsage;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Describes information about a detected access point. In addition
+ * to the attributes described here, the supplicant keeps track of
+ * {@code quality}, {@code noise}, and {@code maxbitrate} attributes,
+ * but does not currently report them to external clients.
+ */
+public class ScanResult implements Parcelable {
+    /**
+     * The network name.
+     */
+    public String SSID;
+
+    /**
+     * Ascii encoded SSID. This will replace SSID when we deprecate it. @hide
+     */
+    @UnsupportedAppUsage
+    public WifiSsid wifiSsid;
+
+    /**
+     * The address of the access point.
+     */
+    public String BSSID;
+
+    /**
+     * The HESSID from the beacon.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public long hessid;
+
+    /**
+     * The ANQP Domain ID from the Hotspot 2.0 Indication element, if present.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public int anqpDomainId;
+
+    /*
+     * This field is equivalent to the |flags|, rather than the |capabilities| field
+     * of the per-BSS scan results returned by WPA supplicant. See the definition of
+     * |struct wpa_bss| in wpa_supplicant/bss.h for more details.
+     */
+    /**
+     * Describes the authentication, key management, and encryption schemes
+     * supported by the access point.
+     */
+    public String capabilities;
+
+    /**
+     * @hide
+     * No security protocol.
+     */
+    public static final int PROTOCOL_NONE = 0;
+    /**
+     * @hide
+     * Security protocol type: WPA version 1.
+     */
+    public static final int PROTOCOL_WPA = 1;
+    /**
+     * @hide
+     * Security protocol type: RSN, for WPA version 2, and version 3.
+     */
+    public static final int PROTOCOL_RSN = 2;
+    /**
+     * @hide
+     * Security protocol type:
+     * OSU Server-only authenticated layer 2 Encryption Network.
+     * Used for Hotspot 2.0.
+     */
+    public static final int PROTOCOL_OSEN = 3;
+
+    /**
+     * @hide
+     * No security key management scheme.
+     */
+    public static final int KEY_MGMT_NONE = 0;
+    /**
+     * @hide
+     * Security key management scheme: PSK.
+     */
+    public static final int KEY_MGMT_PSK = 1;
+    /**
+     * @hide
+     * Security key management scheme: EAP.
+     */
+    public static final int KEY_MGMT_EAP = 2;
+    /**
+     * @hide
+     * Security key management scheme: FT_PSK.
+     */
+    public static final int KEY_MGMT_FT_PSK = 3;
+    /**
+     * @hide
+     * Security key management scheme: FT_EAP.
+     */
+    public static final int KEY_MGMT_FT_EAP = 4;
+    /**
+     * @hide
+     * Security key management scheme: PSK_SHA256
+     */
+    public static final int KEY_MGMT_PSK_SHA256 = 5;
+    /**
+     * @hide
+     * Security key management scheme: EAP_SHA256.
+     */
+    public static final int KEY_MGMT_EAP_SHA256 = 6;
+    /**
+     * @hide
+     * Security key management scheme: OSEN.
+     * Used for Hotspot 2.0.
+     */
+    public static final int KEY_MGMT_OSEN = 7;
+     /**
+     * @hide
+     * Security key management scheme: SAE.
+     */
+    public static final int KEY_MGMT_SAE = 8;
+    /**
+     * @hide
+     * Security key management scheme: OWE.
+     */
+    public static final int KEY_MGMT_OWE = 9;
+    /**
+     * @hide
+     * Security key management scheme: SUITE_B_192.
+     */
+    public static final int KEY_MGMT_EAP_SUITE_B_192 = 10;
+    /**
+     * @hide
+     * Security key management scheme: FT_SAE.
+     */
+    public static final int KEY_MGMT_FT_SAE = 11;
+    /**
+     * @hide
+     * Security key management scheme: OWE in transition mode.
+     */
+    public static final int KEY_MGMT_OWE_TRANSITION = 12;
+    /**
+     * @hide
+     * No cipher suite.
+     */
+    public static final int CIPHER_NONE = 0;
+    /**
+     * @hide
+     * No group addressed, only used for group data cipher.
+     */
+    public static final int CIPHER_NO_GROUP_ADDRESSED = 1;
+    /**
+     * @hide
+     * Cipher suite: TKIP
+     */
+    public static final int CIPHER_TKIP = 2;
+    /**
+     * @hide
+     * Cipher suite: CCMP
+     */
+    public static final int CIPHER_CCMP = 3;
+    /**
+     * @hide
+     * Cipher suite: GCMP
+     */
+    public static final int CIPHER_GCMP_256 = 4;
+
+    /**
+     * The detected signal level in dBm, also known as the RSSI.
+     *
+     * <p>Use {@link android.net.wifi.WifiManager#calculateSignalLevel} to convert this number into
+     * an absolute signal level which can be displayed to a user.
+     */
+    public int level;
+    /**
+     * The primary 20 MHz frequency (in MHz) of the channel over which the client is communicating
+     * with the access point.
+     */
+    public int frequency;
+
+   /**
+    * AP Channel bandwidth is 20 MHZ
+    */
+    public static final int CHANNEL_WIDTH_20MHZ = 0;
+   /**
+    * AP Channel bandwidth is 40 MHZ
+    */
+    public static final int CHANNEL_WIDTH_40MHZ = 1;
+   /**
+    * AP Channel bandwidth is 80 MHZ
+    */
+    public static final int CHANNEL_WIDTH_80MHZ = 2;
+   /**
+    * AP Channel bandwidth is 160 MHZ
+    */
+    public static final int CHANNEL_WIDTH_160MHZ = 3;
+   /**
+    * AP Channel bandwidth is 160 MHZ, but 80MHZ + 80MHZ
+    */
+    public static final int CHANNEL_WIDTH_80MHZ_PLUS_MHZ = 4;
+
+   /**
+    * AP Channel bandwidth; one of {@link #CHANNEL_WIDTH_20MHZ}, {@link #CHANNEL_WIDTH_40MHZ},
+    * {@link #CHANNEL_WIDTH_80MHZ}, {@link #CHANNEL_WIDTH_160MHZ}
+    * or {@link #CHANNEL_WIDTH_80MHZ_PLUS_MHZ}.
+    */
+    public int channelWidth;
+
+    /**
+     * Not used if the AP bandwidth is 20 MHz
+     * If the AP use 40, 80 or 160 MHz, this is the center frequency (in MHz)
+     * if the AP use 80 + 80 MHz, this is the center frequency of the first segment (in MHz)
+     */
+    public int centerFreq0;
+
+    /**
+     * Only used if the AP bandwidth is 80 + 80 MHz
+     * if the AP use 80 + 80 MHz, this is the center frequency of the second segment (in MHz)
+     */
+    public int centerFreq1;
+
+    /**
+     * @deprecated use is80211mcResponder() instead
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public boolean is80211McRTTResponder;
+
+    /**
+     * timestamp in microseconds (since boot) when
+     * this result was last seen.
+     */
+    public long timestamp;
+
+    /**
+     * Timestamp representing date when this result was last seen, in milliseconds from 1970
+     * {@hide}
+     */
+    @UnsupportedAppUsage
+    public long seen;
+
+    /**
+     * On devices with multiple hardware radio chains, this class provides metadata about
+     * each radio chain that was used to receive this scan result (probe response or beacon).
+     * {@hide}
+     */
+    public static class RadioChainInfo {
+        /** Vendor defined id for a radio chain. */
+        public int id;
+        /** Detected signal level in dBm (also known as the RSSI) on this radio chain. */
+        public int level;
+
+        @Override
+        public String toString() {
+            return "RadioChainInfo: id=" + id + ", level=" + level;
+        }
+
+        @Override
+        public boolean equals(Object otherObj) {
+            if (this == otherObj) {
+                return true;
+            }
+            if (!(otherObj instanceof RadioChainInfo)) {
+                return false;
+            }
+            RadioChainInfo other = (RadioChainInfo) otherObj;
+            return id == other.id && level == other.level;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(id, level);
+        }
+    };
+
+    /**
+     * Information about the list of the radio chains used to receive this scan result
+     * (probe response or beacon).
+     *
+     * For Example: On devices with 2 hardware radio chains, this list could hold 1 or 2
+     * entries based on whether this scan result was received using one or both the chains.
+     * {@hide}
+     */
+    public RadioChainInfo[] radioChainInfos;
+
+    /**
+     * Status indicating the scan result does not correspond to a user's saved configuration
+     * @hide
+     * @removed
+     */
+    @SystemApi
+    public boolean untrusted;
+
+    /**
+     * Number of time autojoin used it
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public int numUsage;
+
+    /**
+     * The approximate distance to the AP in centimeter, if available.  Else
+     * {@link UNSPECIFIED}.
+     * {@hide}
+     */
+    @UnsupportedAppUsage
+    public int distanceCm;
+
+    /**
+     * The standard deviation of the distance to the access point, if available.
+     * Else {@link UNSPECIFIED}.
+     * {@hide}
+     */
+    @UnsupportedAppUsage
+    public int distanceSdCm;
+
+    /** {@hide} */
+    public static final long FLAG_PASSPOINT_NETWORK               = 0x0000000000000001;
+
+    /** {@hide} */
+    public static final long FLAG_80211mc_RESPONDER               = 0x0000000000000002;
+
+    /*
+     * These flags are specific to the ScanResult class, and are not related to the |flags|
+     * field of the per-BSS scan results from WPA supplicant.
+     */
+    /**
+     * Defines flags; such as {@link #FLAG_PASSPOINT_NETWORK}.
+     * {@hide}
+     */
+    @UnsupportedAppUsage
+    public long flags;
+
+    /**
+     * sets a flag in {@link #flags} field
+     * @param flag flag to set
+     * @hide
+     */
+    public void setFlag(long flag) {
+        flags |= flag;
+    }
+
+    /**
+     * clears a flag in {@link #flags} field
+     * @param flag flag to set
+     * @hide
+     */
+    public void clearFlag(long flag) {
+        flags &= ~flag;
+    }
+
+    public boolean is80211mcResponder() {
+        return (flags & FLAG_80211mc_RESPONDER) != 0;
+    }
+
+    public boolean isPasspointNetwork() {
+        return (flags & FLAG_PASSPOINT_NETWORK) != 0;
+    }
+
+    /**
+     * Indicates venue name (such as 'San Francisco Airport') published by access point; only
+     * available on Passpoint network and if published by access point.
+     */
+    public CharSequence venueName;
+
+    /**
+     * Indicates Passpoint operator name published by access point.
+     */
+    public CharSequence operatorFriendlyName;
+
+    /**
+     * {@hide}
+     */
+    public final static int UNSPECIFIED = -1;
+    /**
+     * @hide
+     */
+    public boolean is24GHz() {
+        return ScanResult.is24GHz(frequency);
+    }
+
+    /**
+     * @hide
+     * TODO: makes real freq boundaries
+     */
+    public static boolean is24GHz(int freq) {
+        return freq > 2400 && freq < 2500;
+    }
+
+    /**
+     * @hide
+     */
+    public boolean is5GHz() {
+        return ScanResult.is5GHz(frequency);
+    }
+
+    /**
+     * @hide
+     * TODO: makes real freq boundaries
+     */
+    public static boolean is5GHz(int freq) {
+        return freq > 4900 && freq < 5900;
+    }
+
+    /**
+     *  @hide
+     * anqp lines from supplicant BSS response
+     */
+    @UnsupportedAppUsage
+    public List<String> anqpLines;
+
+    /** information elements from beacon
+     * @hide
+     */
+    public static class InformationElement {
+        @UnsupportedAppUsage
+        public static final int EID_SSID = 0;
+        @UnsupportedAppUsage
+        public static final int EID_SUPPORTED_RATES = 1;
+        @UnsupportedAppUsage
+        public static final int EID_TIM = 5;
+        @UnsupportedAppUsage
+        public static final int EID_BSS_LOAD = 11;
+        @UnsupportedAppUsage
+        public static final int EID_ERP = 42;
+        public static final int EID_HT_CAPABILITIES = 45;
+        @UnsupportedAppUsage
+        public static final int EID_RSN = 48;
+        @UnsupportedAppUsage
+        public static final int EID_EXTENDED_SUPPORTED_RATES = 50;
+        @UnsupportedAppUsage
+        public static final int EID_HT_OPERATION = 61;
+        @UnsupportedAppUsage
+        public static final int EID_INTERWORKING = 107;
+        @UnsupportedAppUsage
+        public static final int EID_ROAMING_CONSORTIUM = 111;
+        @UnsupportedAppUsage
+        public static final int EID_EXTENDED_CAPS = 127;
+        public static final int EID_VHT_CAPABILITIES = 191;
+        @UnsupportedAppUsage
+        public static final int EID_VHT_OPERATION = 192;
+        @UnsupportedAppUsage
+        public static final int EID_VSA = 221;
+
+        @UnsupportedAppUsage
+        public int id;
+        @UnsupportedAppUsage
+        public byte[] bytes;
+
+        public InformationElement() {
+        }
+
+        public InformationElement(InformationElement rhs) {
+            this.id = rhs.id;
+            this.bytes = rhs.bytes.clone();
+        }
+    }
+
+    /** information elements found in the beacon
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public InformationElement[] informationElements;
+
+    /** ANQP response elements.
+     * @hide
+     */
+    public AnqpInformationElement[] anqpElements;
+
+    /**
+     * Flag indicating if this AP is a carrier AP. The determination is based
+     * on the AP's SSID and if AP is using EAP security.
+     *
+     * @hide
+     */
+    public boolean isCarrierAp;
+
+    /**
+     * The EAP type {@link WifiEnterpriseConfig.Eap} associated with this AP if it is a carrier AP.
+     *
+     * @hide
+     */
+    public int carrierApEapType;
+
+    /**
+     * The name of the carrier that's associated with this AP if it is a carrier AP.
+     *
+     * @hide
+     */
+    public String carrierName;
+
+    /** {@hide} */
+    public ScanResult(WifiSsid wifiSsid, String BSSID, long hessid, int anqpDomainId,
+            byte[] osuProviders, String caps, int level, int frequency, long tsf) {
+        this.wifiSsid = wifiSsid;
+        this.SSID = (wifiSsid != null) ? wifiSsid.toString() : WifiSsid.NONE;
+        this.BSSID = BSSID;
+        this.hessid = hessid;
+        this.anqpDomainId = anqpDomainId;
+        if (osuProviders != null) {
+            this.anqpElements = new AnqpInformationElement[1];
+            this.anqpElements[0] =
+                    new AnqpInformationElement(AnqpInformationElement.HOTSPOT20_VENDOR_ID,
+                            AnqpInformationElement.HS_OSU_PROVIDERS, osuProviders);
+        }
+        this.capabilities = caps;
+        this.level = level;
+        this.frequency = frequency;
+        this.timestamp = tsf;
+        this.distanceCm = UNSPECIFIED;
+        this.distanceSdCm = UNSPECIFIED;
+        this.channelWidth = UNSPECIFIED;
+        this.centerFreq0 = UNSPECIFIED;
+        this.centerFreq1 = UNSPECIFIED;
+        this.flags = 0;
+        this.isCarrierAp = false;
+        this.carrierApEapType = UNSPECIFIED;
+        this.carrierName = null;
+        this.radioChainInfos = null;
+    }
+
+    /** {@hide} */
+    public ScanResult(WifiSsid wifiSsid, String BSSID, String caps, int level, int frequency,
+            long tsf, int distCm, int distSdCm) {
+        this.wifiSsid = wifiSsid;
+        this.SSID = (wifiSsid != null) ? wifiSsid.toString() : WifiSsid.NONE;
+        this.BSSID = BSSID;
+        this.capabilities = caps;
+        this.level = level;
+        this.frequency = frequency;
+        this.timestamp = tsf;
+        this.distanceCm = distCm;
+        this.distanceSdCm = distSdCm;
+        this.channelWidth = UNSPECIFIED;
+        this.centerFreq0 = UNSPECIFIED;
+        this.centerFreq1 = UNSPECIFIED;
+        this.flags = 0;
+        this.isCarrierAp = false;
+        this.carrierApEapType = UNSPECIFIED;
+        this.carrierName = null;
+        this.radioChainInfos = null;
+    }
+
+    /** {@hide} */
+    public ScanResult(String Ssid, String BSSID, long hessid, int anqpDomainId, String caps,
+            int level, int frequency,
+            long tsf, int distCm, int distSdCm, int channelWidth, int centerFreq0, int centerFreq1,
+            boolean is80211McRTTResponder) {
+        this.SSID = Ssid;
+        this.BSSID = BSSID;
+        this.hessid = hessid;
+        this.anqpDomainId = anqpDomainId;
+        this.capabilities = caps;
+        this.level = level;
+        this.frequency = frequency;
+        this.timestamp = tsf;
+        this.distanceCm = distCm;
+        this.distanceSdCm = distSdCm;
+        this.channelWidth = channelWidth;
+        this.centerFreq0 = centerFreq0;
+        this.centerFreq1 = centerFreq1;
+        if (is80211McRTTResponder) {
+            this.flags = FLAG_80211mc_RESPONDER;
+        } else {
+            this.flags = 0;
+        }
+        this.isCarrierAp = false;
+        this.carrierApEapType = UNSPECIFIED;
+        this.carrierName = null;
+        this.radioChainInfos = null;
+    }
+
+    /** {@hide} */
+    public ScanResult(WifiSsid wifiSsid, String Ssid, String BSSID, long hessid, int anqpDomainId,
+                  String caps, int level,
+                  int frequency, long tsf, int distCm, int distSdCm, int channelWidth,
+                  int centerFreq0, int centerFreq1, boolean is80211McRTTResponder) {
+        this(Ssid, BSSID, hessid, anqpDomainId, caps, level, frequency, tsf, distCm,
+                distSdCm, channelWidth, centerFreq0, centerFreq1, is80211McRTTResponder);
+        this.wifiSsid = wifiSsid;
+    }
+
+    /** copy constructor {@hide} */
+    public ScanResult(ScanResult source) {
+        if (source != null) {
+            wifiSsid = source.wifiSsid;
+            SSID = source.SSID;
+            BSSID = source.BSSID;
+            hessid = source.hessid;
+            anqpDomainId = source.anqpDomainId;
+            informationElements = source.informationElements;
+            anqpElements = source.anqpElements;
+            capabilities = source.capabilities;
+            level = source.level;
+            frequency = source.frequency;
+            channelWidth = source.channelWidth;
+            centerFreq0 = source.centerFreq0;
+            centerFreq1 = source.centerFreq1;
+            timestamp = source.timestamp;
+            distanceCm = source.distanceCm;
+            distanceSdCm = source.distanceSdCm;
+            seen = source.seen;
+            untrusted = source.untrusted;
+            numUsage = source.numUsage;
+            venueName = source.venueName;
+            operatorFriendlyName = source.operatorFriendlyName;
+            flags = source.flags;
+            isCarrierAp = source.isCarrierAp;
+            carrierApEapType = source.carrierApEapType;
+            carrierName = source.carrierName;
+            radioChainInfos = source.radioChainInfos;
+        }
+    }
+
+    /** empty scan result
+     *
+     * {@hide}
+     * */
+    public ScanResult() {
+    }
+
+    @Override
+    public String toString() {
+        StringBuffer sb = new StringBuffer();
+        String none = "<none>";
+
+        sb.append("SSID: ").
+            append(wifiSsid == null ? WifiSsid.NONE : wifiSsid).
+            append(", BSSID: ").
+            append(BSSID == null ? none : BSSID).
+            append(", capabilities: ").
+            append(capabilities == null ? none : capabilities).
+            append(", level: ").
+            append(level).
+            append(", frequency: ").
+            append(frequency).
+            append(", timestamp: ").
+            append(timestamp);
+
+        sb.append(", distance: ").append((distanceCm != UNSPECIFIED ? distanceCm : "?")).
+                append("(cm)");
+        sb.append(", distanceSd: ").append((distanceSdCm != UNSPECIFIED ? distanceSdCm : "?")).
+                append("(cm)");
+
+        sb.append(", passpoint: ");
+        sb.append(((flags & FLAG_PASSPOINT_NETWORK) != 0) ? "yes" : "no");
+        sb.append(", ChannelBandwidth: ").append(channelWidth);
+        sb.append(", centerFreq0: ").append(centerFreq0);
+        sb.append(", centerFreq1: ").append(centerFreq1);
+        sb.append(", 80211mcResponder: ");
+        sb.append(((flags & FLAG_80211mc_RESPONDER) != 0) ? "is supported" : "is not supported");
+        sb.append(", Carrier AP: ").append(isCarrierAp ? "yes" : "no");
+        sb.append(", Carrier AP EAP Type: ").append(carrierApEapType);
+        sb.append(", Carrier name: ").append(carrierName);
+        sb.append(", Radio Chain Infos: ").append(Arrays.toString(radioChainInfos));
+        return sb.toString();
+    }
+
+    /** Implement the Parcelable interface {@hide} */
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Implement the Parcelable interface {@hide} */
+    public void writeToParcel(Parcel dest, int flags) {
+        if (wifiSsid != null) {
+            dest.writeInt(1);
+            wifiSsid.writeToParcel(dest, flags);
+        } else {
+            dest.writeInt(0);
+        }
+        dest.writeString(SSID);
+        dest.writeString(BSSID);
+        dest.writeLong(hessid);
+        dest.writeInt(anqpDomainId);
+        dest.writeString(capabilities);
+        dest.writeInt(level);
+        dest.writeInt(frequency);
+        dest.writeLong(timestamp);
+        dest.writeInt(distanceCm);
+        dest.writeInt(distanceSdCm);
+        dest.writeInt(channelWidth);
+        dest.writeInt(centerFreq0);
+        dest.writeInt(centerFreq1);
+        dest.writeLong(seen);
+        dest.writeInt(untrusted ? 1 : 0);
+        dest.writeInt(numUsage);
+        dest.writeString((venueName != null) ? venueName.toString() : "");
+        dest.writeString((operatorFriendlyName != null) ? operatorFriendlyName.toString() : "");
+        dest.writeLong(this.flags);
+
+        if (informationElements != null) {
+            dest.writeInt(informationElements.length);
+            for (int i = 0; i < informationElements.length; i++) {
+                dest.writeInt(informationElements[i].id);
+                dest.writeInt(informationElements[i].bytes.length);
+                dest.writeByteArray(informationElements[i].bytes);
+            }
+        } else {
+            dest.writeInt(0);
+        }
+
+        if (anqpLines != null) {
+            dest.writeInt(anqpLines.size());
+            for (int i = 0; i < anqpLines.size(); i++) {
+                dest.writeString(anqpLines.get(i));
+            }
+        }
+        else {
+            dest.writeInt(0);
+        }
+        if (anqpElements != null) {
+            dest.writeInt(anqpElements.length);
+            for (AnqpInformationElement element : anqpElements) {
+                dest.writeInt(element.getVendorId());
+                dest.writeInt(element.getElementId());
+                dest.writeInt(element.getPayload().length);
+                dest.writeByteArray(element.getPayload());
+            }
+        } else {
+            dest.writeInt(0);
+        }
+        dest.writeInt(isCarrierAp ? 1 : 0);
+        dest.writeInt(carrierApEapType);
+        dest.writeString(carrierName);
+
+        if (radioChainInfos != null) {
+            dest.writeInt(radioChainInfos.length);
+            for (int i = 0; i < radioChainInfos.length; i++) {
+                dest.writeInt(radioChainInfos[i].id);
+                dest.writeInt(radioChainInfos[i].level);
+            }
+        } else {
+            dest.writeInt(0);
+        }
+    }
+
+    /** Implement the Parcelable interface {@hide} */
+    @UnsupportedAppUsage
+    public static final @android.annotation.NonNull Creator<ScanResult> CREATOR =
+        new Creator<ScanResult>() {
+            public ScanResult createFromParcel(Parcel in) {
+                WifiSsid wifiSsid = null;
+                if (in.readInt() == 1) {
+                    wifiSsid = WifiSsid.CREATOR.createFromParcel(in);
+                }
+                ScanResult sr = new ScanResult(
+                        wifiSsid,
+                        in.readString(),                    /* SSID  */
+                        in.readString(),                    /* BSSID */
+                        in.readLong(),                      /* HESSID */
+                        in.readInt(),                       /* ANQP Domain ID */
+                        in.readString(),                    /* capabilities */
+                        in.readInt(),                       /* level */
+                        in.readInt(),                       /* frequency */
+                        in.readLong(),                      /* timestamp */
+                        in.readInt(),                       /* distanceCm */
+                        in.readInt(),                       /* distanceSdCm */
+                        in.readInt(),                       /* channelWidth */
+                        in.readInt(),                       /* centerFreq0 */
+                        in.readInt(),                       /* centerFreq1 */
+                        false                               /* rtt responder,
+                                                               fixed with flags below */
+                );
+
+                sr.seen = in.readLong();
+                sr.untrusted = in.readInt() != 0;
+                sr.numUsage = in.readInt();
+                sr.venueName = in.readString();
+                sr.operatorFriendlyName = in.readString();
+                sr.flags = in.readLong();
+                int n = in.readInt();
+                if (n != 0) {
+                    sr.informationElements = new InformationElement[n];
+                    for (int i = 0; i < n; i++) {
+                        sr.informationElements[i] = new InformationElement();
+                        sr.informationElements[i].id = in.readInt();
+                        int len = in.readInt();
+                        sr.informationElements[i].bytes = new byte[len];
+                        in.readByteArray(sr.informationElements[i].bytes);
+                    }
+                }
+
+                n = in.readInt();
+                if (n != 0) {
+                    sr.anqpLines = new ArrayList<String>();
+                    for (int i = 0; i < n; i++) {
+                        sr.anqpLines.add(in.readString());
+                    }
+                }
+                n = in.readInt();
+                if (n != 0) {
+                    sr.anqpElements = new AnqpInformationElement[n];
+                    for (int i = 0; i < n; i++) {
+                        int vendorId = in.readInt();
+                        int elementId = in.readInt();
+                        int len = in.readInt();
+                        byte[] payload = new byte[len];
+                        in.readByteArray(payload);
+                        sr.anqpElements[i] =
+                                new AnqpInformationElement(vendorId, elementId, payload);
+                    }
+                }
+                sr.isCarrierAp = in.readInt() != 0;
+                sr.carrierApEapType = in.readInt();
+                sr.carrierName = in.readString();
+                n = in.readInt();
+                if (n != 0) {
+                    sr.radioChainInfos = new RadioChainInfo[n];
+                    for (int i = 0; i < n; i++) {
+                        sr.radioChainInfos[i] = new RadioChainInfo();
+                        sr.radioChainInfos[i].id = in.readInt();
+                        sr.radioChainInfos[i].level = in.readInt();
+                    }
+                }
+                return sr;
+            }
+
+            public ScanResult[] newArray(int size) {
+                return new ScanResult[size];
+            }
+        };
+}
diff --git a/android/net/wifi/SupplicantState.java b/android/net/wifi/SupplicantState.java
new file mode 100644
index 0000000..de7e2b5
--- /dev/null
+++ b/android/net/wifi/SupplicantState.java
@@ -0,0 +1,265 @@
+/*
+ * Copyright (C) 2008 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.wifi;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * From <code>defs.h</code> in <code>wpa_supplicant</code>.
+ * <p/>
+ * These enumeration values are used to indicate the current wpa_supplicant
+ * state. This is more fine-grained than most users will be interested in.
+ * In general, it is better to use
+ * {@link android.net.NetworkInfo.State NetworkInfo.State}.
+ * <p/>
+ * Note, the order of these enum constants must match the numerical values of the
+ * state constants in <code>defs.h</code> in <code>wpa_supplicant</code>.
+ */
+public enum SupplicantState implements Parcelable {
+    /**
+     * This state indicates that client is not associated, but is likely to
+     * start looking for an access point. This state is entered when a
+     * connection is lost.
+     */
+    DISCONNECTED,
+
+    /**
+     * Interface is disabled
+     * <p/>
+     * This state is entered if the network interface is disabled.
+     * wpa_supplicant refuses any new operations that would
+     * use the radio until the interface has been enabled.
+     */
+    INTERFACE_DISABLED,
+
+    /**
+     * Inactive state (wpa_supplicant disabled).
+     * <p/>
+     * This state is entered if there are no enabled networks in the
+     * configuration. wpa_supplicant is not trying to associate with a new
+     * network and external interaction (e.g., ctrl_iface call to add or
+     * enable a network) is needed to start association.
+     */
+    INACTIVE,
+
+    /**
+     * Scanning for a network.
+     * <p/>
+     * This state is entered when wpa_supplicant starts scanning for a
+     * network.
+     */
+    SCANNING,
+
+    /**
+     * Trying to authenticate with a BSS/SSID
+     * <p/>
+     * This state is entered when wpa_supplicant has found a suitable BSS
+     * to authenticate with and the driver is configured to try to
+     * authenticate with this BSS.
+     */
+    AUTHENTICATING,
+
+    /**
+     * Trying to associate with a BSS/SSID.
+     * <p/>
+     * This state is entered when wpa_supplicant has found a suitable BSS
+     * to associate with and the driver is configured to try to associate
+     * with this BSS in ap_scan=1 mode. When using ap_scan=2 mode, this
+     * state is entered when the driver is configured to try to associate
+     * with a network using the configured SSID and security policy.
+     */
+    ASSOCIATING,
+
+    /**
+     * Association completed.
+     * <p/>
+     * This state is entered when the driver reports that association has
+     * been successfully completed with an AP. If IEEE 802.1X is used
+     * (with or without WPA/WPA2), wpa_supplicant remains in this state
+     * until the IEEE 802.1X/EAPOL authentication has been completed.
+     */
+    ASSOCIATED,
+
+    /**
+     * WPA 4-Way Key Handshake in progress.
+     * <p/>
+     * This state is entered when WPA/WPA2 4-Way Handshake is started. In
+     * case of WPA-PSK, this happens when receiving the first EAPOL-Key
+     * frame after association. In case of WPA-EAP, this state is entered
+     * when the IEEE 802.1X/EAPOL authentication has been completed.
+     */
+    FOUR_WAY_HANDSHAKE,
+
+    /**
+     * WPA Group Key Handshake in progress.
+     * <p/>
+     * This state is entered when 4-Way Key Handshake has been completed
+     * (i.e., when the supplicant sends out message 4/4) and when Group
+     * Key rekeying is started by the AP (i.e., when supplicant receives
+     * message 1/2).
+     */
+    GROUP_HANDSHAKE,
+
+    /**
+     * All authentication completed.
+     * <p/>
+     * This state is entered when the full authentication process is
+     * completed. In case of WPA2, this happens when the 4-Way Handshake is
+     * successfully completed. With WPA, this state is entered after the
+     * Group Key Handshake; with IEEE 802.1X (non-WPA) connection is
+     * completed after dynamic keys are received (or if not used, after
+     * the EAP authentication has been completed). With static WEP keys and
+     * plaintext connections, this state is entered when an association
+     * has been completed.
+     * <p/>
+     * This state indicates that the supplicant has completed its
+     * processing for the association phase and that data connection is
+     * fully configured. Note, however, that there may not be any IP
+     * address associated with the connection yet. Typically, a DHCP
+     * request needs to be sent at this point to obtain an address.
+     */
+    COMPLETED,
+
+    /**
+     * An Android-added state that is reported when a client issues an
+     * explicit DISCONNECT command. In such a case, the supplicant is
+     * not only dissociated from the current access point (as for the
+     * DISCONNECTED state above), but it also does not attempt to connect
+     * to any access point until a RECONNECT or REASSOCIATE command
+     * is issued by the client.
+     */
+    DORMANT,
+
+    /**
+     * No connection to wpa_supplicant.
+     * <p/>
+     * This is an additional pseudo-state to handle the case where
+     * wpa_supplicant is not running and/or we have not been able
+     * to establish a connection to it.
+     */
+    UNINITIALIZED,
+
+    /**
+     * A pseudo-state that should normally never be seen.
+     */
+    INVALID;
+
+    /**
+     * Returns {@code true} if the supplicant state is valid and {@code false}
+     * otherwise.
+     * @param state The supplicant state
+     * @return {@code true} if the supplicant state is valid and {@code false}
+     * otherwise.
+     */
+    public static boolean isValidState(SupplicantState state) {
+        return state != UNINITIALIZED && state != INVALID;
+    }
+
+
+    /** Supplicant associating or authenticating is considered a handshake state {@hide} */
+    public static boolean isHandshakeState(SupplicantState state) {
+        switch(state) {
+            case AUTHENTICATING:
+            case ASSOCIATING:
+            case ASSOCIATED:
+            case FOUR_WAY_HANDSHAKE:
+            case GROUP_HANDSHAKE:
+                return true;
+            case COMPLETED:
+            case DISCONNECTED:
+            case INTERFACE_DISABLED:
+            case INACTIVE:
+            case SCANNING:
+            case DORMANT:
+            case UNINITIALIZED:
+            case INVALID:
+                return false;
+            default:
+                throw new IllegalArgumentException("Unknown supplicant state");
+        }
+    }
+
+    /** @hide */
+    public static boolean isConnecting(SupplicantState state) {
+        switch(state) {
+            case AUTHENTICATING:
+            case ASSOCIATING:
+            case ASSOCIATED:
+            case FOUR_WAY_HANDSHAKE:
+            case GROUP_HANDSHAKE:
+            case COMPLETED:
+                return true;
+            case DISCONNECTED:
+            case INTERFACE_DISABLED:
+            case INACTIVE:
+            case SCANNING:
+            case DORMANT:
+            case UNINITIALIZED:
+            case INVALID:
+                return false;
+            default:
+                throw new IllegalArgumentException("Unknown supplicant state");
+        }
+    }
+
+    /** @hide */
+    public static boolean isDriverActive(SupplicantState state) {
+        switch(state) {
+            case DISCONNECTED:
+            case DORMANT:
+            case INACTIVE:
+            case AUTHENTICATING:
+            case ASSOCIATING:
+            case ASSOCIATED:
+            case SCANNING:
+            case FOUR_WAY_HANDSHAKE:
+            case GROUP_HANDSHAKE:
+            case COMPLETED:
+                return true;
+            case INTERFACE_DISABLED:
+            case UNINITIALIZED:
+            case INVALID:
+                return false;
+            default:
+                throw new IllegalArgumentException("Unknown supplicant state");
+        }
+    }
+
+    /** Implement the Parcelable interface {@hide} */
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Implement the Parcelable interface {@hide} */
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeString(name());
+    }
+
+    /** Implement the Parcelable interface {@hide} */
+    public static final @android.annotation.NonNull Creator<SupplicantState> CREATOR =
+        new Creator<SupplicantState>() {
+            public SupplicantState createFromParcel(Parcel in) {
+                return SupplicantState.valueOf(in.readString());
+            }
+
+            public SupplicantState[] newArray(int size) {
+                return new SupplicantState[size];
+            }
+        };
+
+}
diff --git a/android/net/wifi/WifiActivityEnergyInfo.java b/android/net/wifi/WifiActivityEnergyInfo.java
new file mode 100644
index 0000000..0f7fc2d
--- /dev/null
+++ b/android/net/wifi/WifiActivityEnergyInfo.java
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Arrays;
+
+/**
+ * Record of energy and activity information from controller and
+ * underlying wifi stack state. Timestamp the record with elapsed
+ * real-time.
+ * @hide
+ */
+public final class WifiActivityEnergyInfo implements Parcelable {
+    /**
+     * @hide
+     */
+    public long mTimestamp;
+
+    /**
+     * @hide
+     */
+    public int mStackState;
+
+    /**
+     * @hide
+     */
+    public long mControllerTxTimeMs;
+
+    /**
+     * @hide
+     */
+    public long[] mControllerTxTimePerLevelMs;
+
+    /**
+     * @hide
+     */
+    public long mControllerRxTimeMs;
+
+    /**
+     * @hide
+     */
+    public long mControllerScanTimeMs;
+
+    /**
+     * @hide
+     */
+    public long mControllerIdleTimeMs;
+
+    /**
+     * @hide
+     */
+    public long mControllerEnergyUsed;
+
+    public static final int STACK_STATE_INVALID = 0;
+    public static final int STACK_STATE_STATE_ACTIVE = 1;
+    public static final int STACK_STATE_STATE_SCANNING = 2;
+    public static final int STACK_STATE_STATE_IDLE = 3;
+
+    public WifiActivityEnergyInfo(long timestamp, int stackState,
+                                  long txTime, long[] txTimePerLevel, long rxTime, long scanTime,
+                                  long idleTime, long energyUsed) {
+        mTimestamp = timestamp;
+        mStackState = stackState;
+        mControllerTxTimeMs = txTime;
+        mControllerTxTimePerLevelMs = txTimePerLevel;
+        mControllerRxTimeMs = rxTime;
+        mControllerScanTimeMs = scanTime;
+        mControllerIdleTimeMs = idleTime;
+        mControllerEnergyUsed = energyUsed;
+    }
+
+    @Override
+    public String toString() {
+        return "WifiActivityEnergyInfo{"
+            + " timestamp=" + mTimestamp
+            + " mStackState=" + mStackState
+            + " mControllerTxTimeMs=" + mControllerTxTimeMs
+            + " mControllerTxTimePerLevelMs=" + Arrays.toString(mControllerTxTimePerLevelMs)
+            + " mControllerRxTimeMs=" + mControllerRxTimeMs
+            + " mControllerScanTimeMs=" + mControllerScanTimeMs
+            + " mControllerIdleTimeMs=" + mControllerIdleTimeMs
+            + " mControllerEnergyUsed=" + mControllerEnergyUsed
+            + " }";
+    }
+
+    public static final @android.annotation.NonNull Parcelable.Creator<WifiActivityEnergyInfo> CREATOR =
+            new Parcelable.Creator<WifiActivityEnergyInfo>() {
+        public WifiActivityEnergyInfo createFromParcel(Parcel in) {
+            long timestamp = in.readLong();
+            int stackState = in.readInt();
+            long txTime = in.readLong();
+            long[] txTimePerLevel = in.createLongArray();
+            long rxTime = in.readLong();
+            long scanTime = in.readLong();
+            long idleTime = in.readLong();
+            long energyUsed = in.readLong();
+            return new WifiActivityEnergyInfo(timestamp, stackState,
+                    txTime, txTimePerLevel, rxTime, scanTime, idleTime, energyUsed);
+        }
+        public WifiActivityEnergyInfo[] newArray(int size) {
+            return new WifiActivityEnergyInfo[size];
+        }
+    };
+
+    public void writeToParcel(Parcel out, int flags) {
+        out.writeLong(mTimestamp);
+        out.writeInt(mStackState);
+        out.writeLong(mControllerTxTimeMs);
+        out.writeLongArray(mControllerTxTimePerLevelMs);
+        out.writeLong(mControllerRxTimeMs);
+        out.writeLong(mControllerScanTimeMs);
+        out.writeLong(mControllerIdleTimeMs);
+        out.writeLong(mControllerEnergyUsed);
+    }
+
+    public int describeContents() {
+        return 0;
+    }
+
+    /**
+     * @return bt stack reported state
+     */
+    public int getStackState() {
+        return mStackState;
+    }
+
+    /**
+     * @return tx time in ms
+     */
+    public long getControllerTxTimeMillis() {
+        return mControllerTxTimeMs;
+    }
+
+    /**
+     * @return tx time at power level provided in ms
+     */
+    public long getControllerTxTimeMillisAtLevel(int level) {
+        if (level < mControllerTxTimePerLevelMs.length) {
+            return mControllerTxTimePerLevelMs[level];
+        }
+        return 0;
+    }
+
+    /**
+     * @return rx time in ms
+     */
+    public long getControllerRxTimeMillis() {
+        return mControllerRxTimeMs;
+    }
+
+    /**
+     * @return scan time in ms
+     */
+    public long getControllerScanTimeMillis() {
+        return mControllerScanTimeMs;
+    }
+
+    /**
+     * @return idle time in ms
+     */
+    public long getControllerIdleTimeMillis() {
+        return mControllerIdleTimeMs;
+    }
+
+    /**
+     * product of current(mA), voltage(V) and time(ms)
+     * @return energy used
+     */
+    public long getControllerEnergyUsed() {
+        return mControllerEnergyUsed;
+    }
+    /**
+     * @return timestamp(wall clock) of record creation
+     */
+    public long getTimeStamp() {
+        return mTimestamp;
+    }
+
+    /**
+     * @return if the record is valid
+     */
+    public boolean isValid() {
+        return ((mControllerTxTimeMs >=0) &&
+                (mControllerRxTimeMs >=0) &&
+                (mControllerScanTimeMs >=0) &&
+                (mControllerIdleTimeMs >=0));
+    }
+}
\ No newline at end of file
diff --git a/android/net/wifi/WifiConfiguration.java b/android/net/wifi/WifiConfiguration.java
new file mode 100644
index 0000000..ed41642
--- /dev/null
+++ b/android/net/wifi/WifiConfiguration.java
@@ -0,0 +1,2628 @@
+/*
+ * Copyright (C) 2008 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.wifi;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.annotation.UnsupportedAppUsage;
+import android.content.pm.PackageManager;
+import android.net.IpConfiguration;
+import android.net.IpConfiguration.ProxySettings;
+import android.net.MacAddress;
+import android.net.NetworkSpecifier;
+import android.net.ProxyInfo;
+import android.net.StaticIpConfiguration;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.util.BackupUtils;
+import android.util.Log;
+import android.util.TimeUtils;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.BitSet;
+import java.util.HashMap;
+
+/**
+ * A class representing a configured Wi-Fi network, including the
+ * security configuration.
+ *
+ * @deprecated Use {@link WifiNetworkSpecifier.Builder} to create {@link NetworkSpecifier} and
+ * {@link WifiNetworkSuggestion.Builder} to create {@link WifiNetworkSuggestion}. This will become a
+ * system use only object in the future.
+ */
+@Deprecated
+public class WifiConfiguration implements Parcelable {
+    private static final String TAG = "WifiConfiguration";
+    /**
+     * Current Version of the Backup Serializer.
+    */
+    private static final int BACKUP_VERSION = 3;
+    /** {@hide} */
+    public static final String ssidVarName = "ssid";
+    /** {@hide} */
+    public static final String bssidVarName = "bssid";
+    /** {@hide} */
+    public static final String pskVarName = "psk";
+    /** {@hide} */
+    @Deprecated
+    @UnsupportedAppUsage
+    public static final String[] wepKeyVarNames = { "wep_key0", "wep_key1", "wep_key2", "wep_key3" };
+    /** {@hide} */
+    @Deprecated
+    public static final String wepTxKeyIdxVarName = "wep_tx_keyidx";
+    /** {@hide} */
+    public static final String priorityVarName = "priority";
+    /** {@hide} */
+    public static final String hiddenSSIDVarName = "scan_ssid";
+    /** {@hide} */
+    public static final String pmfVarName = "ieee80211w";
+    /** {@hide} */
+    public static final String updateIdentiferVarName = "update_identifier";
+    /** {@hide} */
+    public static final int INVALID_NETWORK_ID = -1;
+    /** {@hide} */
+    public static final int LOCAL_ONLY_NETWORK_ID = -2;
+
+    /** {@hide} */
+    private String mPasspointManagementObjectTree;
+    /** {@hide} */
+    private static final int MAXIMUM_RANDOM_MAC_GENERATION_RETRY = 3;
+
+    /**
+     * Recognized key management schemes.
+     */
+    public static class KeyMgmt {
+        private KeyMgmt() { }
+
+        /** WPA is not used; plaintext or static WEP could be used. */
+        public static final int NONE = 0;
+        /** WPA pre-shared key (requires {@code preSharedKey} to be specified). */
+        public static final int WPA_PSK = 1;
+        /** WPA using EAP authentication. Generally used with an external authentication server. */
+        public static final int WPA_EAP = 2;
+        /** IEEE 802.1X using EAP authentication and (optionally) dynamically
+         * generated WEP keys. */
+        public static final int IEEE8021X = 3;
+
+        /** WPA2 pre-shared key for use with soft access point
+          * (requires {@code preSharedKey} to be specified).
+          * @hide
+          */
+        @SystemApi
+        public static final int WPA2_PSK = 4;
+        /**
+         * Hotspot 2.0 r2 OSEN:
+         * @hide
+         */
+        public static final int OSEN = 5;
+
+        /**
+         * IEEE 802.11r Fast BSS Transition with PSK authentication.
+         * @hide
+         */
+        public static final int FT_PSK = 6;
+
+        /**
+         * IEEE 802.11r Fast BSS Transition with EAP authentication.
+         * @hide
+         */
+        public static final int FT_EAP = 7;
+
+        /**
+         * Simultaneous Authentication of Equals
+         */
+        public static final int SAE = 8;
+
+        /**
+         * Opportunististic Wireless Encryption
+         */
+        public static final int OWE = 9;
+
+        /**
+         * SUITE_B_192 192 bit level
+         */
+        public static final int SUITE_B_192 = 10;
+
+        /**
+         * WPA pre-shared key with stronger SHA256-based algorithms.
+         * @hide
+         */
+        public static final int WPA_PSK_SHA256 = 11;
+
+        /**
+         * WPA using EAP authentication with stronger SHA256-based algorithms.
+         * @hide
+         */
+        public static final int WPA_EAP_SHA256 = 12;
+
+        public static final String varName = "key_mgmt";
+
+        public static final String[] strings = { "NONE", "WPA_PSK", "WPA_EAP",
+                "IEEE8021X", "WPA2_PSK", "OSEN", "FT_PSK", "FT_EAP",
+                "SAE", "OWE", "SUITE_B_192", "WPA_PSK_SHA256", "WPA_EAP_SHA256" };
+    }
+
+    /**
+     * Recognized security protocols.
+     */
+    public static class Protocol {
+        private Protocol() { }
+
+        /** WPA/IEEE 802.11i/D3.0
+         * @deprecated Due to security and performance limitations, use of WPA-1 networks
+         * is discouraged. WPA-2 (RSN) should be used instead. */
+        @Deprecated
+        public static final int WPA = 0;
+        /** RSN WPA2/WPA3/IEEE 802.11i */
+        public static final int RSN = 1;
+        /** HS2.0 r2 OSEN
+         * @hide
+         */
+        public static final int OSEN = 2;
+
+        public static final String varName = "proto";
+
+        public static final String[] strings = { "WPA", "RSN", "OSEN" };
+    }
+
+    /**
+     * Recognized IEEE 802.11 authentication algorithms.
+     */
+    public static class AuthAlgorithm {
+        private AuthAlgorithm() { }
+
+        /** Open System authentication (required for WPA/WPA2) */
+        public static final int OPEN = 0;
+        /** Shared Key authentication (requires static WEP keys)
+         * @deprecated Due to security and performance limitations, use of WEP networks
+         * is discouraged. */
+        @Deprecated
+        public static final int SHARED = 1;
+        /** LEAP/Network EAP (only used with LEAP) */
+        public static final int LEAP = 2;
+
+        public static final String varName = "auth_alg";
+
+        public static final String[] strings = { "OPEN", "SHARED", "LEAP" };
+    }
+
+    /**
+     * Recognized pairwise ciphers for WPA.
+     */
+    public static class PairwiseCipher {
+        private PairwiseCipher() { }
+
+        /** Use only Group keys (deprecated) */
+        public static final int NONE = 0;
+        /** Temporal Key Integrity Protocol [IEEE 802.11i/D7.0]
+         * @deprecated Due to security and performance limitations, use of WPA-1 networks
+         * is discouraged. WPA-2 (RSN) should be used instead. */
+        @Deprecated
+        public static final int TKIP = 1;
+        /** AES in Counter mode with CBC-MAC [RFC 3610, IEEE 802.11i/D7.0] */
+        public static final int CCMP = 2;
+        /**
+         * AES in Galois/Counter Mode
+         */
+        public static final int GCMP_256 = 3;
+
+        public static final String varName = "pairwise";
+
+        public static final String[] strings = { "NONE", "TKIP", "CCMP", "GCMP_256" };
+    }
+
+    /**
+     * Recognized group ciphers.
+     * <pre>
+     * CCMP = AES in Counter mode with CBC-MAC [RFC 3610, IEEE 802.11i/D7.0]
+     * TKIP = Temporal Key Integrity Protocol [IEEE 802.11i/D7.0]
+     * WEP104 = WEP (Wired Equivalent Privacy) with 104-bit key
+     * WEP40 = WEP (Wired Equivalent Privacy) with 40-bit key (original 802.11)
+     * GCMP_256 = AES in Galois/Counter Mode
+     * </pre>
+     */
+    public static class GroupCipher {
+        private GroupCipher() { }
+
+        /** WEP40 = WEP (Wired Equivalent Privacy) with 40-bit key (original 802.11)
+         * @deprecated Due to security and performance limitations, use of WEP networks
+         * is discouraged. */
+        @Deprecated
+        public static final int WEP40 = 0;
+        /** WEP104 = WEP (Wired Equivalent Privacy) with 104-bit key
+         * @deprecated Due to security and performance limitations, use of WEP networks
+         * is discouraged. */
+        @Deprecated
+        public static final int WEP104 = 1;
+        /** Temporal Key Integrity Protocol [IEEE 802.11i/D7.0] */
+        public static final int TKIP = 2;
+        /** AES in Counter mode with CBC-MAC [RFC 3610, IEEE 802.11i/D7.0] */
+        public static final int CCMP = 3;
+        /** Hotspot 2.0 r2 OSEN
+         * @hide
+         */
+        public static final int GTK_NOT_USED = 4;
+        /**
+         * AES in Galois/Counter Mode
+         */
+        public static final int GCMP_256 = 5;
+
+        public static final String varName = "group";
+
+        public static final String[] strings =
+                { /* deprecated */ "WEP40", /* deprecated */ "WEP104",
+                        "TKIP", "CCMP", "GTK_NOT_USED", "GCMP_256" };
+    }
+
+    /**
+     * Recognized group management ciphers.
+     * <pre>
+     * BIP_CMAC_256 = Cipher-based Message Authentication Code 256 bits
+     * BIP_GMAC_128 = Galois Message Authentication Code 128 bits
+     * BIP_GMAC_256 = Galois Message Authentication Code 256 bits
+     * </pre>
+     */
+    public static class GroupMgmtCipher {
+        private GroupMgmtCipher() { }
+
+        /** CMAC-256 = Cipher-based Message Authentication Code */
+        public static final int BIP_CMAC_256 = 0;
+
+        /** GMAC-128 = Galois Message Authentication Code */
+        public static final int BIP_GMAC_128 = 1;
+
+        /** GMAC-256 = Galois Message Authentication Code */
+        public static final int BIP_GMAC_256 = 2;
+
+        private static final String varName = "groupMgmt";
+
+        private static final String[] strings = { "BIP_CMAC_256",
+                "BIP_GMAC_128", "BIP_GMAC_256"};
+    }
+
+    /**
+     * Recognized suiteB ciphers.
+     * <pre>
+     * ECDHE_ECDSA
+     * ECDHE_RSA
+     * </pre>
+     * @hide
+     */
+    public static class SuiteBCipher {
+        private SuiteBCipher() { }
+
+        /** Diffie-Hellman with Elliptic Curve_ECDSA signature */
+        public static final int ECDHE_ECDSA = 0;
+
+        /** Diffie-Hellman with_RSA signature */
+        public static final int ECDHE_RSA = 1;
+
+        private static final String varName = "SuiteB";
+
+        private static final String[] strings = { "ECDHE_ECDSA", "ECDHE_RSA" };
+    }
+
+    /** Possible status of a network configuration. */
+    public static class Status {
+        private Status() { }
+
+        /** this is the network we are currently connected to */
+        public static final int CURRENT = 0;
+        /** supplicant will not attempt to use this network */
+        public static final int DISABLED = 1;
+        /** supplicant will consider this network available for association */
+        public static final int ENABLED = 2;
+
+        public static final String[] strings = { "current", "disabled", "enabled" };
+    }
+
+    /**
+     * Security types we support.
+     */
+    /** @hide */
+    public static final int SECURITY_TYPE_OPEN = 0;
+    /** @hide */
+    public static final int SECURITY_TYPE_WEP = 1;
+    /** @hide */
+    public static final int SECURITY_TYPE_PSK = 2;
+    /** @hide */
+    public static final int SECURITY_TYPE_EAP = 3;
+    /** @hide */
+    public static final int SECURITY_TYPE_SAE = 4;
+    /** @hide */
+    public static final int SECURITY_TYPE_EAP_SUITE_B = 5;
+    /** @hide */
+    public static final int SECURITY_TYPE_OWE = 6;
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = { "SECURITY_TYPE_" }, value = {
+            SECURITY_TYPE_OPEN,
+            SECURITY_TYPE_WEP,
+            SECURITY_TYPE_PSK,
+            SECURITY_TYPE_EAP,
+            SECURITY_TYPE_SAE,
+            SECURITY_TYPE_EAP_SUITE_B,
+            SECURITY_TYPE_OWE
+    })
+    public @interface SecurityType {}
+
+    /**
+     * @hide
+     * Set security params (sets the various bitsets exposed in WifiConfiguration).
+     *
+     * @param securityType One of the security types from {@link SecurityType}.
+     */
+    public void setSecurityParams(@SecurityType int securityType) {
+        // Clear all the bitsets.
+        allowedKeyManagement.clear();
+        allowedProtocols.clear();
+        allowedAuthAlgorithms.clear();
+        allowedPairwiseCiphers.clear();
+        allowedGroupCiphers.clear();
+        allowedGroupManagementCiphers.clear();
+        allowedSuiteBCiphers.clear();
+
+        switch (securityType) {
+            case SECURITY_TYPE_OPEN:
+                allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
+                break;
+            case SECURITY_TYPE_WEP:
+                allowedKeyManagement.set(WifiConfiguration.KeyMgmt.NONE);
+                allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.OPEN);
+                allowedAuthAlgorithms.set(WifiConfiguration.AuthAlgorithm.SHARED);
+                break;
+            case SECURITY_TYPE_PSK:
+                allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_PSK);
+                break;
+            case SECURITY_TYPE_EAP:
+                allowedKeyManagement.set(WifiConfiguration.KeyMgmt.WPA_EAP);
+                allowedKeyManagement.set(WifiConfiguration.KeyMgmt.IEEE8021X);
+                break;
+            case SECURITY_TYPE_SAE:
+                allowedKeyManagement.set(WifiConfiguration.KeyMgmt.SAE);
+                requirePMF = true;
+                break;
+            case SECURITY_TYPE_EAP_SUITE_B:
+                allowedKeyManagement.set(WifiConfiguration.KeyMgmt.SUITE_B_192);
+                allowedGroupCiphers.set(WifiConfiguration.GroupCipher.GCMP_256);
+                allowedGroupManagementCiphers.set(WifiConfiguration.GroupMgmtCipher.BIP_GMAC_256);
+                // Note: allowedSuiteBCiphers bitset will be set by the service once the
+                // certificates are attached to this profile
+                requirePMF = true;
+                break;
+            case SECURITY_TYPE_OWE:
+                allowedKeyManagement.set(WifiConfiguration.KeyMgmt.OWE);
+                requirePMF = true;
+                break;
+            default:
+                throw new IllegalArgumentException("unknown security type " + securityType);
+        }
+    }
+
+    /** @hide */
+    public static final int UNKNOWN_UID = -1;
+
+    /**
+     * The ID number that the supplicant uses to identify this
+     * network configuration entry. This must be passed as an argument
+     * to most calls into the supplicant.
+     */
+    public int networkId;
+
+    // Fixme We need remove this field to use only Quality network selection status only
+    /**
+     * The current status of this network configuration entry.
+     * @see Status
+     */
+    public int status;
+
+    /**
+     * The network's SSID. Can either be a UTF-8 string,
+     * which must be enclosed in double quotation marks
+     * (e.g., {@code "MyNetwork"}), or a string of
+     * hex digits, which are not enclosed in quotes
+     * (e.g., {@code 01a243f405}).
+     */
+    public String SSID;
+
+    /**
+     * When set, this network configuration entry should only be used when
+     * associating with the AP having the specified BSSID. The value is
+     * a string in the format of an Ethernet MAC address, e.g.,
+     * <code>XX:XX:XX:XX:XX:XX</code> where each <code>X</code> is a hex digit.
+     */
+    public String BSSID;
+
+    /**
+     * 2GHz band.
+     * @hide
+     */
+    public static final int AP_BAND_2GHZ = 0;
+
+    /**
+     * 5GHz band.
+     * @hide
+     */
+    public static final int AP_BAND_5GHZ = 1;
+
+    /**
+     * Device is allowed to choose the optimal band (2Ghz or 5Ghz) based on device capability,
+     * operating country code and current radio conditions.
+     * @hide
+     */
+    public static final int AP_BAND_ANY = -1;
+
+    /**
+     * The band which AP resides on
+     * -1:Any 0:2G 1:5G
+     * By default, 2G is chosen
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public int apBand = AP_BAND_2GHZ;
+
+    /**
+     * The channel which AP resides on,currently, US only
+     * 2G  1-11
+     * 5G  36,40,44,48,149,153,157,161,165
+     * 0 - find a random available channel according to the apBand
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public int apChannel = 0;
+
+    /**
+     * Pre-shared key for use with WPA-PSK. Either an ASCII string enclosed in
+     * double quotation marks (e.g., {@code "abcdefghij"} for PSK passphrase or
+     * a string of 64 hex digits for raw PSK.
+     * <p/>
+     * When the value of this key is read, the actual key is
+     * not returned, just a "*" if the key has a value, or the null
+     * string otherwise.
+     */
+    public String preSharedKey;
+
+    /**
+     * Four WEP keys. For each of the four values, provide either an ASCII
+     * string enclosed in double quotation marks (e.g., {@code "abcdef"}),
+     * a string of hex digits (e.g., {@code 0102030405}), or an empty string
+     * (e.g., {@code ""}).
+     * <p/>
+     * When the value of one of these keys is read, the actual key is
+     * not returned, just a "*" if the key has a value, or the null
+     * string otherwise.
+     * @deprecated Due to security and performance limitations, use of WEP networks
+     * is discouraged.
+     */
+    @Deprecated
+    public String[] wepKeys;
+
+    /** Default WEP key index, ranging from 0 to 3.
+     * @deprecated Due to security and performance limitations, use of WEP networks
+     * is discouraged. */
+    @Deprecated
+    public int wepTxKeyIndex;
+
+    /**
+     * Priority determines the preference given to a network by {@code wpa_supplicant}
+     * when choosing an access point with which to associate.
+     * @deprecated This field does not exist anymore.
+     */
+    @Deprecated
+    public int priority;
+
+    /**
+     * This is a network that does not broadcast its SSID, so an
+     * SSID-specific probe request must be used for scans.
+     */
+    public boolean hiddenSSID;
+
+    /**
+     * This is a network that requries Protected Management Frames (PMF).
+     * @hide
+     */
+    public boolean requirePMF;
+
+    /**
+     * Update identifier, for Passpoint network.
+     * @hide
+     */
+    public String updateIdentifier;
+
+    /**
+     * The set of key management protocols supported by this configuration.
+     * See {@link KeyMgmt} for descriptions of the values.
+     * Defaults to WPA-PSK WPA-EAP.
+     */
+    @NonNull
+    public BitSet allowedKeyManagement;
+    /**
+     * The set of security protocols supported by this configuration.
+     * See {@link Protocol} for descriptions of the values.
+     * Defaults to WPA RSN.
+     */
+    @NonNull
+    public BitSet allowedProtocols;
+    /**
+     * The set of authentication protocols supported by this configuration.
+     * See {@link AuthAlgorithm} for descriptions of the values.
+     * Defaults to automatic selection.
+     */
+    @NonNull
+    public BitSet allowedAuthAlgorithms;
+    /**
+     * The set of pairwise ciphers for WPA supported by this configuration.
+     * See {@link PairwiseCipher} for descriptions of the values.
+     * Defaults to CCMP TKIP.
+     */
+    @NonNull
+    public BitSet allowedPairwiseCiphers;
+    /**
+     * The set of group ciphers supported by this configuration.
+     * See {@link GroupCipher} for descriptions of the values.
+     * Defaults to CCMP TKIP WEP104 WEP40.
+     */
+    @NonNull
+    public BitSet allowedGroupCiphers;
+    /**
+     * The set of group management ciphers supported by this configuration.
+     * See {@link GroupMgmtCipher} for descriptions of the values.
+     */
+    @NonNull
+    public BitSet allowedGroupManagementCiphers;
+    /**
+     * The set of SuiteB ciphers supported by this configuration.
+     * To be used for WPA3-Enterprise mode.
+     * See {@link SuiteBCipher} for descriptions of the values.
+     */
+    @NonNull
+    public BitSet allowedSuiteBCiphers;
+    /**
+     * The enterprise configuration details specifying the EAP method,
+     * certificates and other settings associated with the EAP.
+     */
+    public WifiEnterpriseConfig enterpriseConfig;
+
+    /**
+     * Fully qualified domain name of a Passpoint configuration
+     */
+    public String FQDN;
+
+    /**
+     * Name of Passpoint credential provider
+     */
+    public String providerFriendlyName;
+
+    /**
+     * Flag indicating if this network is provided by a home Passpoint provider or a roaming
+     * Passpoint provider.  This flag will be {@code true} if this network is provided by
+     * a home Passpoint provider and {@code false} if is provided by a roaming Passpoint provider
+     * or is a non-Passpoint network.
+     */
+    public boolean isHomeProviderNetwork;
+
+    /**
+     * Roaming Consortium Id list for Passpoint credential; identifies a set of networks where
+     * Passpoint credential will be considered valid
+     */
+    public long[] roamingConsortiumIds;
+
+    /**
+     * @hide
+     * This network configuration is visible to and usable by other users on the
+     * same device.
+     */
+    @UnsupportedAppUsage
+    public boolean shared;
+
+    /**
+     * @hide
+     */
+    @NonNull
+    @UnsupportedAppUsage
+    private IpConfiguration mIpConfiguration;
+
+    /**
+     * @hide
+     * dhcp server MAC address if known
+     */
+    public String dhcpServer;
+
+    /**
+     * @hide
+     * default Gateway MAC address if known
+     */
+    @UnsupportedAppUsage
+    public String defaultGwMacAddress;
+
+    /**
+     * @hide
+     * last time we connected, this configuration had validated internet access
+     */
+    @UnsupportedAppUsage
+    public boolean validatedInternetAccess;
+
+    /**
+     * @hide
+     * The number of beacon intervals between Delivery Traffic Indication Maps (DTIM)
+     * This value is populated from scan results that contain Beacon Frames, which are infrequent.
+     * The value is not guaranteed to be set or current (Although it SHOULDNT change once set)
+     * Valid values are from 1 - 255. Initialized here as 0, use this to check if set.
+     */
+    public int dtimInterval = 0;
+
+    /**
+     * Flag indicating if this configuration represents a legacy Passpoint configuration
+     * (Release N or older).  This is used for migrating Passpoint configuration from N to O.
+     * This will no longer be needed after O.
+     * @hide
+     */
+    public boolean isLegacyPasspointConfig = false;
+    /**
+     * @hide
+     * Uid of app creating the configuration
+     */
+    @SystemApi
+    public int creatorUid;
+
+    /**
+     * @hide
+     * Uid of last app issuing a connection related command
+     */
+    @UnsupportedAppUsage
+    public int lastConnectUid;
+
+    /**
+     * @hide
+     * Uid of last app modifying the configuration
+     */
+    @SystemApi
+    public int lastUpdateUid;
+
+    /**
+     * @hide
+     * Universal name for app creating the configuration
+     *    see {@link PackageManager#getNameForUid(int)}
+     */
+    @SystemApi
+    public String creatorName;
+
+    /**
+     * @hide
+     * Universal name for app updating the configuration
+     *    see {@link PackageManager#getNameForUid(int)}
+     */
+    @SystemApi
+    public String lastUpdateName;
+
+    /**
+     * @hide
+     * Status of user approval for connection
+     */
+    public int userApproved = USER_UNSPECIFIED;
+
+    /** The Below RSSI thresholds are used to configure AutoJoin
+     *  - GOOD/LOW/BAD thresholds are used so as to calculate link score
+     *  - UNWANTED_SOFT are used by the blacklisting logic so as to handle
+     *  the unwanted network message coming from CS
+     *  - UNBLACKLIST thresholds are used so as to tweak the speed at which
+     *  the network is unblacklisted (i.e. if
+     *          it is seen with good RSSI, it is blacklisted faster)
+     *  - INITIAL_AUTOJOIN_ATTEMPT, used to determine how close from
+     *  the network we need to be before autojoin kicks in
+     */
+    /** @hide **/
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    public static int INVALID_RSSI = -127;
+
+    // States for the userApproved field
+    /**
+     * @hide
+     * User hasn't specified if connection is okay
+     */
+    public static final int USER_UNSPECIFIED = 0;
+    /**
+     * @hide
+     * User has approved this for connection
+     */
+    public static final int USER_APPROVED = 1;
+    /**
+     * @hide
+     * User has banned this from connection
+     */
+    public static final int USER_BANNED = 2;
+    /**
+     * @hide
+     * Waiting for user input
+     */
+    public static final int USER_PENDING = 3;
+
+    /**
+     * @hide
+     * Number of reports indicating no Internet Access
+     */
+    @UnsupportedAppUsage
+    public int numNoInternetAccessReports;
+
+    /**
+     * @hide
+     * For debug: date at which the config was last updated
+     */
+    public String updateTime;
+
+    /**
+     * @hide
+     * For debug: date at which the config was last updated
+     */
+    public String creationTime;
+
+    /**
+     * @hide
+     * The WiFi configuration is considered to have no internet access for purpose of autojoining
+     * if there has been a report of it having no internet access, and, it never have had
+     * internet access in the past.
+     */
+    @SystemApi
+    public boolean hasNoInternetAccess() {
+        return numNoInternetAccessReports > 0 && !validatedInternetAccess;
+    }
+
+    /**
+     * The WiFi configuration is expected not to have Internet access (e.g., a wireless printer, a
+     * Chromecast hotspot, etc.). This will be set if the user explicitly confirms a connection to
+     * this configuration and selects "don't ask again".
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public boolean noInternetAccessExpected;
+
+    /**
+     * The WiFi configuration is expected not to have Internet access (e.g., a wireless printer, a
+     * Chromecast hotspot, etc.). This will be set if the user explicitly confirms a connection to
+     * this configuration and selects "don't ask again".
+     * @hide
+     */
+    @SystemApi
+    public boolean isNoInternetAccessExpected() {
+        return noInternetAccessExpected;
+    }
+
+    /**
+     * This Wifi configuration is expected for OSU(Online Sign Up) of Passpoint Release 2.
+     * @hide
+     */
+    public boolean osu;
+
+    /**
+     * @hide
+     * Last time the system was connected to this configuration.
+     */
+    public long lastConnected;
+
+    /**
+     * @hide
+     * Last time the system was disconnected to this configuration.
+     */
+    public long lastDisconnected;
+
+    /**
+     * Set if the configuration was self added by the framework
+     * This boolean is cleared if we get a connect/save/ update or
+     * any wifiManager command that indicate the user interacted with the configuration
+     * since we will now consider that the configuration belong to him.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public boolean selfAdded;
+
+    /**
+     * Set if the configuration was self added by the framework
+     * This boolean is set once and never cleared. It is used
+     * so as we never loose track of who created the
+     * configuration in the first place.
+     * @hide
+     */
+    public boolean didSelfAdd;
+
+    /**
+     * Peer WifiConfiguration this WifiConfiguration was added for
+     * @hide
+     */
+    public String peerWifiConfiguration;
+
+    /**
+     * @hide
+     * Indicate that a WifiConfiguration is temporary and should not be saved
+     * nor considered by AutoJoin.
+     */
+    public boolean ephemeral;
+
+    /**
+     * @hide
+     * Indicate that a WifiConfiguration is temporary and should not be saved
+     * nor considered by AutoJoin.
+     */
+    @SystemApi
+    public boolean isEphemeral() {
+      return ephemeral;
+    }
+
+    /**
+     * Indicate whther the network is trusted or not. Networks are considered trusted
+     * if the user explicitly allowed this network connection.
+     * @hide
+     */
+    public boolean trusted;
+
+    /**
+     * This Wifi configuration is created from a {@link WifiNetworkSuggestion}
+     * @hide
+     */
+    public boolean fromWifiNetworkSuggestion;
+
+    /**
+     * This Wifi configuration is created from a {@link WifiNetworkSpecifier}
+     * @hide
+     */
+    public boolean fromWifiNetworkSpecifier;
+
+    /**
+     * Indicates if the creator of this configuration has expressed that it
+     * should be considered metered.
+     *
+     * @see #isMetered(WifiConfiguration, WifiInfo)
+     * @hide
+     */
+    @SystemApi
+    public boolean meteredHint;
+
+    /** {@hide} */
+    public static final int METERED_OVERRIDE_NONE = 0;
+    /** {@hide} */
+    public static final int METERED_OVERRIDE_METERED = 1;
+    /** {@hide} */
+    public static final int METERED_OVERRIDE_NOT_METERED = 2;
+
+    /**
+     * Indicates if the end user has expressed an explicit opinion about the
+     * meteredness of this network, such as through the Settings app.
+     * <p>
+     * This should always override any values from {@link #meteredHint} or
+     * {@link WifiInfo#getMeteredHint()}.
+     *
+     * @see #isMetered(WifiConfiguration, WifiInfo)
+     * @hide
+     */
+    public int meteredOverride = METERED_OVERRIDE_NONE;
+
+    /**
+     * Blend together all the various opinions to decide if the given network
+     * should be considered metered or not.
+     *
+     * @hide
+     */
+    public static boolean isMetered(WifiConfiguration config, WifiInfo info) {
+        boolean metered = false;
+        if (info != null && info.getMeteredHint()) {
+            metered = true;
+        }
+        if (config != null && config.meteredHint) {
+            metered = true;
+        }
+        if (config != null
+                && config.meteredOverride == WifiConfiguration.METERED_OVERRIDE_METERED) {
+            metered = true;
+        }
+        if (config != null
+                && config.meteredOverride == WifiConfiguration.METERED_OVERRIDE_NOT_METERED) {
+            metered = false;
+        }
+        return metered;
+    }
+
+    /**
+     * @hide
+     * Returns true if this WiFi config is for an open network.
+     */
+    public boolean isOpenNetwork() {
+        final int cardinality = allowedKeyManagement.cardinality();
+        final boolean hasNoKeyMgmt = cardinality == 0
+                || (cardinality == 1 && (allowedKeyManagement.get(KeyMgmt.NONE)
+                || allowedKeyManagement.get(KeyMgmt.OWE)));
+
+        boolean hasNoWepKeys = true;
+        if (wepKeys != null) {
+            for (int i = 0; i < wepKeys.length; i++) {
+                if (wepKeys[i] != null) {
+                    hasNoWepKeys = false;
+                    break;
+                }
+            }
+        }
+
+        return hasNoKeyMgmt && hasNoWepKeys;
+    }
+
+    /**
+     * @hide
+     * Setting this value will force scan results associated with this configuration to
+     * be included in the bucket of networks that are externally scored.
+     * If not set, associated scan results will be treated as legacy saved networks and
+     * will take precedence over networks in the scored category.
+     */
+    @SystemApi
+    public boolean useExternalScores;
+
+    /**
+     * @hide
+     * Number of time the scorer overrode a the priority based choice, when comparing two
+     * WifiConfigurations, note that since comparing WifiConfiguration happens very often
+     * potentially at every scan, this number might become very large, even on an idle
+     * system.
+     */
+    @SystemApi
+    public int numScorerOverride;
+
+    /**
+     * @hide
+     * Number of time the scorer overrode a the priority based choice, and the comparison
+     * triggered a network switch
+     */
+    @SystemApi
+    public int numScorerOverrideAndSwitchedNetwork;
+
+    /**
+     * @hide
+     * Number of time we associated to this configuration.
+     */
+    @SystemApi
+    public int numAssociation;
+
+    /**
+     * @hide
+     * Use factory MAC when connecting to this network
+     */
+    public static final int RANDOMIZATION_NONE = 0;
+    /**
+     * @hide
+     * Generate a randomized MAC once and reuse it for all connections to this network
+     */
+    public static final int RANDOMIZATION_PERSISTENT = 1;
+
+    /**
+     * @hide
+     * Level of MAC randomization for this network
+     */
+    public int macRandomizationSetting = RANDOMIZATION_PERSISTENT;
+
+    /**
+     * @hide
+     * Randomized MAC address to use with this particular network
+     */
+    @NonNull
+    private MacAddress mRandomizedMacAddress;
+
+    /**
+     * @hide
+     * Checks if the given MAC address can be used for Connected Mac Randomization
+     * by verifying that it is non-null, unicast, locally assigned, and not default mac.
+     * @param mac MacAddress to check
+     * @return true if mac is good to use
+     */
+    public static boolean isValidMacAddressForRandomization(MacAddress mac) {
+        return mac != null && !mac.isMulticastAddress() && mac.isLocallyAssigned()
+                && !MacAddress.fromString(WifiInfo.DEFAULT_MAC_ADDRESS).equals(mac);
+    }
+
+    /**
+     * @hide
+     * Returns Randomized MAC address to use with the network.
+     * If it is not set/valid, creates a new randomized address.
+     * If it can't generate a valid mac, returns the default MAC.
+     */
+    public @NonNull MacAddress getOrCreateRandomizedMacAddress() {
+        int randomMacGenerationCount = 0;
+        while (!isValidMacAddressForRandomization(mRandomizedMacAddress)
+                && randomMacGenerationCount < MAXIMUM_RANDOM_MAC_GENERATION_RETRY) {
+            mRandomizedMacAddress = MacAddress.createRandomUnicastAddress();
+            randomMacGenerationCount++;
+        }
+
+        if (!isValidMacAddressForRandomization(mRandomizedMacAddress)) {
+            mRandomizedMacAddress = MacAddress.fromString(WifiInfo.DEFAULT_MAC_ADDRESS);
+        }
+        return mRandomizedMacAddress;
+    }
+
+    /**
+     * Returns MAC address set to be the local randomized MAC address.
+     * Depending on user preference, the device may or may not use the returned MAC address for
+     * connections to this network.
+     * <p>
+     * Information is restricted to Device Owner, Profile Owner, and Carrier apps
+     * (which will only obtain addresses for configurations which they create). Other callers
+     * will receive a default "02:00:00:00:00:00" MAC address.
+     */
+    public @NonNull MacAddress getRandomizedMacAddress() {
+        return mRandomizedMacAddress;
+    }
+
+    /**
+     * @hide
+     * @param mac MacAddress to change into
+     */
+    public void setRandomizedMacAddress(@NonNull MacAddress mac) {
+        if (mac == null) {
+            Log.e(TAG, "setRandomizedMacAddress received null MacAddress.");
+            return;
+        }
+        mRandomizedMacAddress = mac;
+    }
+
+    /** @hide
+     * Boost given to RSSI on a home network for the purpose of calculating the score
+     * This adds stickiness to home networks, as defined by:
+     * - less than 4 known BSSIDs
+     * - PSK only
+     * - TODO: add a test to verify that all BSSIDs are behind same gateway
+     ***/
+    public static final int HOME_NETWORK_RSSI_BOOST = 5;
+
+    /**
+     * @hide
+     * This class is used to contain all the information and API used for quality network selection
+     */
+    public static class NetworkSelectionStatus {
+        /**
+         * Quality Network Selection Status enable, temporary disabled, permanently disabled
+         */
+        /**
+         * This network is allowed to join Quality Network Selection
+         */
+        public static final int NETWORK_SELECTION_ENABLED = 0;
+        /**
+         * network was temporary disabled. Can be re-enabled after a time period expire
+         */
+        public static final int NETWORK_SELECTION_TEMPORARY_DISABLED  = 1;
+        /**
+         * network was permanently disabled.
+         */
+        public static final int NETWORK_SELECTION_PERMANENTLY_DISABLED  = 2;
+        /**
+         * Maximum Network selection status
+         */
+        public static final int NETWORK_SELECTION_STATUS_MAX = 3;
+
+        /**
+         * Quality network selection status String (for debug purpose). Use Quality network
+         * selection status value as index to extec the corresponding debug string
+         */
+        public static final String[] QUALITY_NETWORK_SELECTION_STATUS = {
+                "NETWORK_SELECTION_ENABLED",
+                "NETWORK_SELECTION_TEMPORARY_DISABLED",
+                "NETWORK_SELECTION_PERMANENTLY_DISABLED"};
+
+        //Quality Network disabled reasons
+        /**
+         * Default value. Means not disabled
+         */
+        public static final int NETWORK_SELECTION_ENABLE = 0;
+        /**
+         * The starting index for network selection disabled reasons
+         */
+        public static final int NETWORK_SELECTION_DISABLED_STARTING_INDEX = 1;
+        /**
+         * @deprecated it is not used any more.
+         * This network is disabled because higher layer (>2) network is bad
+         */
+        public static final int DISABLED_BAD_LINK = 1;
+        /**
+         * This network is disabled because multiple association rejects
+         */
+        public static final int DISABLED_ASSOCIATION_REJECTION = 2;
+        /**
+         * This network is disabled because multiple authentication failure
+         */
+        public static final int DISABLED_AUTHENTICATION_FAILURE = 3;
+        /**
+         * This network is disabled because multiple DHCP failure
+         */
+        public static final int DISABLED_DHCP_FAILURE = 4;
+        /**
+         * This network is disabled because of security network but no credentials
+         */
+        public static final int DISABLED_DNS_FAILURE = 5;
+        /**
+         * This network is temporarily disabled because it has no Internet access.
+         */
+        public static final int DISABLED_NO_INTERNET_TEMPORARY = 6;
+        /**
+         * This network is disabled because we started WPS
+         */
+        public static final int DISABLED_WPS_START = 7;
+        /**
+         * This network is disabled because EAP-TLS failure
+         */
+        public static final int DISABLED_TLS_VERSION_MISMATCH = 8;
+        // Values above are for temporary disablement; values below are for permanent disablement.
+        /**
+         * This network is disabled due to absence of user credentials
+         */
+        public static final int DISABLED_AUTHENTICATION_NO_CREDENTIALS = 9;
+        /**
+         * This network is permanently disabled because it has no Internet access and user does not
+         * want to stay connected.
+         */
+        public static final int DISABLED_NO_INTERNET_PERMANENT = 10;
+        /**
+         * This network is disabled due to WifiManager disable it explicitly
+         */
+        public static final int DISABLED_BY_WIFI_MANAGER = 11;
+        /**
+         * This network is disabled due to user switching
+         */
+        public static final int DISABLED_DUE_TO_USER_SWITCH = 12;
+        /**
+         * This network is disabled due to wrong password
+         */
+        public static final int DISABLED_BY_WRONG_PASSWORD = 13;
+        /**
+         * This network is disabled because service is not subscribed
+         */
+        public static final int DISABLED_AUTHENTICATION_NO_SUBSCRIPTION = 14;
+        /**
+         * This Maximum disable reason value
+         */
+        public static final int NETWORK_SELECTION_DISABLED_MAX = 15;
+
+        /**
+         * Quality network selection disable reason String (for debug purpose)
+         */
+        public static final String[] QUALITY_NETWORK_SELECTION_DISABLE_REASON = {
+                "NETWORK_SELECTION_ENABLE",
+                "NETWORK_SELECTION_DISABLED_BAD_LINK", // deprecated
+                "NETWORK_SELECTION_DISABLED_ASSOCIATION_REJECTION ",
+                "NETWORK_SELECTION_DISABLED_AUTHENTICATION_FAILURE",
+                "NETWORK_SELECTION_DISABLED_DHCP_FAILURE",
+                "NETWORK_SELECTION_DISABLED_DNS_FAILURE",
+                "NETWORK_SELECTION_DISABLED_NO_INTERNET_TEMPORARY",
+                "NETWORK_SELECTION_DISABLED_WPS_START",
+                "NETWORK_SELECTION_DISABLED_TLS_VERSION",
+                "NETWORK_SELECTION_DISABLED_AUTHENTICATION_NO_CREDENTIALS",
+                "NETWORK_SELECTION_DISABLED_NO_INTERNET_PERMANENT",
+                "NETWORK_SELECTION_DISABLED_BY_WIFI_MANAGER",
+                "NETWORK_SELECTION_DISABLED_BY_USER_SWITCH",
+                "NETWORK_SELECTION_DISABLED_BY_WRONG_PASSWORD",
+                "NETWORK_SELECTION_DISABLED_AUTHENTICATION_NO_SUBSCRIPTION"
+        };
+
+        /**
+         * Invalid time stamp for network selection disable
+         */
+        public static final long INVALID_NETWORK_SELECTION_DISABLE_TIMESTAMP = -1L;
+
+        /**
+         *  This constant indicates the current configuration has connect choice set
+         */
+        private static final int CONNECT_CHOICE_EXISTS = 1;
+
+        /**
+         *  This constant indicates the current configuration does not have connect choice set
+         */
+        private static final int CONNECT_CHOICE_NOT_EXISTS = -1;
+
+        // fields for QualityNetwork Selection
+        /**
+         * Network selection status, should be in one of three status: enable, temporaily disabled
+         * or permanently disabled
+         */
+        private int mStatus;
+
+        /**
+         * Reason for disable this network
+         */
+        private int mNetworkSelectionDisableReason;
+
+        /**
+         * Last time we temporarily disabled the configuration
+         */
+        private long mTemporarilyDisabledTimestamp = INVALID_NETWORK_SELECTION_DISABLE_TIMESTAMP;
+
+        /**
+         * counter for each Network selection disable reason
+         */
+        private int[] mNetworkSeclectionDisableCounter = new int[NETWORK_SELECTION_DISABLED_MAX];
+
+        /**
+         * Connect Choice over this configuration
+         *
+         * When current wifi configuration is visible to the user but user explicitly choose to
+         * connect to another network X, the another networks X's configure key will be stored here.
+         * We will consider user has a preference of X over this network. And in the future,
+         * network selection will always give X a higher preference over this configuration.
+         * configKey is : "SSID"-WEP-WPA_PSK-WPA_EAP
+         */
+        private String mConnectChoice;
+
+        /**
+         * The system timestamp when we records the connectChoice. This value is obtained from
+         * System.currentTimeMillis
+         */
+        private long mConnectChoiceTimestamp = INVALID_NETWORK_SELECTION_DISABLE_TIMESTAMP;
+
+        /**
+         * Used to cache the temporary candidate during the network selection procedure. It will be
+         * kept updating once a new scan result has a higher score than current one
+         */
+        private ScanResult mCandidate;
+
+        /**
+         * Used to cache the score of the current temporary candidate during the network
+         * selection procedure.
+         */
+        private int mCandidateScore;
+
+        /**
+         * Indicate whether this network is visible in latest Qualified Network Selection. This
+         * means there is scan result found related to this Configuration and meet the minimum
+         * requirement. The saved network need not join latest Qualified Network Selection. For
+         * example, it is disabled. True means network is visible in latest Qualified Network
+         * Selection and false means network is invisible
+         */
+        private boolean mSeenInLastQualifiedNetworkSelection;
+
+        /**
+         * Boolean indicating if we have ever successfully connected to this network.
+         *
+         * This value will be set to true upon a successful connection.
+         * This value will be set to false if a previous value was not stored in the config or if
+         * the credentials are updated (ex. a password change).
+         */
+        private boolean mHasEverConnected;
+
+        /**
+         * Boolean indicating whether {@link com.android.server.wifi.RecommendedNetworkEvaluator}
+         * chose not to connect to this network in the last qualified network selection process.
+         */
+        private boolean mNotRecommended;
+
+        /**
+         * Set whether {@link com.android.server.wifi.RecommendedNetworkEvaluator} does not
+         * recommend connecting to this network.
+         */
+        public void setNotRecommended(boolean notRecommended) {
+            mNotRecommended = notRecommended;
+        }
+
+        /**
+         * Returns whether {@link com.android.server.wifi.RecommendedNetworkEvaluator} does not
+         * recommend connecting to this network.
+         */
+        public boolean isNotRecommended() {
+            return mNotRecommended;
+        }
+
+        /**
+         * set whether this network is visible in latest Qualified Network Selection
+         * @param seen value set to candidate
+         */
+        public void setSeenInLastQualifiedNetworkSelection(boolean seen) {
+            mSeenInLastQualifiedNetworkSelection =  seen;
+        }
+
+        /**
+         * get whether this network is visible in latest Qualified Network Selection
+         * @return returns true -- network is visible in latest Qualified Network Selection
+         *         false -- network is invisible in latest Qualified Network Selection
+         */
+        public boolean getSeenInLastQualifiedNetworkSelection() {
+            return mSeenInLastQualifiedNetworkSelection;
+        }
+        /**
+         * set the temporary candidate of current network selection procedure
+         * @param scanCandidate {@link ScanResult} the candidate set to mCandidate
+         */
+        public void setCandidate(ScanResult scanCandidate) {
+            mCandidate = scanCandidate;
+        }
+
+        /**
+         * get the temporary candidate of current network selection procedure
+         * @return  returns {@link ScanResult} temporary candidate of current network selection
+         * procedure
+         */
+        public ScanResult getCandidate() {
+            return mCandidate;
+        }
+
+        /**
+         * set the score of the temporary candidate of current network selection procedure
+         * @param score value set to mCandidateScore
+         */
+        public void setCandidateScore(int score) {
+            mCandidateScore = score;
+        }
+
+        /**
+         * get the score of the temporary candidate of current network selection procedure
+         * @return returns score of the temporary candidate of current network selection procedure
+         */
+        public int getCandidateScore() {
+            return mCandidateScore;
+        }
+
+        /**
+         * get user preferred choice over this configuration
+         *@return returns configKey of user preferred choice over this configuration
+         */
+        public String getConnectChoice() {
+            return mConnectChoice;
+        }
+
+        /**
+         * set user preferred choice over this configuration
+         * @param newConnectChoice, the configKey of user preferred choice over this configuration
+         */
+        public void setConnectChoice(String newConnectChoice) {
+            mConnectChoice = newConnectChoice;
+        }
+
+        /**
+         * get the timeStamp when user select a choice over this configuration
+         * @return returns when current connectChoice is set (time from System.currentTimeMillis)
+         */
+        public long getConnectChoiceTimestamp() {
+            return mConnectChoiceTimestamp;
+        }
+
+        /**
+         * set the timeStamp when user select a choice over this configuration
+         * @param timeStamp, the timestamp set to connectChoiceTimestamp, expected timestamp should
+         *        be obtained from System.currentTimeMillis
+         */
+        public void setConnectChoiceTimestamp(long timeStamp) {
+            mConnectChoiceTimestamp = timeStamp;
+        }
+
+        /**
+         * get current Quality network selection status
+         * @return returns current Quality network selection status in String (for debug purpose)
+         */
+        public String getNetworkStatusString() {
+            return QUALITY_NETWORK_SELECTION_STATUS[mStatus];
+        }
+
+        public void setHasEverConnected(boolean value) {
+            mHasEverConnected = value;
+        }
+
+        public boolean getHasEverConnected() {
+            return mHasEverConnected;
+        }
+
+        public NetworkSelectionStatus() {
+            // previously stored configs will not have this parameter, so we default to false.
+            mHasEverConnected = false;
+        };
+
+        /**
+         * @param reason specific error reason
+         * @return  corresponding network disable reason String (for debug purpose)
+         */
+        public static String getNetworkDisableReasonString(int reason) {
+            if (reason >= NETWORK_SELECTION_ENABLE && reason < NETWORK_SELECTION_DISABLED_MAX) {
+                return QUALITY_NETWORK_SELECTION_DISABLE_REASON[reason];
+            } else {
+                return null;
+            }
+        }
+        /**
+         * get current network disable reason
+         * @return current network disable reason in String (for debug purpose)
+         */
+        public String getNetworkDisableReasonString() {
+            return QUALITY_NETWORK_SELECTION_DISABLE_REASON[mNetworkSelectionDisableReason];
+        }
+
+        /**
+         * get current network network selection status
+         * @return return current network network selection status
+         */
+        public int getNetworkSelectionStatus() {
+            return mStatus;
+        }
+        /**
+         * @return whether current network is enabled to join network selection
+         */
+        public boolean isNetworkEnabled() {
+            return mStatus == NETWORK_SELECTION_ENABLED;
+        }
+
+        /**
+         * @return whether current network is temporary disabled
+         */
+        public boolean isNetworkTemporaryDisabled() {
+            return mStatus == NETWORK_SELECTION_TEMPORARY_DISABLED;
+        }
+
+        /**
+         * @return returns whether current network is permanently disabled
+         */
+        public boolean isNetworkPermanentlyDisabled() {
+            return mStatus == NETWORK_SELECTION_PERMANENTLY_DISABLED;
+        }
+
+        /**
+         * set current networ work selection status
+         * @param status network selection status to set
+         */
+        public void setNetworkSelectionStatus(int status) {
+            if (status >= 0 && status < NETWORK_SELECTION_STATUS_MAX) {
+                mStatus = status;
+            }
+        }
+
+        /**
+         * @return returns current network's disable reason
+         */
+        public int getNetworkSelectionDisableReason() {
+            return mNetworkSelectionDisableReason;
+        }
+
+        /**
+         * set Network disable reason
+         * @param  reason Network disable reason
+         */
+        public void setNetworkSelectionDisableReason(int reason) {
+            if (reason >= 0 && reason < NETWORK_SELECTION_DISABLED_MAX) {
+                mNetworkSelectionDisableReason = reason;
+            } else {
+                throw new IllegalArgumentException("Illegal reason value: " + reason);
+            }
+        }
+
+        /**
+         * check whether network is disabled by this reason
+         * @param reason a specific disable reason
+         * @return true -- network is disabled for this reason
+         *         false -- network is not disabled for this reason
+         */
+        public boolean isDisabledByReason(int reason) {
+            return mNetworkSelectionDisableReason == reason;
+        }
+
+        /**
+         * @param timeStamp Set when current network is disabled in millisecond since January 1,
+         * 1970 00:00:00.0 UTC
+         */
+        public void setDisableTime(long timeStamp) {
+            mTemporarilyDisabledTimestamp = timeStamp;
+        }
+
+        /**
+         * @return returns when current network is disabled in millisecond since January 1,
+         * 1970 00:00:00.0 UTC
+         */
+        public long getDisableTime() {
+            return mTemporarilyDisabledTimestamp;
+        }
+
+        /**
+         * get the disable counter of a specific reason
+         * @param  reason specific failure reason
+         * @exception throw IllegalArgumentException for illegal input
+         * @return counter number for specific error reason.
+         */
+        public int getDisableReasonCounter(int reason) {
+            if (reason >= NETWORK_SELECTION_ENABLE && reason < NETWORK_SELECTION_DISABLED_MAX) {
+                return mNetworkSeclectionDisableCounter[reason];
+            } else {
+                throw new IllegalArgumentException("Illegal reason value: " + reason);
+            }
+        }
+
+        /**
+         * set the counter of a specific failure reason
+         * @param reason reason for disable error
+         * @param value the counter value for this specific reason
+         * @exception throw IllegalArgumentException for illegal input
+         */
+        public void setDisableReasonCounter(int reason, int value) {
+            if (reason >= NETWORK_SELECTION_ENABLE && reason < NETWORK_SELECTION_DISABLED_MAX) {
+                mNetworkSeclectionDisableCounter[reason] = value;
+            } else {
+                throw new IllegalArgumentException("Illegal reason value: " + reason);
+            }
+        }
+
+        /**
+         * increment the counter of a specific failure reason
+         * @param reason a specific failure reason
+         * @exception throw IllegalArgumentException for illegal input
+         */
+        public void incrementDisableReasonCounter(int reason) {
+            if (reason >= NETWORK_SELECTION_ENABLE  && reason < NETWORK_SELECTION_DISABLED_MAX) {
+                mNetworkSeclectionDisableCounter[reason]++;
+            } else {
+                throw new IllegalArgumentException("Illegal reason value: " + reason);
+            }
+        }
+
+        /**
+         * clear the counter of a specific failure reason
+         * @hide
+         * @param reason a specific failure reason
+         * @exception throw IllegalArgumentException for illegal input
+         */
+        public void clearDisableReasonCounter(int reason) {
+            if (reason >= NETWORK_SELECTION_ENABLE && reason < NETWORK_SELECTION_DISABLED_MAX) {
+                mNetworkSeclectionDisableCounter[reason] = NETWORK_SELECTION_ENABLE;
+            } else {
+                throw new IllegalArgumentException("Illegal reason value: " + reason);
+            }
+        }
+
+        /**
+         * clear all the failure reason counters
+         */
+        public void clearDisableReasonCounter() {
+            Arrays.fill(mNetworkSeclectionDisableCounter, NETWORK_SELECTION_ENABLE);
+        }
+
+        /**
+         * BSSID for connection to this network (through network selection procedure)
+         */
+        private String mNetworkSelectionBSSID;
+
+        /**
+         * get current network Selection BSSID
+         * @return current network Selection BSSID
+         */
+        public String getNetworkSelectionBSSID() {
+            return mNetworkSelectionBSSID;
+        }
+
+        /**
+         * set network Selection BSSID
+         * @param bssid The target BSSID for assocaition
+         */
+        public void setNetworkSelectionBSSID(String bssid) {
+            mNetworkSelectionBSSID = bssid;
+        }
+
+        public void copy(NetworkSelectionStatus source) {
+            mStatus = source.mStatus;
+            mNetworkSelectionDisableReason = source.mNetworkSelectionDisableReason;
+            for (int index = NETWORK_SELECTION_ENABLE; index < NETWORK_SELECTION_DISABLED_MAX;
+                    index++) {
+                mNetworkSeclectionDisableCounter[index] =
+                        source.mNetworkSeclectionDisableCounter[index];
+            }
+            mTemporarilyDisabledTimestamp = source.mTemporarilyDisabledTimestamp;
+            mNetworkSelectionBSSID = source.mNetworkSelectionBSSID;
+            setSeenInLastQualifiedNetworkSelection(source.getSeenInLastQualifiedNetworkSelection());
+            setCandidate(source.getCandidate());
+            setCandidateScore(source.getCandidateScore());
+            setConnectChoice(source.getConnectChoice());
+            setConnectChoiceTimestamp(source.getConnectChoiceTimestamp());
+            setHasEverConnected(source.getHasEverConnected());
+            setNotRecommended(source.isNotRecommended());
+        }
+
+        public void writeToParcel(Parcel dest) {
+            dest.writeInt(getNetworkSelectionStatus());
+            dest.writeInt(getNetworkSelectionDisableReason());
+            for (int index = NETWORK_SELECTION_ENABLE; index < NETWORK_SELECTION_DISABLED_MAX;
+                    index++) {
+                dest.writeInt(getDisableReasonCounter(index));
+            }
+            dest.writeLong(getDisableTime());
+            dest.writeString(getNetworkSelectionBSSID());
+            if (getConnectChoice() != null) {
+                dest.writeInt(CONNECT_CHOICE_EXISTS);
+                dest.writeString(getConnectChoice());
+                dest.writeLong(getConnectChoiceTimestamp());
+            } else {
+                dest.writeInt(CONNECT_CHOICE_NOT_EXISTS);
+            }
+            dest.writeInt(getHasEverConnected() ? 1 : 0);
+            dest.writeInt(isNotRecommended() ? 1 : 0);
+        }
+
+        public void readFromParcel(Parcel in) {
+            setNetworkSelectionStatus(in.readInt());
+            setNetworkSelectionDisableReason(in.readInt());
+            for (int index = NETWORK_SELECTION_ENABLE; index < NETWORK_SELECTION_DISABLED_MAX;
+                    index++) {
+                setDisableReasonCounter(index, in.readInt());
+            }
+            setDisableTime(in.readLong());
+            setNetworkSelectionBSSID(in.readString());
+            if (in.readInt() == CONNECT_CHOICE_EXISTS) {
+                setConnectChoice(in.readString());
+                setConnectChoiceTimestamp(in.readLong());
+            } else {
+                setConnectChoice(null);
+                setConnectChoiceTimestamp(INVALID_NETWORK_SELECTION_DISABLE_TIMESTAMP);
+            }
+            setHasEverConnected(in.readInt() != 0);
+            setNotRecommended(in.readInt() != 0);
+        }
+    }
+
+    /**
+     * @hide
+     * network selection related member
+     */
+    private NetworkSelectionStatus mNetworkSelectionStatus = new NetworkSelectionStatus();
+
+    /**
+     * @hide
+     * This class is intended to store extra failure reason information for the most recent
+     * connection attempt, so that it may be surfaced to the settings UI
+     */
+    public static class RecentFailure {
+
+        /**
+         * No recent failure, or no specific reason given for the recent connection failure
+         */
+        public static final int NONE = 0;
+        /**
+         * Connection to this network recently failed due to Association Rejection Status 17
+         * (AP is full)
+         */
+        public static final int STATUS_AP_UNABLE_TO_HANDLE_NEW_STA = 17;
+        /**
+         * Association Rejection Status code (NONE for success/non-association-rejection-fail)
+         */
+        private int mAssociationStatus = NONE;
+
+        /**
+         * @param status the association status code for the recent failure
+         */
+        public void setAssociationStatus(int status) {
+            mAssociationStatus = status;
+        }
+        /**
+         * Sets the RecentFailure to NONE
+         */
+        public void clear() {
+            mAssociationStatus = NONE;
+        }
+        /**
+         * Get the recent failure code
+         */
+        public int getAssociationStatus() {
+            return mAssociationStatus;
+        }
+    }
+
+    /**
+     * @hide
+     * RecentFailure member
+     */
+    final public RecentFailure recentFailure = new RecentFailure();
+
+    /**
+     * @hide
+     * @return network selection status
+     */
+    public NetworkSelectionStatus getNetworkSelectionStatus() {
+        return mNetworkSelectionStatus;
+    }
+
+    /**
+     * Set the network selection status
+     * @hide
+     */
+    public void setNetworkSelectionStatus(NetworkSelectionStatus status) {
+        mNetworkSelectionStatus = status;
+    }
+
+    /**
+     * @hide
+     * Linked Configurations: represent the set of Wificonfigurations that are equivalent
+     * regarding roaming and auto-joining.
+     * The linked configuration may or may not have same SSID, and may or may not have same
+     * credentials.
+     * For instance, linked configurations will have same defaultGwMacAddress or same dhcp server.
+     */
+    public HashMap<String, Integer>  linkedConfigurations;
+
+    public WifiConfiguration() {
+        networkId = INVALID_NETWORK_ID;
+        SSID = null;
+        BSSID = null;
+        FQDN = null;
+        roamingConsortiumIds = new long[0];
+        priority = 0;
+        hiddenSSID = false;
+        allowedKeyManagement = new BitSet();
+        allowedProtocols = new BitSet();
+        allowedAuthAlgorithms = new BitSet();
+        allowedPairwiseCiphers = new BitSet();
+        allowedGroupCiphers = new BitSet();
+        allowedGroupManagementCiphers = new BitSet();
+        allowedSuiteBCiphers = new BitSet();
+        wepKeys = new String[4];
+        for (int i = 0; i < wepKeys.length; i++) {
+            wepKeys[i] = null;
+        }
+        enterpriseConfig = new WifiEnterpriseConfig();
+        selfAdded = false;
+        didSelfAdd = false;
+        ephemeral = false;
+        osu = false;
+        trusted = true; // Networks are considered trusted by default.
+        fromWifiNetworkSuggestion = false;
+        fromWifiNetworkSpecifier = false;
+        meteredHint = false;
+        meteredOverride = METERED_OVERRIDE_NONE;
+        useExternalScores = false;
+        validatedInternetAccess = false;
+        mIpConfiguration = new IpConfiguration();
+        lastUpdateUid = -1;
+        creatorUid = -1;
+        shared = true;
+        dtimInterval = 0;
+        mRandomizedMacAddress = MacAddress.fromString(WifiInfo.DEFAULT_MAC_ADDRESS);
+    }
+
+    /**
+     * Identify if this configuration represents a Passpoint network
+     */
+    public boolean isPasspoint() {
+        return !TextUtils.isEmpty(FQDN)
+                && !TextUtils.isEmpty(providerFriendlyName)
+                && enterpriseConfig != null
+                && enterpriseConfig.getEapMethod() != WifiEnterpriseConfig.Eap.NONE;
+    }
+
+    /**
+     * Helper function, identify if a configuration is linked
+     * @hide
+     */
+    public boolean isLinked(WifiConfiguration config) {
+        if (config != null) {
+            if (config.linkedConfigurations != null && linkedConfigurations != null) {
+                if (config.linkedConfigurations.get(configKey()) != null
+                        && linkedConfigurations.get(config.configKey()) != null) {
+                    return true;
+                }
+            }
+        }
+        return  false;
+    }
+
+    /**
+     * Helper function, idenfity if a configuration should be treated as an enterprise network
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public boolean isEnterprise() {
+        return (allowedKeyManagement.get(KeyMgmt.WPA_EAP)
+                || allowedKeyManagement.get(KeyMgmt.IEEE8021X)
+                || allowedKeyManagement.get(KeyMgmt.SUITE_B_192))
+                && enterpriseConfig != null
+                && enterpriseConfig.getEapMethod() != WifiEnterpriseConfig.Eap.NONE;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sbuf = new StringBuilder();
+        if (this.status == WifiConfiguration.Status.CURRENT) {
+            sbuf.append("* ");
+        } else if (this.status == WifiConfiguration.Status.DISABLED) {
+            sbuf.append("- DSBLE ");
+        }
+        sbuf.append("ID: ").append(this.networkId).append(" SSID: ").append(this.SSID).
+                append(" PROVIDER-NAME: ").append(this.providerFriendlyName).
+                append(" BSSID: ").append(this.BSSID).append(" FQDN: ").append(this.FQDN)
+                .append(" PRIO: ").append(this.priority)
+                .append(" HIDDEN: ").append(this.hiddenSSID)
+                .append(" PMF: ").append(this.requirePMF)
+                .append('\n');
+
+
+        sbuf.append(" NetworkSelectionStatus ")
+                .append(mNetworkSelectionStatus.getNetworkStatusString() + "\n");
+        if (mNetworkSelectionStatus.getNetworkSelectionDisableReason() > 0) {
+            sbuf.append(" mNetworkSelectionDisableReason ")
+                    .append(mNetworkSelectionStatus.getNetworkDisableReasonString() + "\n");
+
+            for (int index = mNetworkSelectionStatus.NETWORK_SELECTION_ENABLE;
+                    index < mNetworkSelectionStatus.NETWORK_SELECTION_DISABLED_MAX; index++) {
+                if (mNetworkSelectionStatus.getDisableReasonCounter(index) != 0) {
+                    sbuf.append(NetworkSelectionStatus.getNetworkDisableReasonString(index)
+                            + " counter:" + mNetworkSelectionStatus.getDisableReasonCounter(index)
+                            + "\n");
+                }
+            }
+        }
+        if (mNetworkSelectionStatus.getConnectChoice() != null) {
+            sbuf.append(" connect choice: ").append(mNetworkSelectionStatus.getConnectChoice());
+            sbuf.append(" connect choice set time: ")
+                    .append(TimeUtils.logTimeOfDay(
+                            mNetworkSelectionStatus.getConnectChoiceTimestamp()));
+        }
+        sbuf.append(" hasEverConnected: ")
+                .append(mNetworkSelectionStatus.getHasEverConnected()).append("\n");
+
+        if (this.numAssociation > 0) {
+            sbuf.append(" numAssociation ").append(this.numAssociation).append("\n");
+        }
+        if (this.numNoInternetAccessReports > 0) {
+            sbuf.append(" numNoInternetAccessReports ");
+            sbuf.append(this.numNoInternetAccessReports).append("\n");
+        }
+        if (this.updateTime != null) {
+            sbuf.append(" update ").append(this.updateTime).append("\n");
+        }
+        if (this.creationTime != null) {
+            sbuf.append(" creation ").append(this.creationTime).append("\n");
+        }
+        if (this.didSelfAdd) sbuf.append(" didSelfAdd");
+        if (this.selfAdded) sbuf.append(" selfAdded");
+        if (this.validatedInternetAccess) sbuf.append(" validatedInternetAccess");
+        if (this.ephemeral) sbuf.append(" ephemeral");
+        if (this.osu) sbuf.append(" osu");
+        if (this.trusted) sbuf.append(" trusted");
+        if (this.fromWifiNetworkSuggestion) sbuf.append(" fromWifiNetworkSuggestion");
+        if (this.fromWifiNetworkSpecifier) sbuf.append(" fromWifiNetworkSpecifier");
+        if (this.meteredHint) sbuf.append(" meteredHint");
+        if (this.useExternalScores) sbuf.append(" useExternalScores");
+        if (this.didSelfAdd || this.selfAdded || this.validatedInternetAccess
+                || this.ephemeral || this.trusted || this.fromWifiNetworkSuggestion
+                || this.fromWifiNetworkSpecifier || this.meteredHint || this.useExternalScores) {
+            sbuf.append("\n");
+        }
+        if (this.meteredOverride != METERED_OVERRIDE_NONE) {
+            sbuf.append(" meteredOverride ").append(meteredOverride).append("\n");
+        }
+        sbuf.append(" macRandomizationSetting: ").append(macRandomizationSetting).append("\n");
+        sbuf.append(" mRandomizedMacAddress: ").append(mRandomizedMacAddress).append("\n");
+        sbuf.append(" KeyMgmt:");
+        for (int k = 0; k < this.allowedKeyManagement.size(); k++) {
+            if (this.allowedKeyManagement.get(k)) {
+                sbuf.append(" ");
+                if (k < KeyMgmt.strings.length) {
+                    sbuf.append(KeyMgmt.strings[k]);
+                } else {
+                    sbuf.append("??");
+                }
+            }
+        }
+        sbuf.append(" Protocols:");
+        for (int p = 0; p < this.allowedProtocols.size(); p++) {
+            if (this.allowedProtocols.get(p)) {
+                sbuf.append(" ");
+                if (p < Protocol.strings.length) {
+                    sbuf.append(Protocol.strings[p]);
+                } else {
+                    sbuf.append("??");
+                }
+            }
+        }
+        sbuf.append('\n');
+        sbuf.append(" AuthAlgorithms:");
+        for (int a = 0; a < this.allowedAuthAlgorithms.size(); a++) {
+            if (this.allowedAuthAlgorithms.get(a)) {
+                sbuf.append(" ");
+                if (a < AuthAlgorithm.strings.length) {
+                    sbuf.append(AuthAlgorithm.strings[a]);
+                } else {
+                    sbuf.append("??");
+                }
+            }
+        }
+        sbuf.append('\n');
+        sbuf.append(" PairwiseCiphers:");
+        for (int pc = 0; pc < this.allowedPairwiseCiphers.size(); pc++) {
+            if (this.allowedPairwiseCiphers.get(pc)) {
+                sbuf.append(" ");
+                if (pc < PairwiseCipher.strings.length) {
+                    sbuf.append(PairwiseCipher.strings[pc]);
+                } else {
+                    sbuf.append("??");
+                }
+            }
+        }
+        sbuf.append('\n');
+        sbuf.append(" GroupCiphers:");
+        for (int gc = 0; gc < this.allowedGroupCiphers.size(); gc++) {
+            if (this.allowedGroupCiphers.get(gc)) {
+                sbuf.append(" ");
+                if (gc < GroupCipher.strings.length) {
+                    sbuf.append(GroupCipher.strings[gc]);
+                } else {
+                    sbuf.append("??");
+                }
+            }
+        }
+        sbuf.append('\n');
+        sbuf.append(" GroupMgmtCiphers:");
+        for (int gmc = 0; gmc < this.allowedGroupManagementCiphers.size(); gmc++) {
+            if (this.allowedGroupManagementCiphers.get(gmc)) {
+                sbuf.append(" ");
+                if (gmc < GroupMgmtCipher.strings.length) {
+                    sbuf.append(GroupMgmtCipher.strings[gmc]);
+                } else {
+                    sbuf.append("??");
+                }
+            }
+        }
+        sbuf.append('\n');
+        sbuf.append(" SuiteBCiphers:");
+        for (int sbc = 0; sbc < this.allowedSuiteBCiphers.size(); sbc++) {
+            if (this.allowedSuiteBCiphers.get(sbc)) {
+                sbuf.append(" ");
+                if (sbc < SuiteBCipher.strings.length) {
+                    sbuf.append(SuiteBCipher.strings[sbc]);
+                } else {
+                    sbuf.append("??");
+                }
+            }
+        }
+        sbuf.append('\n').append(" PSK/SAE: ");
+        if (this.preSharedKey != null) {
+            sbuf.append('*');
+        }
+
+        sbuf.append("\nEnterprise config:\n");
+        sbuf.append(enterpriseConfig);
+
+        sbuf.append("IP config:\n");
+        sbuf.append(mIpConfiguration.toString());
+
+        if (mNetworkSelectionStatus.getNetworkSelectionBSSID() != null) {
+            sbuf.append(" networkSelectionBSSID="
+                    + mNetworkSelectionStatus.getNetworkSelectionBSSID());
+        }
+        long now_ms = SystemClock.elapsedRealtime();
+        if (mNetworkSelectionStatus.getDisableTime() != NetworkSelectionStatus
+                .INVALID_NETWORK_SELECTION_DISABLE_TIMESTAMP) {
+            sbuf.append('\n');
+            long diff = now_ms - mNetworkSelectionStatus.getDisableTime();
+            if (diff <= 0) {
+                sbuf.append(" blackListed since <incorrect>");
+            } else {
+                sbuf.append(" blackListed: ").append(Long.toString(diff / 1000)).append("sec ");
+            }
+        }
+        if (creatorUid != 0) sbuf.append(" cuid=" + creatorUid);
+        if (creatorName != null) sbuf.append(" cname=" + creatorName);
+        if (lastUpdateUid != 0) sbuf.append(" luid=" + lastUpdateUid);
+        if (lastUpdateName != null) sbuf.append(" lname=" + lastUpdateName);
+        if (updateIdentifier != null) sbuf.append(" updateIdentifier=" + updateIdentifier);
+        sbuf.append(" lcuid=" + lastConnectUid);
+        sbuf.append(" userApproved=" + userApprovedAsString(userApproved));
+        sbuf.append(" noInternetAccessExpected=" + noInternetAccessExpected);
+        sbuf.append(" ");
+
+        if (this.lastConnected != 0) {
+            sbuf.append('\n');
+            sbuf.append("lastConnected: ").append(TimeUtils.logTimeOfDay(this.lastConnected));
+            sbuf.append(" ");
+        }
+        sbuf.append('\n');
+        if (this.linkedConfigurations != null) {
+            for (String key : this.linkedConfigurations.keySet()) {
+                sbuf.append(" linked: ").append(key);
+                sbuf.append('\n');
+            }
+        }
+        sbuf.append("recentFailure: ").append("Association Rejection code: ")
+                .append(recentFailure.getAssociationStatus()).append("\n");
+        return sbuf.toString();
+    }
+
+    /** {@hide} */
+    @UnsupportedAppUsage
+    public String getPrintableSsid() {
+        if (SSID == null) return "";
+        final int length = SSID.length();
+        if (length > 2 && (SSID.charAt(0) == '"') && SSID.charAt(length - 1) == '"') {
+            return SSID.substring(1, length - 1);
+        }
+
+        /** The ascii-encoded string format is P"<ascii-encoded-string>"
+         * The decoding is implemented in the supplicant for a newly configured
+         * network.
+         */
+        if (length > 3 && (SSID.charAt(0) == 'P') && (SSID.charAt(1) == '"') &&
+                (SSID.charAt(length-1) == '"')) {
+            WifiSsid wifiSsid = WifiSsid.createFromAsciiEncoded(
+                    SSID.substring(2, length - 1));
+            return wifiSsid.toString();
+        }
+        return SSID;
+    }
+
+    /** @hide **/
+    public static String userApprovedAsString(int userApproved) {
+        switch (userApproved) {
+            case USER_APPROVED:
+                return "USER_APPROVED";
+            case USER_BANNED:
+                return "USER_BANNED";
+            case USER_UNSPECIFIED:
+                return "USER_UNSPECIFIED";
+            default:
+                return "INVALID";
+        }
+    }
+
+    /**
+     * Get an identifier for associating credentials with this config
+     * @param current configuration contains values for additional fields
+     *                that are not part of this configuration. Used
+     *                when a config with some fields is passed by an application.
+     * @throws IllegalStateException if config is invalid for key id generation
+     * @hide
+     */
+    public String getKeyIdForCredentials(WifiConfiguration current) {
+        String keyMgmt = "";
+
+        try {
+            // Get current config details for fields that are not initialized
+            if (TextUtils.isEmpty(SSID)) SSID = current.SSID;
+            if (allowedKeyManagement.cardinality() == 0) {
+                allowedKeyManagement = current.allowedKeyManagement;
+            }
+            if (allowedKeyManagement.get(KeyMgmt.WPA_EAP)) {
+                keyMgmt += KeyMgmt.strings[KeyMgmt.WPA_EAP];
+            }
+            if (allowedKeyManagement.get(KeyMgmt.OSEN)) {
+                keyMgmt += KeyMgmt.strings[KeyMgmt.OSEN];
+            }
+            if (allowedKeyManagement.get(KeyMgmt.IEEE8021X)) {
+                keyMgmt += KeyMgmt.strings[KeyMgmt.IEEE8021X];
+            }
+            if (allowedKeyManagement.get(KeyMgmt.SUITE_B_192)) {
+                keyMgmt += KeyMgmt.strings[KeyMgmt.SUITE_B_192];
+            }
+
+            if (TextUtils.isEmpty(keyMgmt)) {
+                throw new IllegalStateException("Not an EAP network");
+            }
+
+            return trimStringForKeyId(SSID) + "_" + keyMgmt + "_" +
+                    trimStringForKeyId(enterpriseConfig.getKeyId(current != null ?
+                            current.enterpriseConfig : null));
+        } catch (NullPointerException e) {
+            throw new IllegalStateException("Invalid config details");
+        }
+    }
+
+    private String trimStringForKeyId(String string) {
+        // Remove quotes and spaces
+        return string.replace("\"", "").replace(" ", "");
+    }
+
+    private static BitSet readBitSet(Parcel src) {
+        int cardinality = src.readInt();
+
+        BitSet set = new BitSet();
+        for (int i = 0; i < cardinality; i++) {
+            set.set(src.readInt());
+        }
+
+        return set;
+    }
+
+    private static void writeBitSet(Parcel dest, BitSet set) {
+        int nextSetBit = -1;
+
+        dest.writeInt(set.cardinality());
+
+        while ((nextSetBit = set.nextSetBit(nextSetBit + 1)) != -1) {
+            dest.writeInt(nextSetBit);
+        }
+    }
+
+    /** @hide */
+    @UnsupportedAppUsage
+    public int getAuthType() {
+        if (allowedKeyManagement.cardinality() > 1) {
+            throw new IllegalStateException("More than one auth type set");
+        }
+        if (allowedKeyManagement.get(KeyMgmt.WPA_PSK)) {
+            return KeyMgmt.WPA_PSK;
+        } else if (allowedKeyManagement.get(KeyMgmt.WPA2_PSK)) {
+            return KeyMgmt.WPA2_PSK;
+        } else if (allowedKeyManagement.get(KeyMgmt.WPA_EAP)) {
+            return KeyMgmt.WPA_EAP;
+        } else if (allowedKeyManagement.get(KeyMgmt.IEEE8021X)) {
+            return KeyMgmt.IEEE8021X;
+        } else if (allowedKeyManagement.get(KeyMgmt.SAE)) {
+            return KeyMgmt.SAE;
+        } else if (allowedKeyManagement.get(KeyMgmt.OWE)) {
+            return KeyMgmt.OWE;
+        } else if (allowedKeyManagement.get(KeyMgmt.SUITE_B_192)) {
+            return KeyMgmt.SUITE_B_192;
+        }
+        return KeyMgmt.NONE;
+    }
+
+    /* @hide
+     * Cache the config key, this seems useful as a speed up since a lot of
+     * lookups in the config store are done and based on this key.
+     */
+    String mCachedConfigKey;
+
+    /** @hide
+     *  return the string used to calculate the hash in WifiConfigStore
+     *  and uniquely identify this WifiConfiguration
+     */
+    public String configKey(boolean allowCached) {
+        String key;
+        if (allowCached && mCachedConfigKey != null) {
+            key = mCachedConfigKey;
+        } else if (providerFriendlyName != null) {
+            key = FQDN + KeyMgmt.strings[KeyMgmt.WPA_EAP];
+            if (!shared) {
+                key += "-" + Integer.toString(UserHandle.getUserId(creatorUid));
+            }
+        } else {
+            key = getSsidAndSecurityTypeString();
+            if (!shared) {
+                key += "-" + Integer.toString(UserHandle.getUserId(creatorUid));
+            }
+            mCachedConfigKey = key;
+        }
+        return key;
+    }
+
+    /** @hide
+     *  return the SSID + security type in String format.
+     */
+    public String getSsidAndSecurityTypeString() {
+        String key;
+        if (allowedKeyManagement.get(KeyMgmt.WPA_PSK)) {
+            key = SSID + KeyMgmt.strings[KeyMgmt.WPA_PSK];
+        } else if (allowedKeyManagement.get(KeyMgmt.WPA_EAP)
+                || allowedKeyManagement.get(KeyMgmt.IEEE8021X)) {
+            key = SSID + KeyMgmt.strings[KeyMgmt.WPA_EAP];
+        } else if (wepKeys[0] != null) {
+            key = SSID + "WEP";
+        } else if (allowedKeyManagement.get(KeyMgmt.OWE)) {
+            key = SSID + KeyMgmt.strings[KeyMgmt.OWE];
+        } else if (allowedKeyManagement.get(KeyMgmt.SAE)) {
+            key = SSID + KeyMgmt.strings[KeyMgmt.SAE];
+        } else if (allowedKeyManagement.get(KeyMgmt.SUITE_B_192)) {
+            key = SSID + KeyMgmt.strings[KeyMgmt.SUITE_B_192];
+        } else {
+            key = SSID + KeyMgmt.strings[KeyMgmt.NONE];
+        }
+        return key;
+    }
+
+    /** @hide
+     * get configKey, force calculating the config string
+     */
+    public String configKey() {
+        return configKey(false);
+    }
+
+    /** @hide */
+    @UnsupportedAppUsage
+    public IpConfiguration getIpConfiguration() {
+        return mIpConfiguration;
+    }
+
+    /** @hide */
+    @UnsupportedAppUsage
+    public void setIpConfiguration(IpConfiguration ipConfiguration) {
+        if (ipConfiguration == null) ipConfiguration = new IpConfiguration();
+        mIpConfiguration = ipConfiguration;
+    }
+
+    /** @hide */
+    @UnsupportedAppUsage
+    public StaticIpConfiguration getStaticIpConfiguration() {
+        return mIpConfiguration.getStaticIpConfiguration();
+    }
+
+    /** @hide */
+    @UnsupportedAppUsage
+    public void setStaticIpConfiguration(StaticIpConfiguration staticIpConfiguration) {
+        mIpConfiguration.setStaticIpConfiguration(staticIpConfiguration);
+    }
+
+    /** @hide */
+    @UnsupportedAppUsage
+    public IpConfiguration.IpAssignment getIpAssignment() {
+        return mIpConfiguration.ipAssignment;
+    }
+
+    /** @hide */
+    @UnsupportedAppUsage
+    public void setIpAssignment(IpConfiguration.IpAssignment ipAssignment) {
+        mIpConfiguration.ipAssignment = ipAssignment;
+    }
+
+    /** @hide */
+    @UnsupportedAppUsage
+    public IpConfiguration.ProxySettings getProxySettings() {
+        return mIpConfiguration.proxySettings;
+    }
+
+    /** @hide */
+    @UnsupportedAppUsage
+    public void setProxySettings(IpConfiguration.ProxySettings proxySettings) {
+        mIpConfiguration.proxySettings = proxySettings;
+    }
+
+    /**
+     * Returns the HTTP proxy used by this object.
+     * @return a {@link ProxyInfo httpProxy} representing the proxy specified by this
+     *                  WifiConfiguration, or {@code null} if no proxy is specified.
+     */
+    public ProxyInfo getHttpProxy() {
+        if (mIpConfiguration.proxySettings == IpConfiguration.ProxySettings.NONE) {
+            return null;
+        }
+        return new ProxyInfo(mIpConfiguration.httpProxy);
+    }
+
+    /**
+     * Set the {@link ProxyInfo} for this WifiConfiguration. This method should only be used by a
+     * device owner or profile owner. When other apps attempt to save a {@link WifiConfiguration}
+     * with modified proxy settings, the methods {@link WifiManager#addNetwork} and
+     * {@link WifiManager#updateNetwork} fail and return {@code -1}.
+     *
+     * @param httpProxy {@link ProxyInfo} representing the httpProxy to be used by this
+     *                  WifiConfiguration. Setting this to {@code null} will explicitly set no
+     *                  proxy, removing any proxy that was previously set.
+     * @exception IllegalArgumentException for invalid httpProxy
+     */
+    public void setHttpProxy(ProxyInfo httpProxy) {
+        if (httpProxy == null) {
+            mIpConfiguration.setProxySettings(IpConfiguration.ProxySettings.NONE);
+            mIpConfiguration.setHttpProxy(null);
+            return;
+        }
+        ProxyInfo httpProxyCopy;
+        ProxySettings proxySettingCopy;
+        if (!Uri.EMPTY.equals(httpProxy.getPacFileUrl())) {
+            proxySettingCopy = IpConfiguration.ProxySettings.PAC;
+            // Construct a new PAC URL Proxy
+            httpProxyCopy = new ProxyInfo(httpProxy.getPacFileUrl(), httpProxy.getPort());
+        } else {
+            proxySettingCopy = IpConfiguration.ProxySettings.STATIC;
+            // Construct a new HTTP Proxy
+            httpProxyCopy = new ProxyInfo(httpProxy.getHost(), httpProxy.getPort(),
+                    httpProxy.getExclusionListAsString());
+        }
+        if (!httpProxyCopy.isValid()) {
+            throw new IllegalArgumentException("Invalid ProxyInfo: " + httpProxyCopy.toString());
+        }
+        mIpConfiguration.setProxySettings(proxySettingCopy);
+        mIpConfiguration.setHttpProxy(httpProxyCopy);
+    }
+
+    /** @hide */
+    @UnsupportedAppUsage
+    public void setProxy(ProxySettings settings, ProxyInfo proxy) {
+        mIpConfiguration.proxySettings = settings;
+        mIpConfiguration.httpProxy = proxy;
+    }
+
+    /** Implement the Parcelable interface {@hide} */
+    public int describeContents() {
+        return 0;
+    }
+
+    /** @hide */
+    public void setPasspointManagementObjectTree(String passpointManagementObjectTree) {
+        mPasspointManagementObjectTree = passpointManagementObjectTree;
+    }
+
+    /** @hide */
+    public String getMoTree() {
+        return mPasspointManagementObjectTree;
+    }
+
+    /** copy constructor {@hide} */
+    @UnsupportedAppUsage
+    public WifiConfiguration(WifiConfiguration source) {
+        if (source != null) {
+            networkId = source.networkId;
+            status = source.status;
+            SSID = source.SSID;
+            BSSID = source.BSSID;
+            FQDN = source.FQDN;
+            roamingConsortiumIds = source.roamingConsortiumIds.clone();
+            providerFriendlyName = source.providerFriendlyName;
+            isHomeProviderNetwork = source.isHomeProviderNetwork;
+            preSharedKey = source.preSharedKey;
+
+            mNetworkSelectionStatus.copy(source.getNetworkSelectionStatus());
+            apBand = source.apBand;
+            apChannel = source.apChannel;
+
+            wepKeys = new String[4];
+            for (int i = 0; i < wepKeys.length; i++) {
+                wepKeys[i] = source.wepKeys[i];
+            }
+
+            wepTxKeyIndex = source.wepTxKeyIndex;
+            priority = source.priority;
+            hiddenSSID = source.hiddenSSID;
+            allowedKeyManagement   = (BitSet) source.allowedKeyManagement.clone();
+            allowedProtocols       = (BitSet) source.allowedProtocols.clone();
+            allowedAuthAlgorithms  = (BitSet) source.allowedAuthAlgorithms.clone();
+            allowedPairwiseCiphers = (BitSet) source.allowedPairwiseCiphers.clone();
+            allowedGroupCiphers    = (BitSet) source.allowedGroupCiphers.clone();
+            allowedGroupManagementCiphers = (BitSet) source.allowedGroupManagementCiphers.clone();
+            allowedSuiteBCiphers    = (BitSet) source.allowedSuiteBCiphers.clone();
+            enterpriseConfig = new WifiEnterpriseConfig(source.enterpriseConfig);
+
+            defaultGwMacAddress = source.defaultGwMacAddress;
+
+            mIpConfiguration = new IpConfiguration(source.mIpConfiguration);
+
+            if ((source.linkedConfigurations != null)
+                    && (source.linkedConfigurations.size() > 0)) {
+                linkedConfigurations = new HashMap<String, Integer>();
+                linkedConfigurations.putAll(source.linkedConfigurations);
+            }
+            mCachedConfigKey = null; //force null configKey
+            selfAdded = source.selfAdded;
+            validatedInternetAccess = source.validatedInternetAccess;
+            isLegacyPasspointConfig = source.isLegacyPasspointConfig;
+            ephemeral = source.ephemeral;
+            osu = source.osu;
+            trusted = source.trusted;
+            fromWifiNetworkSuggestion = source.fromWifiNetworkSuggestion;
+            fromWifiNetworkSpecifier = source.fromWifiNetworkSpecifier;
+            meteredHint = source.meteredHint;
+            meteredOverride = source.meteredOverride;
+            useExternalScores = source.useExternalScores;
+
+            didSelfAdd = source.didSelfAdd;
+            lastConnectUid = source.lastConnectUid;
+            lastUpdateUid = source.lastUpdateUid;
+            creatorUid = source.creatorUid;
+            creatorName = source.creatorName;
+            lastUpdateName = source.lastUpdateName;
+            peerWifiConfiguration = source.peerWifiConfiguration;
+
+            lastConnected = source.lastConnected;
+            lastDisconnected = source.lastDisconnected;
+            numScorerOverride = source.numScorerOverride;
+            numScorerOverrideAndSwitchedNetwork = source.numScorerOverrideAndSwitchedNetwork;
+            numAssociation = source.numAssociation;
+            userApproved = source.userApproved;
+            numNoInternetAccessReports = source.numNoInternetAccessReports;
+            noInternetAccessExpected = source.noInternetAccessExpected;
+            creationTime = source.creationTime;
+            updateTime = source.updateTime;
+            shared = source.shared;
+            recentFailure.setAssociationStatus(source.recentFailure.getAssociationStatus());
+            mRandomizedMacAddress = source.mRandomizedMacAddress;
+            macRandomizationSetting = source.macRandomizationSetting;
+            requirePMF = source.requirePMF;
+            updateIdentifier = source.updateIdentifier;
+        }
+    }
+
+    /** Implement the Parcelable interface {@hide} */
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(networkId);
+        dest.writeInt(status);
+        mNetworkSelectionStatus.writeToParcel(dest);
+        dest.writeString(SSID);
+        dest.writeString(BSSID);
+        dest.writeInt(apBand);
+        dest.writeInt(apChannel);
+        dest.writeString(FQDN);
+        dest.writeString(providerFriendlyName);
+        dest.writeInt(isHomeProviderNetwork ? 1 : 0);
+        dest.writeInt(roamingConsortiumIds.length);
+        for (long roamingConsortiumId : roamingConsortiumIds) {
+            dest.writeLong(roamingConsortiumId);
+        }
+        dest.writeString(preSharedKey);
+        for (String wepKey : wepKeys) {
+            dest.writeString(wepKey);
+        }
+        dest.writeInt(wepTxKeyIndex);
+        dest.writeInt(priority);
+        dest.writeInt(hiddenSSID ? 1 : 0);
+        dest.writeInt(requirePMF ? 1 : 0);
+        dest.writeString(updateIdentifier);
+
+        writeBitSet(dest, allowedKeyManagement);
+        writeBitSet(dest, allowedProtocols);
+        writeBitSet(dest, allowedAuthAlgorithms);
+        writeBitSet(dest, allowedPairwiseCiphers);
+        writeBitSet(dest, allowedGroupCiphers);
+        writeBitSet(dest, allowedGroupManagementCiphers);
+        writeBitSet(dest, allowedSuiteBCiphers);
+
+        dest.writeParcelable(enterpriseConfig, flags);
+
+        dest.writeParcelable(mIpConfiguration, flags);
+        dest.writeString(dhcpServer);
+        dest.writeString(defaultGwMacAddress);
+        dest.writeInt(selfAdded ? 1 : 0);
+        dest.writeInt(didSelfAdd ? 1 : 0);
+        dest.writeInt(validatedInternetAccess ? 1 : 0);
+        dest.writeInt(isLegacyPasspointConfig ? 1 : 0);
+        dest.writeInt(ephemeral ? 1 : 0);
+        dest.writeInt(trusted ? 1 : 0);
+        dest.writeInt(fromWifiNetworkSuggestion ? 1 : 0);
+        dest.writeInt(fromWifiNetworkSpecifier ? 1 : 0);
+        dest.writeInt(meteredHint ? 1 : 0);
+        dest.writeInt(meteredOverride);
+        dest.writeInt(useExternalScores ? 1 : 0);
+        dest.writeInt(creatorUid);
+        dest.writeInt(lastConnectUid);
+        dest.writeInt(lastUpdateUid);
+        dest.writeString(creatorName);
+        dest.writeString(lastUpdateName);
+        dest.writeInt(numScorerOverride);
+        dest.writeInt(numScorerOverrideAndSwitchedNetwork);
+        dest.writeInt(numAssociation);
+        dest.writeInt(userApproved);
+        dest.writeInt(numNoInternetAccessReports);
+        dest.writeInt(noInternetAccessExpected ? 1 : 0);
+        dest.writeInt(shared ? 1 : 0);
+        dest.writeString(mPasspointManagementObjectTree);
+        dest.writeInt(recentFailure.getAssociationStatus());
+        dest.writeParcelable(mRandomizedMacAddress, flags);
+        dest.writeInt(macRandomizationSetting);
+        dest.writeInt(osu ? 1 : 0);
+    }
+
+    /** Implement the Parcelable interface {@hide} */
+    @UnsupportedAppUsage
+    public static final @android.annotation.NonNull Creator<WifiConfiguration> CREATOR =
+        new Creator<WifiConfiguration>() {
+            public WifiConfiguration createFromParcel(Parcel in) {
+                WifiConfiguration config = new WifiConfiguration();
+                config.networkId = in.readInt();
+                config.status = in.readInt();
+                config.mNetworkSelectionStatus.readFromParcel(in);
+                config.SSID = in.readString();
+                config.BSSID = in.readString();
+                config.apBand = in.readInt();
+                config.apChannel = in.readInt();
+                config.FQDN = in.readString();
+                config.providerFriendlyName = in.readString();
+                config.isHomeProviderNetwork = in.readInt() != 0;
+                int numRoamingConsortiumIds = in.readInt();
+                config.roamingConsortiumIds = new long[numRoamingConsortiumIds];
+                for (int i = 0; i < numRoamingConsortiumIds; i++) {
+                    config.roamingConsortiumIds[i] = in.readLong();
+                }
+                config.preSharedKey = in.readString();
+                for (int i = 0; i < config.wepKeys.length; i++) {
+                    config.wepKeys[i] = in.readString();
+                }
+                config.wepTxKeyIndex = in.readInt();
+                config.priority = in.readInt();
+                config.hiddenSSID = in.readInt() != 0;
+                config.requirePMF = in.readInt() != 0;
+                config.updateIdentifier = in.readString();
+
+                config.allowedKeyManagement   = readBitSet(in);
+                config.allowedProtocols       = readBitSet(in);
+                config.allowedAuthAlgorithms  = readBitSet(in);
+                config.allowedPairwiseCiphers = readBitSet(in);
+                config.allowedGroupCiphers    = readBitSet(in);
+                config.allowedGroupManagementCiphers = readBitSet(in);
+                config.allowedSuiteBCiphers   = readBitSet(in);
+
+                config.enterpriseConfig = in.readParcelable(null);
+                config.setIpConfiguration(in.readParcelable(null));
+                config.dhcpServer = in.readString();
+                config.defaultGwMacAddress = in.readString();
+                config.selfAdded = in.readInt() != 0;
+                config.didSelfAdd = in.readInt() != 0;
+                config.validatedInternetAccess = in.readInt() != 0;
+                config.isLegacyPasspointConfig = in.readInt() != 0;
+                config.ephemeral = in.readInt() != 0;
+                config.trusted = in.readInt() != 0;
+                config.fromWifiNetworkSuggestion =  in.readInt() != 0;
+                config.fromWifiNetworkSpecifier =  in.readInt() != 0;
+                config.meteredHint = in.readInt() != 0;
+                config.meteredOverride = in.readInt();
+                config.useExternalScores = in.readInt() != 0;
+                config.creatorUid = in.readInt();
+                config.lastConnectUid = in.readInt();
+                config.lastUpdateUid = in.readInt();
+                config.creatorName = in.readString();
+                config.lastUpdateName = in.readString();
+                config.numScorerOverride = in.readInt();
+                config.numScorerOverrideAndSwitchedNetwork = in.readInt();
+                config.numAssociation = in.readInt();
+                config.userApproved = in.readInt();
+                config.numNoInternetAccessReports = in.readInt();
+                config.noInternetAccessExpected = in.readInt() != 0;
+                config.shared = in.readInt() != 0;
+                config.mPasspointManagementObjectTree = in.readString();
+                config.recentFailure.setAssociationStatus(in.readInt());
+                config.mRandomizedMacAddress = in.readParcelable(null);
+                config.macRandomizationSetting = in.readInt();
+                config.osu = in.readInt() != 0;
+                return config;
+            }
+
+            public WifiConfiguration[] newArray(int size) {
+                return new WifiConfiguration[size];
+            }
+        };
+
+    /**
+     * Serializes the object for backup
+     * @hide
+     */
+    public byte[] getBytesForBackup() throws IOException {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        DataOutputStream out = new DataOutputStream(baos);
+
+        out.writeInt(BACKUP_VERSION);
+        BackupUtils.writeString(out, SSID);
+        out.writeInt(apBand);
+        out.writeInt(apChannel);
+        BackupUtils.writeString(out, preSharedKey);
+        out.writeInt(getAuthType());
+        out.writeBoolean(hiddenSSID);
+        return baos.toByteArray();
+    }
+
+    /**
+     * Deserializes a byte array into the WiFiConfiguration Object
+     * @hide
+     */
+    public static WifiConfiguration getWifiConfigFromBackup(DataInputStream in) throws IOException,
+            BackupUtils.BadVersionException {
+        WifiConfiguration config = new WifiConfiguration();
+        int version = in.readInt();
+        if (version < 1 || version > BACKUP_VERSION) {
+            throw new BackupUtils.BadVersionException("Unknown Backup Serialization Version");
+        }
+
+        if (version == 1) return null; // Version 1 is a bad dataset.
+
+        config.SSID = BackupUtils.readString(in);
+        config.apBand = in.readInt();
+        config.apChannel = in.readInt();
+        config.preSharedKey = BackupUtils.readString(in);
+        config.allowedKeyManagement.set(in.readInt());
+        if (version >= 3) {
+            config.hiddenSSID = in.readBoolean();
+        }
+        return config;
+    }
+}
diff --git a/android/net/wifi/WifiEnterpriseConfig.java b/android/net/wifi/WifiEnterpriseConfig.java
new file mode 100644
index 0000000..9d4b837
--- /dev/null
+++ b/android/net/wifi/WifiEnterpriseConfig.java
@@ -0,0 +1,1193 @@
+/*
+ * Copyright (C) 2013 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.wifi;
+
+import android.annotation.Nullable;
+import android.annotation.UnsupportedAppUsage;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.security.Credentials;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.nio.charset.StandardCharsets;
+import java.security.PrivateKey;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Enterprise configuration details for Wi-Fi. Stores details about the EAP method
+ * and any associated credentials.
+ */
+public class WifiEnterpriseConfig implements Parcelable {
+
+    /** @hide */
+    public static final String EMPTY_VALUE         = "NULL";
+    /** @hide */
+    public static final String EAP_KEY             = "eap";
+    /** @hide */
+    public static final String PHASE2_KEY          = "phase2";
+    /** @hide */
+    public static final String IDENTITY_KEY        = "identity";
+    /** @hide */
+    public static final String ANON_IDENTITY_KEY   = "anonymous_identity";
+    /** @hide */
+    public static final String PASSWORD_KEY        = "password";
+    /** @hide */
+    public static final String SUBJECT_MATCH_KEY   = "subject_match";
+    /** @hide */
+    public static final String ALTSUBJECT_MATCH_KEY = "altsubject_match";
+    /** @hide */
+    public static final String DOM_SUFFIX_MATCH_KEY = "domain_suffix_match";
+    /** @hide */
+    public static final String OPP_KEY_CACHING     = "proactive_key_caching";
+    /**
+     * String representing the keystore OpenSSL ENGINE's ID.
+     * @hide
+     */
+    public static final String ENGINE_ID_KEYSTORE = "keystore";
+
+    /**
+     * String representing the keystore URI used for wpa_supplicant.
+     * @hide
+     */
+    public static final String KEYSTORE_URI = "keystore://";
+
+    /**
+     * String representing the keystore URI used for wpa_supplicant,
+     * Unlike #KEYSTORE_URI, this supports a list of space-delimited aliases
+     * @hide
+     */
+    public static final String KEYSTORES_URI = "keystores://";
+
+    /**
+     * String to set the engine value to when it should be enabled.
+     * @hide
+     */
+    public static final String ENGINE_ENABLE = "1";
+
+    /**
+     * String to set the engine value to when it should be disabled.
+     * @hide
+     */
+    public static final String ENGINE_DISABLE = "0";
+
+    /** @hide */
+    public static final String CA_CERT_PREFIX = KEYSTORE_URI + Credentials.CA_CERTIFICATE;
+    /** @hide */
+    public static final String CLIENT_CERT_PREFIX = KEYSTORE_URI + Credentials.USER_CERTIFICATE;
+    /** @hide */
+    public static final String CLIENT_CERT_KEY     = "client_cert";
+    /** @hide */
+    public static final String CA_CERT_KEY         = "ca_cert";
+    /** @hide */
+    public static final String CA_PATH_KEY         = "ca_path";
+    /** @hide */
+    public static final String ENGINE_KEY          = "engine";
+    /** @hide */
+    public static final String ENGINE_ID_KEY       = "engine_id";
+    /** @hide */
+    public static final String PRIVATE_KEY_ID_KEY  = "key_id";
+    /** @hide */
+    public static final String REALM_KEY           = "realm";
+    /** @hide */
+    public static final String PLMN_KEY            = "plmn";
+    /** @hide */
+    public static final String CA_CERT_ALIAS_DELIMITER = " ";
+
+    // Fields to copy verbatim from wpa_supplicant.
+    private static final String[] SUPPLICANT_CONFIG_KEYS = new String[] {
+            IDENTITY_KEY,
+            ANON_IDENTITY_KEY,
+            PASSWORD_KEY,
+            CLIENT_CERT_KEY,
+            CA_CERT_KEY,
+            SUBJECT_MATCH_KEY,
+            ENGINE_KEY,
+            ENGINE_ID_KEY,
+            PRIVATE_KEY_ID_KEY,
+            ALTSUBJECT_MATCH_KEY,
+            DOM_SUFFIX_MATCH_KEY,
+            CA_PATH_KEY
+    };
+
+    /**
+     * Fields that have unquoted values in {@link #mFields}.
+     */
+    private static final List<String> UNQUOTED_KEYS = Arrays.asList(ENGINE_KEY, OPP_KEY_CACHING);
+
+    @UnsupportedAppUsage
+    private HashMap<String, String> mFields = new HashMap<String, String>();
+    private X509Certificate[] mCaCerts;
+    private PrivateKey mClientPrivateKey;
+    private X509Certificate[] mClientCertificateChain;
+    private int mEapMethod = Eap.NONE;
+    private int mPhase2Method = Phase2.NONE;
+    private boolean mIsAppInstalledDeviceKeyAndCert = false;
+    private boolean mIsAppInstalledCaCert = false;
+
+    private static final String TAG = "WifiEnterpriseConfig";
+
+    public WifiEnterpriseConfig() {
+        // Do not set defaults so that the enterprise fields that are not changed
+        // by API are not changed underneath
+        // This is essential because an app may not have all fields like password
+        // available. It allows modification of subset of fields.
+
+    }
+
+    /**
+     * Copy over the contents of the source WifiEnterpriseConfig object over to this object.
+     *
+     * @param source Source WifiEnterpriseConfig object.
+     * @param ignoreMaskedPassword Set to true to ignore masked password field, false otherwise.
+     * @param mask if |ignoreMaskedPassword| is set, check if the incoming password field is set
+     *             to this value.
+     */
+    private void copyFrom(WifiEnterpriseConfig source, boolean ignoreMaskedPassword, String mask) {
+        for (String key : source.mFields.keySet()) {
+            if (ignoreMaskedPassword && key.equals(PASSWORD_KEY)
+                    && TextUtils.equals(source.mFields.get(key), mask)) {
+                continue;
+            }
+            mFields.put(key, source.mFields.get(key));
+        }
+        if (source.mCaCerts != null) {
+            mCaCerts = Arrays.copyOf(source.mCaCerts, source.mCaCerts.length);
+        } else {
+            mCaCerts = null;
+        }
+        mClientPrivateKey = source.mClientPrivateKey;
+        if (source.mClientCertificateChain != null) {
+            mClientCertificateChain = Arrays.copyOf(
+                    source.mClientCertificateChain,
+                    source.mClientCertificateChain.length);
+        } else {
+            mClientCertificateChain = null;
+        }
+        mEapMethod = source.mEapMethod;
+        mPhase2Method = source.mPhase2Method;
+        mIsAppInstalledDeviceKeyAndCert = source.mIsAppInstalledDeviceKeyAndCert;
+        mIsAppInstalledCaCert = source.mIsAppInstalledCaCert;
+    }
+
+    /**
+     * Copy constructor.
+     * This copies over all the fields verbatim (does not ignore masked password fields).
+     *
+     * @param source Source WifiEnterpriseConfig object.
+     */
+    public WifiEnterpriseConfig(WifiEnterpriseConfig source) {
+        copyFrom(source, false, "");
+    }
+
+    /**
+     * Copy fields from the provided external WifiEnterpriseConfig.
+     * This is needed to handle the WifiEnterpriseConfig objects which were sent by apps with the
+     * password field masked.
+     *
+     * @param externalConfig External WifiEnterpriseConfig object.
+     * @param mask String mask to compare against.
+     * @hide
+     */
+    public void copyFromExternal(WifiEnterpriseConfig externalConfig, String mask) {
+        copyFrom(externalConfig, true, convertToQuotedString(mask));
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(mFields.size());
+        for (Map.Entry<String, String> entry : mFields.entrySet()) {
+            dest.writeString(entry.getKey());
+            dest.writeString(entry.getValue());
+        }
+
+        dest.writeInt(mEapMethod);
+        dest.writeInt(mPhase2Method);
+        ParcelUtil.writeCertificates(dest, mCaCerts);
+        ParcelUtil.writePrivateKey(dest, mClientPrivateKey);
+        ParcelUtil.writeCertificates(dest, mClientCertificateChain);
+        dest.writeBoolean(mIsAppInstalledDeviceKeyAndCert);
+        dest.writeBoolean(mIsAppInstalledCaCert);
+    }
+
+    public static final @android.annotation.NonNull Creator<WifiEnterpriseConfig> CREATOR =
+            new Creator<WifiEnterpriseConfig>() {
+                @Override
+                public WifiEnterpriseConfig createFromParcel(Parcel in) {
+                    WifiEnterpriseConfig enterpriseConfig = new WifiEnterpriseConfig();
+                    int count = in.readInt();
+                    for (int i = 0; i < count; i++) {
+                        String key = in.readString();
+                        String value = in.readString();
+                        enterpriseConfig.mFields.put(key, value);
+                    }
+
+                    enterpriseConfig.mEapMethod = in.readInt();
+                    enterpriseConfig.mPhase2Method = in.readInt();
+                    enterpriseConfig.mCaCerts = ParcelUtil.readCertificates(in);
+                    enterpriseConfig.mClientPrivateKey = ParcelUtil.readPrivateKey(in);
+                    enterpriseConfig.mClientCertificateChain = ParcelUtil.readCertificates(in);
+                    enterpriseConfig.mIsAppInstalledDeviceKeyAndCert = in.readBoolean();
+                    enterpriseConfig.mIsAppInstalledCaCert = in.readBoolean();
+                    return enterpriseConfig;
+                }
+
+                @Override
+                public WifiEnterpriseConfig[] newArray(int size) {
+                    return new WifiEnterpriseConfig[size];
+                }
+            };
+
+    /** The Extensible Authentication Protocol method used */
+    public static final class Eap {
+        /** No EAP method used. Represents an empty config */
+        public static final int NONE    = -1;
+        /** Protected EAP */
+        public static final int PEAP    = 0;
+        /** EAP-Transport Layer Security */
+        public static final int TLS     = 1;
+        /** EAP-Tunneled Transport Layer Security */
+        public static final int TTLS    = 2;
+        /** EAP-Password */
+        public static final int PWD     = 3;
+        /** EAP-Subscriber Identity Module [RFC-4186] */
+        public static final int SIM     = 4;
+        /** EAP-Authentication and Key Agreement [RFC-4187] */
+        public static final int AKA     = 5;
+        /** EAP-Authentication and Key Agreement Prime [RFC-5448] */
+        public static final int AKA_PRIME = 6;
+        /** Hotspot 2.0 r2 OSEN */
+        public static final int UNAUTH_TLS = 7;
+        /** @hide */
+        public static final String[] strings =
+                { "PEAP", "TLS", "TTLS", "PWD", "SIM", "AKA", "AKA'", "WFA-UNAUTH-TLS" };
+
+        /** Prevent initialization */
+        private Eap() {}
+    }
+
+    /** The inner authentication method used */
+    public static final class Phase2 {
+        public static final int NONE        = 0;
+        /** Password Authentication Protocol */
+        public static final int PAP         = 1;
+        /** Microsoft Challenge Handshake Authentication Protocol */
+        public static final int MSCHAP      = 2;
+        /** Microsoft Challenge Handshake Authentication Protocol v2 */
+        public static final int MSCHAPV2    = 3;
+        /** Generic Token Card */
+        public static final int GTC         = 4;
+        /** EAP-Subscriber Identity Module [RFC-4186] */
+        public static final int SIM         = 5;
+        /** EAP-Authentication and Key Agreement [RFC-4187] */
+        public static final int AKA         = 6;
+        /** EAP-Authentication and Key Agreement Prime [RFC-5448] */
+        public static final int AKA_PRIME   = 7;
+        private static final String AUTH_PREFIX = "auth=";
+        private static final String AUTHEAP_PREFIX = "autheap=";
+        /** @hide */
+        public static final String[] strings = {EMPTY_VALUE, "PAP", "MSCHAP",
+                "MSCHAPV2", "GTC", "SIM", "AKA", "AKA'" };
+
+        /** Prevent initialization */
+        private Phase2() {}
+    }
+
+    // Loader and saver interfaces for exchanging data with wpa_supplicant.
+    // TODO: Decouple this object (which is just a placeholder of the configuration)
+    // from the implementation that knows what wpa_supplicant wants.
+    /**
+     * Interface used for retrieving supplicant configuration from WifiEnterpriseConfig
+     * @hide
+     */
+    public interface SupplicantSaver {
+        /**
+         * Set a value within wpa_supplicant configuration
+         * @param key index to set within wpa_supplciant
+         * @param value the value for the key
+         * @return true if successful; false otherwise
+         */
+        boolean saveValue(String key, String value);
+    }
+
+    /**
+     * Interface used for populating a WifiEnterpriseConfig from supplicant configuration
+     * @hide
+     */
+    public interface SupplicantLoader {
+        /**
+         * Returns a value within wpa_supplicant configuration
+         * @param key index to set within wpa_supplciant
+         * @return string value if successful; null otherwise
+         */
+        String loadValue(String key);
+    }
+
+    /**
+     * Internal use only; supply field values to wpa_supplicant config.  The configuration
+     * process aborts on the first failed call on {@code saver}.
+     * @param saver proxy for setting configuration in wpa_supplciant
+     * @return whether the save succeeded on all attempts
+     * @hide
+     */
+    public boolean saveToSupplicant(SupplicantSaver saver) {
+        if (!isEapMethodValid()) {
+            return false;
+        }
+
+        // wpa_supplicant can update the anonymous identity for these kinds of networks after
+        // framework reads them, so make sure the framework doesn't try to overwrite them.
+        boolean shouldNotWriteAnonIdentity = mEapMethod == WifiEnterpriseConfig.Eap.SIM
+                || mEapMethod == WifiEnterpriseConfig.Eap.AKA
+                || mEapMethod == WifiEnterpriseConfig.Eap.AKA_PRIME;
+        for (String key : mFields.keySet()) {
+            if (shouldNotWriteAnonIdentity && ANON_IDENTITY_KEY.equals(key)) {
+                continue;
+            }
+            if (!saver.saveValue(key, mFields.get(key))) {
+                return false;
+            }
+        }
+
+        if (!saver.saveValue(EAP_KEY, Eap.strings[mEapMethod])) {
+            return false;
+        }
+
+        if (mEapMethod != Eap.TLS && mPhase2Method != Phase2.NONE) {
+            boolean is_autheap = mEapMethod == Eap.TTLS && mPhase2Method == Phase2.GTC;
+            String prefix = is_autheap ? Phase2.AUTHEAP_PREFIX : Phase2.AUTH_PREFIX;
+            String value = convertToQuotedString(prefix + Phase2.strings[mPhase2Method]);
+            return saver.saveValue(PHASE2_KEY, value);
+        } else if (mPhase2Method == Phase2.NONE) {
+            // By default, send a null phase 2 to clear old configuration values.
+            return saver.saveValue(PHASE2_KEY, null);
+        } else {
+            Log.e(TAG, "WiFi enterprise configuration is invalid as it supplies a "
+                    + "phase 2 method but the phase1 method does not support it.");
+            return false;
+        }
+    }
+
+    /**
+     * Internal use only; retrieve configuration from wpa_supplicant config.
+     * @param loader proxy for retrieving configuration keys from wpa_supplicant
+     * @hide
+     */
+    public void loadFromSupplicant(SupplicantLoader loader) {
+        for (String key : SUPPLICANT_CONFIG_KEYS) {
+            String value = loader.loadValue(key);
+            if (value == null) {
+                mFields.put(key, EMPTY_VALUE);
+            } else {
+                mFields.put(key, value);
+            }
+        }
+        String eapMethod  = loader.loadValue(EAP_KEY);
+        mEapMethod = getStringIndex(Eap.strings, eapMethod, Eap.NONE);
+
+        String phase2Method = removeDoubleQuotes(loader.loadValue(PHASE2_KEY));
+        // Remove "auth=" or "autheap=" prefix.
+        if (phase2Method.startsWith(Phase2.AUTH_PREFIX)) {
+            phase2Method = phase2Method.substring(Phase2.AUTH_PREFIX.length());
+        } else if (phase2Method.startsWith(Phase2.AUTHEAP_PREFIX)) {
+            phase2Method = phase2Method.substring(Phase2.AUTHEAP_PREFIX.length());
+        }
+        mPhase2Method = getStringIndex(Phase2.strings, phase2Method, Phase2.NONE);
+    }
+
+    /**
+     * Set the EAP authentication method.
+     * @param  eapMethod is one {@link Eap#PEAP}, {@link Eap#TLS}, {@link Eap#TTLS} or
+     *                   {@link Eap#PWD}
+     * @throws IllegalArgumentException on an invalid eap method
+     */
+    public void setEapMethod(int eapMethod) {
+        switch (eapMethod) {
+            /** Valid methods */
+            case Eap.TLS:
+            case Eap.UNAUTH_TLS:
+                setPhase2Method(Phase2.NONE);
+                /* fall through */
+            case Eap.PEAP:
+            case Eap.PWD:
+            case Eap.TTLS:
+            case Eap.SIM:
+            case Eap.AKA:
+            case Eap.AKA_PRIME:
+                mEapMethod = eapMethod;
+                setFieldValue(OPP_KEY_CACHING, "1");
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown EAP method");
+        }
+    }
+
+    /**
+     * Get the eap method.
+     * @return eap method configured
+     */
+    public int getEapMethod() {
+        return mEapMethod;
+    }
+
+    /**
+     * Set Phase 2 authentication method. Sets the inner authentication method to be used in
+     * phase 2 after setting up a secure channel
+     * @param phase2Method is the inner authentication method and can be one of {@link Phase2#NONE},
+     *                     {@link Phase2#PAP}, {@link Phase2#MSCHAP}, {@link Phase2#MSCHAPV2},
+     *                     {@link Phase2#GTC}
+     * @throws IllegalArgumentException on an invalid phase2 method
+     *
+     */
+    public void setPhase2Method(int phase2Method) {
+        switch (phase2Method) {
+            case Phase2.NONE:
+            case Phase2.PAP:
+            case Phase2.MSCHAP:
+            case Phase2.MSCHAPV2:
+            case Phase2.GTC:
+            case Phase2.SIM:
+            case Phase2.AKA:
+            case Phase2.AKA_PRIME:
+                mPhase2Method = phase2Method;
+                break;
+            default:
+                throw new IllegalArgumentException("Unknown Phase 2 method");
+        }
+    }
+
+    /**
+     * Get the phase 2 authentication method.
+     * @return a phase 2 method defined at {@link Phase2}
+     * */
+    public int getPhase2Method() {
+        return mPhase2Method;
+    }
+
+    /**
+     * Set the identity
+     * @param identity
+     */
+    public void setIdentity(String identity) {
+        setFieldValue(IDENTITY_KEY, identity, "");
+    }
+
+    /**
+     * Get the identity
+     * @return the identity
+     */
+    public String getIdentity() {
+        return getFieldValue(IDENTITY_KEY);
+    }
+
+    /**
+     * Set anonymous identity. This is used as the unencrypted identity with
+     * certain EAP types
+     * @param anonymousIdentity the anonymous identity
+     */
+    public void setAnonymousIdentity(String anonymousIdentity) {
+        setFieldValue(ANON_IDENTITY_KEY, anonymousIdentity);
+    }
+
+    /**
+     * Get the anonymous identity
+     * @return anonymous identity
+     */
+    public String getAnonymousIdentity() {
+        return getFieldValue(ANON_IDENTITY_KEY);
+    }
+
+    /**
+     * Set the password.
+     * @param password the password
+     */
+    public void setPassword(String password) {
+        setFieldValue(PASSWORD_KEY, password);
+    }
+
+    /**
+     * Get the password.
+     *
+     * Returns locally set password value. For networks fetched from
+     * framework, returns "*".
+     */
+    public String getPassword() {
+        return getFieldValue(PASSWORD_KEY);
+    }
+
+    /**
+     * Encode a CA certificate alias so it does not contain illegal character.
+     * @hide
+     */
+    public static String encodeCaCertificateAlias(String alias) {
+        byte[] bytes = alias.getBytes(StandardCharsets.UTF_8);
+        StringBuilder sb = new StringBuilder(bytes.length * 2);
+        for (byte o : bytes) {
+            sb.append(String.format("%02x", o & 0xFF));
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Decode a previously-encoded CA certificate alias.
+     * @hide
+     */
+    public static String decodeCaCertificateAlias(String alias) {
+        byte[] data = new byte[alias.length() >> 1];
+        for (int n = 0, position = 0; n < alias.length(); n += 2, position++) {
+            data[position] = (byte) Integer.parseInt(alias.substring(n,  n + 2), 16);
+        }
+        try {
+            return new String(data, StandardCharsets.UTF_8);
+        } catch (NumberFormatException e) {
+            e.printStackTrace();
+            return alias;
+        }
+    }
+
+    /**
+     * Set CA certificate alias.
+     *
+     * <p> See the {@link android.security.KeyChain} for details on installing or choosing
+     * a certificate
+     * </p>
+     * @param alias identifies the certificate
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public void setCaCertificateAlias(String alias) {
+        setFieldValue(CA_CERT_KEY, alias, CA_CERT_PREFIX);
+    }
+
+    /**
+     * Set CA certificate aliases. When creating installing the corresponding certificate to
+     * the keystore, please use alias encoded by {@link #encodeCaCertificateAlias(String)}.
+     *
+     * <p> See the {@link android.security.KeyChain} for details on installing or choosing
+     * a certificate.
+     * </p>
+     * @param aliases identifies the certificate
+     * @hide
+     */
+    public void setCaCertificateAliases(@Nullable String[] aliases) {
+        if (aliases == null) {
+            setFieldValue(CA_CERT_KEY, null, CA_CERT_PREFIX);
+        } else if (aliases.length == 1) {
+            // Backwards compatibility: use the original cert prefix if setting only one alias.
+            setCaCertificateAlias(aliases[0]);
+        } else {
+            // Use KEYSTORES_URI which supports multiple aliases.
+            StringBuilder sb = new StringBuilder();
+            for (int i = 0; i < aliases.length; i++) {
+                if (i > 0) {
+                    sb.append(CA_CERT_ALIAS_DELIMITER);
+                }
+                sb.append(encodeCaCertificateAlias(Credentials.CA_CERTIFICATE + aliases[i]));
+            }
+            setFieldValue(CA_CERT_KEY, sb.toString(), KEYSTORES_URI);
+        }
+    }
+
+    /**
+     * Get CA certificate alias
+     * @return alias to the CA certificate
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public String getCaCertificateAlias() {
+        return getFieldValue(CA_CERT_KEY, CA_CERT_PREFIX);
+    }
+
+    /**
+     * Get CA certificate aliases
+     * @return alias to the CA certificate
+     * @hide
+     */
+    @Nullable public String[] getCaCertificateAliases() {
+        String value = getFieldValue(CA_CERT_KEY);
+        if (value.startsWith(CA_CERT_PREFIX)) {
+            // Backwards compatibility: parse the original alias prefix.
+            return new String[] {getFieldValue(CA_CERT_KEY, CA_CERT_PREFIX)};
+        } else if (value.startsWith(KEYSTORES_URI)) {
+            String values = value.substring(KEYSTORES_URI.length());
+
+            String[] aliases = TextUtils.split(values, CA_CERT_ALIAS_DELIMITER);
+            for (int i = 0; i < aliases.length; i++) {
+                aliases[i] = decodeCaCertificateAlias(aliases[i]);
+                if (aliases[i].startsWith(Credentials.CA_CERTIFICATE)) {
+                    aliases[i] = aliases[i].substring(Credentials.CA_CERTIFICATE.length());
+                }
+            }
+            return aliases.length != 0 ? aliases : null;
+        } else {
+            return TextUtils.isEmpty(value) ? null : new String[] {value};
+        }
+    }
+
+    /**
+     * Specify a X.509 certificate that identifies the server.
+     *
+     * <p>A default name is automatically assigned to the certificate and used
+     * with this configuration. The framework takes care of installing the
+     * certificate when the config is saved and removing the certificate when
+     * the config is removed.
+     *
+     * @param cert X.509 CA certificate
+     * @throws IllegalArgumentException if not a CA certificate
+     */
+    public void setCaCertificate(@Nullable X509Certificate cert) {
+        if (cert != null) {
+            if (cert.getBasicConstraints() >= 0) {
+                mIsAppInstalledCaCert = true;
+                mCaCerts = new X509Certificate[] {cert};
+            } else {
+                mCaCerts = null;
+                throw new IllegalArgumentException("Not a CA certificate");
+            }
+        } else {
+            mCaCerts = null;
+        }
+    }
+
+    /**
+     * Get CA certificate. If multiple CA certificates are configured previously,
+     * return the first one.
+     * @return X.509 CA certificate
+     */
+    @Nullable public X509Certificate getCaCertificate() {
+        if (mCaCerts != null && mCaCerts.length > 0) {
+            return mCaCerts[0];
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Specify a list of X.509 certificates that identifies the server. The validation
+     * passes if the CA of server certificate matches one of the given certificates.
+
+     * <p>Default names are automatically assigned to the certificates and used
+     * with this configuration. The framework takes care of installing the
+     * certificates when the config is saved and removing the certificates when
+     * the config is removed.
+     *
+     * @param certs X.509 CA certificates
+     * @throws IllegalArgumentException if any of the provided certificates is
+     *     not a CA certificate
+     */
+    public void setCaCertificates(@Nullable X509Certificate[] certs) {
+        if (certs != null) {
+            X509Certificate[] newCerts = new X509Certificate[certs.length];
+            for (int i = 0; i < certs.length; i++) {
+                if (certs[i].getBasicConstraints() >= 0) {
+                    newCerts[i] = certs[i];
+                } else {
+                    mCaCerts = null;
+                    throw new IllegalArgumentException("Not a CA certificate");
+                }
+            }
+            mCaCerts = newCerts;
+            mIsAppInstalledCaCert = true;
+        } else {
+            mCaCerts = null;
+        }
+    }
+
+    /**
+     * Get CA certificates.
+     */
+    @Nullable public X509Certificate[] getCaCertificates() {
+        if (mCaCerts != null && mCaCerts.length > 0) {
+            return mCaCerts;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * @hide
+     */
+    public void resetCaCertificate() {
+        mCaCerts = null;
+    }
+
+    /**
+     * Set the ca_path directive on wpa_supplicant.
+     *
+     * From wpa_supplicant documentation:
+     *
+     * Directory path for CA certificate files (PEM). This path may contain
+     * multiple CA certificates in OpenSSL format. Common use for this is to
+     * point to system trusted CA list which is often installed into directory
+     * like /etc/ssl/certs. If configured, these certificates are added to the
+     * list of trusted CAs. ca_cert may also be included in that case, but it is
+     * not required.
+     * @param domain The path for CA certificate files
+     * @hide
+     */
+    public void setCaPath(String path) {
+        setFieldValue(CA_PATH_KEY, path);
+    }
+
+    /**
+     * Get the domain_suffix_match value. See setDomSuffixMatch.
+     * @return The path for CA certificate files.
+     * @hide
+     */
+    public String getCaPath() {
+        return getFieldValue(CA_PATH_KEY);
+    }
+
+    /** Set Client certificate alias.
+     *
+     * <p> See the {@link android.security.KeyChain} for details on installing or choosing
+     * a certificate
+     * </p>
+     * @param alias identifies the certificate
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public void setClientCertificateAlias(String alias) {
+        setFieldValue(CLIENT_CERT_KEY, alias, CLIENT_CERT_PREFIX);
+        setFieldValue(PRIVATE_KEY_ID_KEY, alias, Credentials.USER_PRIVATE_KEY);
+        // Also, set engine parameters
+        if (TextUtils.isEmpty(alias)) {
+            setFieldValue(ENGINE_KEY, ENGINE_DISABLE);
+            setFieldValue(ENGINE_ID_KEY, "");
+        } else {
+            setFieldValue(ENGINE_KEY, ENGINE_ENABLE);
+            setFieldValue(ENGINE_ID_KEY, ENGINE_ID_KEYSTORE);
+        }
+    }
+
+    /**
+     * Get client certificate alias
+     * @return alias to the client certificate
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public String getClientCertificateAlias() {
+        return getFieldValue(CLIENT_CERT_KEY, CLIENT_CERT_PREFIX);
+    }
+
+    /**
+     * Specify a private key and client certificate for client authorization.
+     *
+     * <p>A default name is automatically assigned to the key entry and used
+     * with this configuration.  The framework takes care of installing the
+     * key entry when the config is saved and removing the key entry when
+     * the config is removed.
+
+     * @param privateKey a PrivateKey instance for the end certificate.
+     * @param clientCertificate an X509Certificate representing the end certificate.
+     * @throws IllegalArgumentException for an invalid key or certificate.
+     */
+    public void setClientKeyEntry(PrivateKey privateKey, X509Certificate clientCertificate) {
+        X509Certificate[] clientCertificates = null;
+        if (clientCertificate != null) {
+            clientCertificates = new X509Certificate[] {clientCertificate};
+        }
+        setClientKeyEntryWithCertificateChain(privateKey, clientCertificates);
+    }
+
+    /**
+     * Specify a private key and client certificate chain for client authorization.
+     *
+     * <p>A default name is automatically assigned to the key entry and used
+     * with this configuration.  The framework takes care of installing the
+     * key entry when the config is saved and removing the key entry when
+     * the config is removed.
+     *
+     * @param privateKey a PrivateKey instance for the end certificate.
+     * @param clientCertificateChain an array of X509Certificate instances which starts with
+     *         end certificate and continues with additional CA certificates necessary to
+     *         link the end certificate with some root certificate known by the authenticator.
+     * @throws IllegalArgumentException for an invalid key or certificate.
+     */
+    public void setClientKeyEntryWithCertificateChain(PrivateKey privateKey,
+            X509Certificate[] clientCertificateChain) {
+        X509Certificate[] newCerts = null;
+        if (clientCertificateChain != null && clientCertificateChain.length > 0) {
+            // We validate that this is a well formed chain that starts
+            // with an end-certificate and is followed by CA certificates.
+            // We don't validate that each following certificate verifies
+            // the previous. https://en.wikipedia.org/wiki/Chain_of_trust
+            //
+            // Basic constraints is an X.509 extension type that defines
+            // whether a given certificate is allowed to sign additional
+            // certificates and what path length restrictions may exist.
+            // We use this to judge whether the certificate is an end
+            // certificate or a CA certificate.
+            // https://cryptography.io/en/latest/x509/reference/
+            if (clientCertificateChain[0].getBasicConstraints() != -1) {
+                throw new IllegalArgumentException(
+                        "First certificate in the chain must be a client end certificate");
+            }
+
+            for (int i = 1; i < clientCertificateChain.length; i++) {
+                if (clientCertificateChain[i].getBasicConstraints() == -1) {
+                    throw new IllegalArgumentException(
+                            "All certificates following the first must be CA certificates");
+                }
+            }
+            newCerts = Arrays.copyOf(clientCertificateChain,
+                    clientCertificateChain.length);
+
+            if (privateKey == null) {
+                throw new IllegalArgumentException("Client cert without a private key");
+            }
+            if (privateKey.getEncoded() == null) {
+                throw new IllegalArgumentException("Private key cannot be encoded");
+            }
+        }
+
+        mClientPrivateKey = privateKey;
+        mClientCertificateChain = newCerts;
+        mIsAppInstalledDeviceKeyAndCert = true;
+    }
+
+    /**
+     * Get client certificate
+     *
+     * @return X.509 client certificate
+     */
+    public X509Certificate getClientCertificate() {
+        if (mClientCertificateChain != null && mClientCertificateChain.length > 0) {
+            return mClientCertificateChain[0];
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Get the complete client certificate chain in the same order as it was last supplied.
+     *
+     * <p>If the chain was last supplied by a call to
+     * {@link #setClientKeyEntry(java.security.PrivateKey, java.security.cert.X509Certificate)}
+     * with a non-null * certificate instance, a single-element array containing the certificate
+     * will be * returned. If {@link #setClientKeyEntryWithCertificateChain(
+     * java.security.PrivateKey, java.security.cert.X509Certificate[])} was last called with a
+     * non-empty array, this array will be returned in the same order as it was supplied.
+     * Otherwise, {@code null} will be returned.
+     *
+     * @return X.509 client certificates
+     */
+    @Nullable public X509Certificate[] getClientCertificateChain() {
+        if (mClientCertificateChain != null && mClientCertificateChain.length > 0) {
+            return mClientCertificateChain;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * @hide
+     */
+    public void resetClientKeyEntry() {
+        mClientPrivateKey = null;
+        mClientCertificateChain = null;
+    }
+
+    /**
+     * @hide
+     */
+    public PrivateKey getClientPrivateKey() {
+        return mClientPrivateKey;
+    }
+
+    /**
+     * Set subject match (deprecated). This is the substring to be matched against the subject of
+     * the authentication server certificate.
+     * @param subjectMatch substring to be matched
+     * @deprecated in favor of altSubjectMatch
+     */
+    public void setSubjectMatch(String subjectMatch) {
+        setFieldValue(SUBJECT_MATCH_KEY, subjectMatch);
+    }
+
+    /**
+     * Get subject match (deprecated)
+     * @return the subject match string
+     * @deprecated in favor of altSubjectMatch
+     */
+    public String getSubjectMatch() {
+        return getFieldValue(SUBJECT_MATCH_KEY);
+    }
+
+    /**
+     * Set alternate subject match. This is the substring to be matched against the
+     * alternate subject of the authentication server certificate.
+     * @param altSubjectMatch substring to be matched, for example
+     *                     DNS:server.example.com;EMAIL:[email protected]
+     */
+    public void setAltSubjectMatch(String altSubjectMatch) {
+        setFieldValue(ALTSUBJECT_MATCH_KEY, altSubjectMatch);
+    }
+
+    /**
+     * Get alternate subject match
+     * @return the alternate subject match string
+     */
+    public String getAltSubjectMatch() {
+        return getFieldValue(ALTSUBJECT_MATCH_KEY);
+    }
+
+    /**
+     * Set the domain_suffix_match directive on wpa_supplicant. This is the parameter to use
+     * for Hotspot 2.0 defined matching of AAA server certs per WFA HS2.0 spec, section 7.3.3.2,
+     * second paragraph.
+     *
+     * <p>From wpa_supplicant documentation:
+     * <p>Constraint for server domain name. If set, this FQDN is used as a suffix match requirement
+     * for the AAAserver certificate in SubjectAltName dNSName element(s). If a matching dNSName is
+     * found, this constraint is met.
+     * <p>Suffix match here means that the host/domain name is compared one label at a time starting
+     * from the top-level domain and all the labels in domain_suffix_match shall be included in the
+     * certificate. The certificate may include additional sub-level labels in addition to the
+     * required labels.
+     * <p>More than one match string can be provided by using semicolons to separate the strings
+     * (e.g., example.org;example.com). When multiple strings are specified, a match with any one of
+     * the values is considered a sufficient match for the certificate, i.e., the conditions are
+     * ORed ogether.
+     * <p>For example, domain_suffix_match=example.com would match test.example.com but would not
+     * match test-example.com.
+     * @param domain The domain value
+     */
+    public void setDomainSuffixMatch(String domain) {
+        setFieldValue(DOM_SUFFIX_MATCH_KEY, domain);
+    }
+
+    /**
+     * Get the domain_suffix_match value. See setDomSuffixMatch.
+     * @return The domain value.
+     */
+    public String getDomainSuffixMatch() {
+        return getFieldValue(DOM_SUFFIX_MATCH_KEY);
+    }
+
+    /**
+     * Set realm for Passpoint credential; realm identifies a set of networks where your
+     * Passpoint credential can be used
+     * @param realm the realm
+     */
+    public void setRealm(String realm) {
+        setFieldValue(REALM_KEY, realm);
+    }
+
+    /**
+     * Get realm for Passpoint credential; see {@link #setRealm(String)} for more information
+     * @return the realm
+     */
+    public String getRealm() {
+        return getFieldValue(REALM_KEY);
+    }
+
+    /**
+     * Set plmn (Public Land Mobile Network) of the provider of Passpoint credential
+     * @param plmn the plmn value derived from mcc (mobile country code) & mnc (mobile network code)
+     */
+    public void setPlmn(String plmn) {
+        setFieldValue(PLMN_KEY, plmn);
+    }
+
+    /**
+     * Get plmn (Public Land Mobile Network) for Passpoint credential; see {@link #setPlmn
+     * (String)} for more information
+     * @return the plmn
+     */
+    public String getPlmn() {
+        return getFieldValue(PLMN_KEY);
+    }
+
+    /** See {@link WifiConfiguration#getKeyIdForCredentials} @hide */
+    public String getKeyId(WifiEnterpriseConfig current) {
+        // If EAP method is not initialized, use current config details
+        if (mEapMethod == Eap.NONE) {
+            return (current != null) ? current.getKeyId(null) : EMPTY_VALUE;
+        }
+        if (!isEapMethodValid()) {
+            return EMPTY_VALUE;
+        }
+        return Eap.strings[mEapMethod] + "_" + Phase2.strings[mPhase2Method];
+    }
+
+    private String removeDoubleQuotes(String string) {
+        if (TextUtils.isEmpty(string)) return "";
+        int length = string.length();
+        if ((length > 1) && (string.charAt(0) == '"')
+                && (string.charAt(length - 1) == '"')) {
+            return string.substring(1, length - 1);
+        }
+        return string;
+    }
+
+    private String convertToQuotedString(String string) {
+        return "\"" + string + "\"";
+    }
+
+    /**
+     * Returns the index at which the toBeFound string is found in the array.
+     * @param arr array of strings
+     * @param toBeFound string to be found
+     * @param defaultIndex default index to be returned when string is not found
+     * @return the index into array
+     */
+    private int getStringIndex(String arr[], String toBeFound, int defaultIndex) {
+        if (TextUtils.isEmpty(toBeFound)) return defaultIndex;
+        for (int i = 0; i < arr.length; i++) {
+            if (toBeFound.equals(arr[i])) return i;
+        }
+        return defaultIndex;
+    }
+
+    /**
+     * Returns the field value for the key with prefix removed.
+     * @param key into the hash
+     * @param prefix is the prefix that the value may have
+     * @return value
+     * @hide
+     */
+    private String getFieldValue(String key, String prefix) {
+        // TODO: Should raise an exception if |key| is EAP_KEY or PHASE2_KEY since
+        // neither of these keys should be retrieved in this manner.
+        String value = mFields.get(key);
+        // Uninitialized or known to be empty after reading from supplicant
+        if (TextUtils.isEmpty(value) || EMPTY_VALUE.equals(value)) return "";
+
+        value = removeDoubleQuotes(value);
+        if (value.startsWith(prefix)) {
+            return value.substring(prefix.length());
+        } else {
+            return value;
+        }
+    }
+
+    /**
+     * Returns the field value for the key.
+     * @param key into the hash
+     * @return value
+     * @hide
+     */
+    public String getFieldValue(String key) {
+        return getFieldValue(key, "");
+    }
+
+    /**
+     * Set a value with an optional prefix at key
+     * @param key into the hash
+     * @param value to be set
+     * @param prefix an optional value to be prefixed to actual value
+     * @hide
+     */
+    private void setFieldValue(String key, String value, String prefix) {
+        // TODO: Should raise an exception if |key| is EAP_KEY or PHASE2_KEY since
+        // neither of these keys should be set in this manner.
+        if (TextUtils.isEmpty(value)) {
+            mFields.put(key, EMPTY_VALUE);
+        } else {
+            String valueToSet;
+            if (!UNQUOTED_KEYS.contains(key)) {
+                valueToSet = convertToQuotedString(prefix + value);
+            } else {
+                valueToSet = prefix + value;
+            }
+            mFields.put(key, valueToSet);
+        }
+    }
+
+    /**
+     * Set a value at key
+     * @param key into the hash
+     * @param value to be set
+     * @hide
+     */
+    public void setFieldValue(String key, String value) {
+        setFieldValue(key, value, "");
+    }
+
+    @Override
+    public String toString() {
+        StringBuffer sb = new StringBuffer();
+        for (String key : mFields.keySet()) {
+            // Don't display password in toString().
+            String value = PASSWORD_KEY.equals(key) ? "<removed>" : mFields.get(key);
+            sb.append(key).append(" ").append(value).append("\n");
+        }
+        if (mEapMethod >= 0 && mEapMethod < Eap.strings.length) {
+            sb.append("eap_method: ").append(Eap.strings[mEapMethod]).append("\n");
+        }
+        if (mPhase2Method > 0 && mPhase2Method < Phase2.strings.length) {
+            sb.append("phase2_method: ").append(Phase2.strings[mPhase2Method]).append("\n");
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Returns whether the EAP method data is valid, i.e., whether mEapMethod and mPhase2Method
+     * are valid indices into {@code Eap.strings[]} and {@code Phase2.strings[]} respectively.
+     */
+    private boolean isEapMethodValid() {
+        if (mEapMethod == Eap.NONE) {
+            Log.e(TAG, "WiFi enterprise configuration is invalid as it supplies no EAP method.");
+            return false;
+        }
+        if (mEapMethod < 0 || mEapMethod >= Eap.strings.length) {
+            Log.e(TAG, "mEapMethod is invald for WiFi enterprise configuration: " + mEapMethod);
+            return false;
+        }
+        if (mPhase2Method < 0 || mPhase2Method >= Phase2.strings.length) {
+            Log.e(TAG, "mPhase2Method is invald for WiFi enterprise configuration: "
+                    + mPhase2Method);
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Check if certificate was installed by an app, or manually (not by an app). If true,
+     * certificate and keys will be removed from key storage when this network is removed. If not,
+     * then certificates and keys remain persistent until the user manually removes them.
+     *
+     * @return true if certificate was installed by an app, false if certificate was installed
+     * manually by the user.
+     * @hide
+     */
+    public boolean isAppInstalledDeviceKeyAndCert() {
+        return mIsAppInstalledDeviceKeyAndCert;
+    }
+
+    /**
+     * Check if CA certificate was installed by an app, or manually (not by an app). If true,
+     * CA certificate will be removed from key storage when this network is removed. If not,
+     * then certificates and keys remain persistent until the user manually removes them.
+     *
+     * @return true if CA certificate was installed by an app, false if CA certificate was installed
+     * manually by the user.
+     * @hide
+     */
+    public boolean isAppInstalledCaCert() {
+        return mIsAppInstalledCaCert;
+    }
+}
diff --git a/android/net/wifi/WifiInfo.java b/android/net/wifi/WifiInfo.java
new file mode 100644
index 0000000..0b55794
--- /dev/null
+++ b/android/net/wifi/WifiInfo.java
@@ -0,0 +1,785 @@
+/*
+ * Copyright (C) 2008 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.wifi;
+
+import android.annotation.IntRange;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.annotation.UnsupportedAppUsage;
+import android.net.NetworkInfo.DetailedState;
+import android.net.NetworkUtils;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.util.EnumMap;
+import java.util.Locale;
+
+/**
+ * Describes the state of any Wifi connection that is active or
+ * is in the process of being set up.
+ */
+public class WifiInfo implements Parcelable {
+    private static final String TAG = "WifiInfo";
+    /**
+     * This is the map described in the Javadoc comment above. The positions
+     * of the elements of the array must correspond to the ordinal values
+     * of <code>DetailedState</code>.
+     */
+    private static final EnumMap<SupplicantState, DetailedState> stateMap =
+            new EnumMap<SupplicantState, DetailedState>(SupplicantState.class);
+
+    /**
+     * Default MAC address reported to a client that does not have the
+     * android.permission.LOCAL_MAC_ADDRESS permission.
+     *
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public static final String DEFAULT_MAC_ADDRESS = "02:00:00:00:00:00";
+
+    static {
+        stateMap.put(SupplicantState.DISCONNECTED, DetailedState.DISCONNECTED);
+        stateMap.put(SupplicantState.INTERFACE_DISABLED, DetailedState.DISCONNECTED);
+        stateMap.put(SupplicantState.INACTIVE, DetailedState.IDLE);
+        stateMap.put(SupplicantState.SCANNING, DetailedState.SCANNING);
+        stateMap.put(SupplicantState.AUTHENTICATING, DetailedState.CONNECTING);
+        stateMap.put(SupplicantState.ASSOCIATING, DetailedState.CONNECTING);
+        stateMap.put(SupplicantState.ASSOCIATED, DetailedState.CONNECTING);
+        stateMap.put(SupplicantState.FOUR_WAY_HANDSHAKE, DetailedState.AUTHENTICATING);
+        stateMap.put(SupplicantState.GROUP_HANDSHAKE, DetailedState.AUTHENTICATING);
+        stateMap.put(SupplicantState.COMPLETED, DetailedState.OBTAINING_IPADDR);
+        stateMap.put(SupplicantState.DORMANT, DetailedState.DISCONNECTED);
+        stateMap.put(SupplicantState.UNINITIALIZED, DetailedState.IDLE);
+        stateMap.put(SupplicantState.INVALID, DetailedState.FAILED);
+    }
+
+    private SupplicantState mSupplicantState;
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    private String mBSSID;
+    @UnsupportedAppUsage
+    private WifiSsid mWifiSsid;
+    private int mNetworkId;
+
+    /** @hide **/
+    @UnsupportedAppUsage
+    public static final int INVALID_RSSI = -127;
+
+    /** @hide **/
+    public static final int MIN_RSSI = -126;
+
+    /** @hide **/
+    public static final int MAX_RSSI = 200;
+
+
+    /**
+     * Received Signal Strength Indicator
+     */
+    private int mRssi;
+
+    /**
+     * The unit in which links speeds are expressed.
+     */
+    public static final String LINK_SPEED_UNITS = "Mbps";
+    private int mLinkSpeed;
+
+    /**
+     * Constant for unknown link speed.
+     */
+    public static final int LINK_SPEED_UNKNOWN = -1;
+
+    /**
+     * Tx(transmit) Link speed in Mbps
+     */
+    private int mTxLinkSpeed;
+
+    /**
+     * Rx(receive) Link speed in Mbps
+     */
+    private int mRxLinkSpeed;
+
+    /**
+     * Frequency in MHz
+     */
+    public static final String FREQUENCY_UNITS = "MHz";
+    private int mFrequency;
+
+    @UnsupportedAppUsage
+    private InetAddress mIpAddress;
+    @UnsupportedAppUsage
+    private String mMacAddress = DEFAULT_MAC_ADDRESS;
+
+    /**
+     * Whether the network is ephemeral or not.
+     */
+    private boolean mEphemeral;
+
+    /**
+     * Whether the network is trusted or not.
+     */
+    private boolean mTrusted;
+
+    /**
+     * OSU (Online Sign Up) AP for Passpoint R2.
+     */
+    private boolean mOsuAp;
+
+    /**
+     * Fully qualified domain name of a Passpoint configuration
+     */
+    private String mFqdn;
+
+    /**
+     * Name of Passpoint credential provider
+     */
+    private String mProviderFriendlyName;
+
+    /**
+     * If connected to a network suggestion or specifier, store the package name of the app,
+     * else null.
+     */
+    private String mNetworkSuggestionOrSpecifierPackageName;
+
+    /**
+     * Running total count of lost (not ACKed) transmitted unicast data packets.
+     * @hide
+     */
+    public long txBad;
+    /**
+     * Running total count of transmitted unicast data retry packets.
+     * @hide
+     */
+    public long txRetries;
+    /**
+     * Running total count of successfully transmitted (ACKed) unicast data packets.
+     * @hide
+     */
+    public long txSuccess;
+    /**
+     * Running total count of received unicast data packets.
+     * @hide
+     */
+    public long rxSuccess;
+
+    /**
+     * Average rate of lost transmitted packets, in units of packets per second.
+     * @hide
+     */
+    public double txBadRate;
+    /**
+     * Average rate of transmitted retry packets, in units of packets per second.
+     * @hide
+     */
+    public double txRetriesRate;
+    /**
+     * Average rate of successfully transmitted unicast packets, in units of packets per second.
+     * @hide
+     */
+    public double txSuccessRate;
+    /**
+     * Average rate of received unicast data packets, in units of packets per second.
+     * @hide
+     */
+    public double rxSuccessRate;
+
+    /**
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public int score;
+
+    /**
+     * Flag indicating that AP has hinted that upstream connection is metered,
+     * and sensitive to heavy data transfers.
+     */
+    private boolean mMeteredHint;
+
+    /** @hide */
+    @UnsupportedAppUsage
+    public WifiInfo() {
+        mWifiSsid = null;
+        mBSSID = null;
+        mNetworkId = -1;
+        mSupplicantState = SupplicantState.UNINITIALIZED;
+        mRssi = INVALID_RSSI;
+        mLinkSpeed = LINK_SPEED_UNKNOWN;
+        mFrequency = -1;
+    }
+
+    /** @hide */
+    public void reset() {
+        setInetAddress(null);
+        setBSSID(null);
+        setSSID(null);
+        setNetworkId(-1);
+        setRssi(INVALID_RSSI);
+        setLinkSpeed(LINK_SPEED_UNKNOWN);
+        setTxLinkSpeedMbps(LINK_SPEED_UNKNOWN);
+        setRxLinkSpeedMbps(LINK_SPEED_UNKNOWN);
+        setFrequency(-1);
+        setMeteredHint(false);
+        setEphemeral(false);
+        setOsuAp(false);
+        setNetworkSuggestionOrSpecifierPackageName(null);
+        setFQDN(null);
+        setProviderFriendlyName(null);
+        txBad = 0;
+        txSuccess = 0;
+        rxSuccess = 0;
+        txRetries = 0;
+        txBadRate = 0;
+        txSuccessRate = 0;
+        rxSuccessRate = 0;
+        txRetriesRate = 0;
+        score = 0;
+    }
+
+    /**
+     * Copy constructor
+     * @hide
+     */
+    public WifiInfo(WifiInfo source) {
+        if (source != null) {
+            mSupplicantState = source.mSupplicantState;
+            mBSSID = source.mBSSID;
+            mWifiSsid = source.mWifiSsid;
+            mNetworkId = source.mNetworkId;
+            mRssi = source.mRssi;
+            mLinkSpeed = source.mLinkSpeed;
+            mTxLinkSpeed = source.mTxLinkSpeed;
+            mRxLinkSpeed = source.mRxLinkSpeed;
+            mFrequency = source.mFrequency;
+            mIpAddress = source.mIpAddress;
+            mMacAddress = source.mMacAddress;
+            mMeteredHint = source.mMeteredHint;
+            mEphemeral = source.mEphemeral;
+            mTrusted = source.mTrusted;
+            mNetworkSuggestionOrSpecifierPackageName =
+                    source.mNetworkSuggestionOrSpecifierPackageName;
+            mOsuAp = source.mOsuAp;
+            mFqdn = source.mFqdn;
+            mProviderFriendlyName = source.mProviderFriendlyName;
+            txBad = source.txBad;
+            txRetries = source.txRetries;
+            txSuccess = source.txSuccess;
+            rxSuccess = source.rxSuccess;
+            txBadRate = source.txBadRate;
+            txRetriesRate = source.txRetriesRate;
+            txSuccessRate = source.txSuccessRate;
+            rxSuccessRate = source.rxSuccessRate;
+            score = source.score;
+        }
+    }
+
+    /** @hide */
+    public void setSSID(WifiSsid wifiSsid) {
+        mWifiSsid = wifiSsid;
+    }
+
+    /**
+     * Returns the service set identifier (SSID) of the current 802.11 network.
+     * <p>
+     * If the SSID can be decoded as UTF-8, it will be returned surrounded by double
+     * quotation marks. Otherwise, it is returned as a string of hex digits.
+     * The SSID may be
+     * <lt>&lt;unknown ssid&gt;, if there is no network currently connected or if the caller has
+     * insufficient permissions to access the SSID.<lt>
+     * </p>
+     * <p>
+     * Prior to {@link android.os.Build.VERSION_CODES#JELLY_BEAN_MR1}, this method
+     * always returned the SSID with no quotes around it.
+     * </p>
+     *
+     * @return the SSID.
+     */
+    public String getSSID() {
+        if (mWifiSsid != null) {
+            String unicode = mWifiSsid.toString();
+            if (!TextUtils.isEmpty(unicode)) {
+                return "\"" + unicode + "\"";
+            } else {
+                String hex = mWifiSsid.getHexString();
+                return (hex != null) ? hex : WifiSsid.NONE;
+            }
+        }
+        return WifiSsid.NONE;
+    }
+
+    /** @hide */
+    @UnsupportedAppUsage
+    public WifiSsid getWifiSsid() {
+        return mWifiSsid;
+    }
+
+    /** @hide */
+    @UnsupportedAppUsage
+    public void setBSSID(String BSSID) {
+        mBSSID = BSSID;
+    }
+
+    /**
+     * Return the basic service set identifier (BSSID) of the current access point.
+     * <p>
+     * The BSSID may be
+     * <lt>{@code null}, if there is no network currently connected.</lt>
+     * <lt>{@code "02:00:00:00:00:00"}, if the caller has insufficient permissions to access the
+     * BSSID.<lt>
+     * </p>
+     *
+     * @return the BSSID, in the form of a six-byte MAC address: {@code XX:XX:XX:XX:XX:XX}
+     */
+    public String getBSSID() {
+        return mBSSID;
+    }
+
+    /**
+     * Returns the received signal strength indicator of the current 802.11
+     * network, in dBm.
+     *
+     * <p>Use {@link android.net.wifi.WifiManager#calculateSignalLevel} to convert this number into
+     * an absolute signal level which can be displayed to a user.
+     *
+     * @return the RSSI.
+     */
+    public int getRssi() {
+        return mRssi;
+    }
+
+    /** @hide */
+    @UnsupportedAppUsage
+    public void setRssi(int rssi) {
+        if (rssi < INVALID_RSSI)
+            rssi = INVALID_RSSI;
+        if (rssi > MAX_RSSI)
+            rssi = MAX_RSSI;
+        mRssi = rssi;
+    }
+
+    /**
+     * Returns the current link speed in {@link #LINK_SPEED_UNITS}.
+     * @return the link speed or {@link #LINK_SPEED_UNKNOWN} if link speed is unknown.
+     * @see #LINK_SPEED_UNITS
+     * @see #LINK_SPEED_UNKNOWN
+     */
+    public int getLinkSpeed() {
+        return mLinkSpeed;
+    }
+
+    /** @hide */
+    @UnsupportedAppUsage
+    public void setLinkSpeed(int linkSpeed) {
+        mLinkSpeed = linkSpeed;
+    }
+
+    /**
+     * Returns the current transmit link speed in Mbps.
+     * @return the Tx link speed or {@link #LINK_SPEED_UNKNOWN} if link speed is unknown.
+     * @see #LINK_SPEED_UNKNOWN
+     */
+    @IntRange(from = -1)
+    public int getTxLinkSpeedMbps() {
+        return mTxLinkSpeed;
+    }
+
+    /**
+     * Update the last transmitted packet bit rate in Mbps.
+     * @hide
+     */
+    public void setTxLinkSpeedMbps(int txLinkSpeed) {
+        mTxLinkSpeed = txLinkSpeed;
+    }
+
+    /**
+     * Returns the current receive link speed in Mbps.
+     * @return the Rx link speed or {@link #LINK_SPEED_UNKNOWN} if link speed is unknown.
+     * @see #LINK_SPEED_UNKNOWN
+     */
+    @IntRange(from = -1)
+    public int getRxLinkSpeedMbps() {
+        return mRxLinkSpeed;
+    }
+
+    /**
+     * Update the last received packet bit rate in Mbps.
+     * @hide
+     */
+    public void setRxLinkSpeedMbps(int rxLinkSpeed) {
+        mRxLinkSpeed = rxLinkSpeed;
+    }
+
+    /**
+     * Returns the current frequency in {@link #FREQUENCY_UNITS}.
+     * @return the frequency.
+     * @see #FREQUENCY_UNITS
+     */
+    public int getFrequency() {
+        return mFrequency;
+    }
+
+    /** @hide */
+    public void setFrequency(int frequency) {
+        this.mFrequency = frequency;
+    }
+
+    /**
+     * @hide
+     * TODO: makes real freq boundaries
+     */
+    public boolean is24GHz() {
+        return ScanResult.is24GHz(mFrequency);
+    }
+
+    /**
+     * @hide
+     * TODO: makes real freq boundaries
+     */
+    @UnsupportedAppUsage
+    public boolean is5GHz() {
+        return ScanResult.is5GHz(mFrequency);
+    }
+
+    /**
+     * Record the MAC address of the WLAN interface
+     * @param macAddress the MAC address in {@code XX:XX:XX:XX:XX:XX} form
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public void setMacAddress(String macAddress) {
+        this.mMacAddress = macAddress;
+    }
+
+    public String getMacAddress() {
+        return mMacAddress;
+    }
+
+    /**
+     * @return true if {@link #getMacAddress()} has a real MAC address.
+     *
+     * @hide
+     */
+    public boolean hasRealMacAddress() {
+        return mMacAddress != null && !DEFAULT_MAC_ADDRESS.equals(mMacAddress);
+    }
+
+    /**
+     * Indicates if we've dynamically detected this active network connection as
+     * being metered.
+     *
+     * @see WifiConfiguration#isMetered(WifiConfiguration, WifiInfo)
+     * @hide
+     */
+    public void setMeteredHint(boolean meteredHint) {
+        mMeteredHint = meteredHint;
+    }
+
+    /** {@hide} */
+    @UnsupportedAppUsage
+    public boolean getMeteredHint() {
+        return mMeteredHint;
+    }
+
+    /** {@hide} */
+    public void setEphemeral(boolean ephemeral) {
+        mEphemeral = ephemeral;
+    }
+
+    /** {@hide} */
+    @UnsupportedAppUsage
+    public boolean isEphemeral() {
+        return mEphemeral;
+    }
+
+    /** {@hide} */
+    public void setTrusted(boolean trusted) {
+        mTrusted = trusted;
+    }
+
+    /** {@hide} */
+    public boolean isTrusted() {
+        return mTrusted;
+    }
+
+    /** {@hide} */
+    public void setOsuAp(boolean osuAp) {
+        mOsuAp = osuAp;
+    }
+
+    /** {@hide} */
+    @SystemApi
+    public boolean isOsuAp() {
+        return mOsuAp;
+    }
+
+    /** {@hide} */
+    @SystemApi
+    public boolean isPasspointAp() {
+        return mFqdn != null && mProviderFriendlyName != null;
+    }
+
+    /** {@hide} */
+    public void setFQDN(@Nullable String fqdn) {
+        mFqdn = fqdn;
+    }
+
+    /**
+     * Returns the Fully Qualified Domain Name of the network if it is a Passpoint network.
+     */
+    public @Nullable String getPasspointFqdn() {
+        return mFqdn;
+    }
+
+    /** {@hide} */
+    public void setProviderFriendlyName(@Nullable String providerFriendlyName) {
+        mProviderFriendlyName = providerFriendlyName;
+    }
+
+    /**
+     * Returns the Provider Friendly Name of the network if it is a Passpoint network.
+     */
+    public @Nullable String getPasspointProviderFriendlyName() {
+        return mProviderFriendlyName;
+    }
+
+    /** {@hide} */
+    public void setNetworkSuggestionOrSpecifierPackageName(@Nullable String packageName) {
+        mNetworkSuggestionOrSpecifierPackageName = packageName;
+    }
+
+    /** {@hide} */
+    public @Nullable String getNetworkSuggestionOrSpecifierPackageName() {
+        return mNetworkSuggestionOrSpecifierPackageName;
+    }
+
+
+    /** @hide */
+    @UnsupportedAppUsage
+    public void setNetworkId(int id) {
+        mNetworkId = id;
+    }
+
+    /**
+     * Each configured network has a unique small integer ID, used to identify
+     * the network. This method returns the ID for the currently connected network.
+     * <p>
+     * The networkId may be {@code -1} if there is no currently connected network or if the caller
+     * has insufficient permissions to access the network ID.
+     * </p>
+     *
+     * @return the network ID.
+     */
+    public int getNetworkId() {
+        return mNetworkId;
+    }
+
+    /**
+     * Return the detailed state of the supplicant's negotiation with an
+     * access point, in the form of a {@link SupplicantState SupplicantState} object.
+     * @return the current {@link SupplicantState SupplicantState}
+     */
+    public SupplicantState getSupplicantState() {
+        return mSupplicantState;
+    }
+
+    /** @hide */
+    @UnsupportedAppUsage
+    public void setSupplicantState(SupplicantState state) {
+        mSupplicantState = state;
+    }
+
+    /** @hide */
+    public void setInetAddress(InetAddress address) {
+        mIpAddress = address;
+    }
+
+    public int getIpAddress() {
+        int result = 0;
+        if (mIpAddress instanceof Inet4Address) {
+            result = NetworkUtils.inetAddressToInt((Inet4Address)mIpAddress);
+        }
+        return result;
+    }
+
+    /**
+     * @return {@code true} if this network does not broadcast its SSID, so an
+     * SSID-specific probe request must be used for scans.
+     */
+    public boolean getHiddenSSID() {
+        if (mWifiSsid == null) return false;
+        return mWifiSsid.isHidden();
+    }
+
+   /**
+     * Map a supplicant state into a fine-grained network connectivity state.
+     * @param suppState the supplicant state
+     * @return the corresponding {@link DetailedState}
+     */
+    public static DetailedState getDetailedStateOf(SupplicantState suppState) {
+        return stateMap.get(suppState);
+    }
+
+    /**
+     * Set the <code>SupplicantState</code> from the string name
+     * of the state.
+     * @param stateName the name of the state, as a <code>String</code> returned
+     * in an event sent by {@code wpa_supplicant}.
+     */
+    @UnsupportedAppUsage
+    void setSupplicantState(String stateName) {
+        mSupplicantState = valueOf(stateName);
+    }
+
+    static SupplicantState valueOf(String stateName) {
+        if ("4WAY_HANDSHAKE".equalsIgnoreCase(stateName))
+            return SupplicantState.FOUR_WAY_HANDSHAKE;
+        else {
+            try {
+                return SupplicantState.valueOf(stateName.toUpperCase(Locale.ROOT));
+            } catch (IllegalArgumentException e) {
+                return SupplicantState.INVALID;
+            }
+        }
+    }
+
+    /** {@hide} */
+    @UnsupportedAppUsage
+    public static String removeDoubleQuotes(String string) {
+        if (string == null) return null;
+        final int length = string.length();
+        if ((length > 1) && (string.charAt(0) == '"') && (string.charAt(length - 1) == '"')) {
+            return string.substring(1, length - 1);
+        }
+        return string;
+    }
+
+    @Override
+    public String toString() {
+        StringBuffer sb = new StringBuffer();
+        String none = "<none>";
+
+        sb.append("SSID: ").append(mWifiSsid == null ? WifiSsid.NONE : mWifiSsid)
+                .append(", BSSID: ").append(mBSSID == null ? none : mBSSID)
+                .append(", MAC: ").append(mMacAddress == null ? none : mMacAddress)
+                .append(", Supplicant state: ")
+                .append(mSupplicantState == null ? none : mSupplicantState)
+                .append(", RSSI: ").append(mRssi)
+                .append(", Link speed: ").append(mLinkSpeed).append(LINK_SPEED_UNITS)
+                .append(", Tx Link speed: ").append(mTxLinkSpeed).append(LINK_SPEED_UNITS)
+                .append(", Rx Link speed: ").append(mRxLinkSpeed).append(LINK_SPEED_UNITS)
+                .append(", Frequency: ").append(mFrequency).append(FREQUENCY_UNITS)
+                .append(", Net ID: ").append(mNetworkId)
+                .append(", Metered hint: ").append(mMeteredHint)
+                .append(", score: ").append(Integer.toString(score));
+        return sb.toString();
+    }
+
+    /** Implement the Parcelable interface {@hide} */
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Implement the Parcelable interface {@hide} */
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(mNetworkId);
+        dest.writeInt(mRssi);
+        dest.writeInt(mLinkSpeed);
+        dest.writeInt(mTxLinkSpeed);
+        dest.writeInt(mRxLinkSpeed);
+        dest.writeInt(mFrequency);
+        if (mIpAddress != null) {
+            dest.writeByte((byte)1);
+            dest.writeByteArray(mIpAddress.getAddress());
+        } else {
+            dest.writeByte((byte)0);
+        }
+        if (mWifiSsid != null) {
+            dest.writeInt(1);
+            mWifiSsid.writeToParcel(dest, flags);
+        } else {
+            dest.writeInt(0);
+        }
+        dest.writeString(mBSSID);
+        dest.writeString(mMacAddress);
+        dest.writeInt(mMeteredHint ? 1 : 0);
+        dest.writeInt(mEphemeral ? 1 : 0);
+        dest.writeInt(mTrusted ? 1 : 0);
+        dest.writeInt(score);
+        dest.writeLong(txSuccess);
+        dest.writeDouble(txSuccessRate);
+        dest.writeLong(txRetries);
+        dest.writeDouble(txRetriesRate);
+        dest.writeLong(txBad);
+        dest.writeDouble(txBadRate);
+        dest.writeLong(rxSuccess);
+        dest.writeDouble(rxSuccessRate);
+        mSupplicantState.writeToParcel(dest, flags);
+        dest.writeInt(mOsuAp ? 1 : 0);
+        dest.writeString(mNetworkSuggestionOrSpecifierPackageName);
+        dest.writeString(mFqdn);
+        dest.writeString(mProviderFriendlyName);
+    }
+
+    /** Implement the Parcelable interface {@hide} */
+    @UnsupportedAppUsage
+    public static final @android.annotation.NonNull Creator<WifiInfo> CREATOR =
+        new Creator<WifiInfo>() {
+            public WifiInfo createFromParcel(Parcel in) {
+                WifiInfo info = new WifiInfo();
+                info.setNetworkId(in.readInt());
+                info.setRssi(in.readInt());
+                info.setLinkSpeed(in.readInt());
+                info.setTxLinkSpeedMbps(in.readInt());
+                info.setRxLinkSpeedMbps(in.readInt());
+                info.setFrequency(in.readInt());
+                if (in.readByte() == 1) {
+                    try {
+                        info.setInetAddress(InetAddress.getByAddress(in.createByteArray()));
+                    } catch (UnknownHostException e) {}
+                }
+                if (in.readInt() == 1) {
+                    info.mWifiSsid = WifiSsid.CREATOR.createFromParcel(in);
+                }
+                info.mBSSID = in.readString();
+                info.mMacAddress = in.readString();
+                info.mMeteredHint = in.readInt() != 0;
+                info.mEphemeral = in.readInt() != 0;
+                info.mTrusted = in.readInt() != 0;
+                info.score = in.readInt();
+                info.txSuccess = in.readLong();
+                info.txSuccessRate = in.readDouble();
+                info.txRetries = in.readLong();
+                info.txRetriesRate = in.readDouble();
+                info.txBad = in.readLong();
+                info.txBadRate = in.readDouble();
+                info.rxSuccess = in.readLong();
+                info.rxSuccessRate = in.readDouble();
+                info.mSupplicantState = SupplicantState.CREATOR.createFromParcel(in);
+                info.mOsuAp = in.readInt() != 0;
+                info.mNetworkSuggestionOrSpecifierPackageName = in.readString();
+                info.mFqdn = in.readString();
+                info.mProviderFriendlyName = in.readString();
+                return info;
+            }
+
+            public WifiInfo[] newArray(int size) {
+                return new WifiInfo[size];
+            }
+        };
+}
diff --git a/android/net/wifi/WifiManager.java b/android/net/wifi/WifiManager.java
new file mode 100644
index 0000000..4115663
--- /dev/null
+++ b/android/net/wifi/WifiManager.java
@@ -0,0 +1,4925 @@
+/*
+ * Copyright (C) 2008 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.wifi;
+
+import static android.Manifest.permission.ACCESS_FINE_LOCATION;
+import static android.Manifest.permission.ACCESS_WIFI_STATE;
+import static android.Manifest.permission.READ_WIFI_CREDENTIAL;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.SystemApi;
+import android.annotation.SystemService;
+import android.annotation.UnsupportedAppUsage;
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.pm.ParceledListSlice;
+import android.net.ConnectivityManager;
+import android.net.DhcpInfo;
+import android.net.Network;
+import android.net.NetworkCapabilities;
+import android.net.NetworkRequest;
+import android.net.wifi.hotspot2.IProvisioningCallback;
+import android.net.wifi.hotspot2.OsuProvider;
+import android.net.wifi.hotspot2.PasspointConfiguration;
+import android.net.wifi.hotspot2.ProvisioningCallback;
+import android.os.Binder;
+import android.os.Build;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
+import android.os.WorkSource;
+import android.util.Log;
+import android.util.Pair;
+import android.util.SparseArray;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.AsyncChannel;
+import com.android.internal.util.Protocol;
+import com.android.server.net.NetworkPinner;
+
+import dalvik.system.CloseGuard;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.ref.WeakReference;
+import java.net.InetAddress;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+
+/**
+ * This class provides the primary API for managing all aspects of Wi-Fi
+ * connectivity.
+ * <p>
+ * On releases before {@link android.os.Build.VERSION_CODES#N}, this object
+ * should only be obtained from an {@linkplain Context#getApplicationContext()
+ * application context}, and not from any other derived context to avoid memory
+ * leaks within the calling process.
+ * <p>
+ * It deals with several categories of items:
+ * </p>
+ * <ul>
+ * <li>The list of configured networks. The list can be viewed and updated, and
+ * attributes of individual entries can be modified.</li>
+ * <li>The currently active Wi-Fi network, if any. Connectivity can be
+ * established or torn down, and dynamic information about the state of the
+ * network can be queried.</li>
+ * <li>Results of access point scans, containing enough information to make
+ * decisions about what access point to connect to.</li>
+ * <li>It defines the names of various Intent actions that are broadcast upon
+ * any sort of change in Wi-Fi state.
+ * </ul>
+ * <p>
+ * This is the API to use when performing Wi-Fi specific operations. To perform
+ * operations that pertain to network connectivity at an abstract level, use
+ * {@link android.net.ConnectivityManager}.
+ * </p>
+ */
+@SystemService(Context.WIFI_SERVICE)
+public class WifiManager {
+
+    private static final String TAG = "WifiManager";
+    // Supplicant error codes:
+    /**
+     * The error code if there was a problem authenticating.
+     * @deprecated This is no longer supported.
+     */
+    @Deprecated
+    public static final int ERROR_AUTHENTICATING = 1;
+
+    /**
+     * The reason code if there is no error during authentication.
+     * It could also imply that there no authentication in progress,
+     * this reason code also serves as a reset value.
+     * @deprecated This is no longer supported.
+     * @hide
+     */
+    @Deprecated
+    public static final int ERROR_AUTH_FAILURE_NONE = 0;
+
+    /**
+     * The reason code if there was a timeout authenticating.
+     * @deprecated This is no longer supported.
+     * @hide
+     */
+    @Deprecated
+    public static final int ERROR_AUTH_FAILURE_TIMEOUT = 1;
+
+    /**
+     * The reason code if there was a wrong password while
+     * authenticating.
+     * @deprecated This is no longer supported.
+     * @hide
+     */
+    @Deprecated
+    public static final int ERROR_AUTH_FAILURE_WRONG_PSWD = 2;
+
+    /**
+     * The reason code if there was EAP failure while
+     * authenticating.
+     * @deprecated This is no longer supported.
+     * @hide
+     */
+    @Deprecated
+    public static final int ERROR_AUTH_FAILURE_EAP_FAILURE = 3;
+
+    /**
+     * Maximum number of active network suggestions allowed per app.
+     * @hide
+     */
+    public static final int NETWORK_SUGGESTIONS_MAX_PER_APP =
+            ActivityManager.isLowRamDeviceStatic() ? 256 : 1024;
+
+    /**
+     * Reason code if all of the network suggestions were successfully added or removed.
+     */
+    public static final int STATUS_NETWORK_SUGGESTIONS_SUCCESS = 0;
+
+    /**
+     * Reason code if there was an internal error in the platform while processing the addition or
+     * removal of suggestions.
+     */
+    public static final int STATUS_NETWORK_SUGGESTIONS_ERROR_INTERNAL = 1;
+
+    /**
+     * Reason code if the user has disallowed "android:change_wifi_state" app-ops from the app.
+     * @see android.app.AppOpsManager#unsafeCheckOp(String, int, String).
+     */
+    public static final int STATUS_NETWORK_SUGGESTIONS_ERROR_APP_DISALLOWED = 2;
+
+    /**
+     * Reason code if one or more of the network suggestions added already exists in platform's
+     * database.
+     * @see WifiNetworkSuggestion#equals(Object)
+     */
+    public static final int STATUS_NETWORK_SUGGESTIONS_ERROR_ADD_DUPLICATE = 3;
+
+    /**
+     * Reason code if the number of network suggestions provided by the app crosses the max
+     * threshold set per app.
+     * @see #getMaxNumberOfNetworkSuggestionsPerApp()
+     */
+    public static final int STATUS_NETWORK_SUGGESTIONS_ERROR_ADD_EXCEEDS_MAX_PER_APP = 4;
+
+    /**
+     * Reason code if one or more of the network suggestions removed does not exist in platform's
+     * database.
+     */
+    public static final int STATUS_NETWORK_SUGGESTIONS_ERROR_REMOVE_INVALID = 5;
+
+    /** @hide */
+    @IntDef(prefix = { "STATUS_NETWORK_SUGGESTIONS_" }, value = {
+            STATUS_NETWORK_SUGGESTIONS_SUCCESS,
+            STATUS_NETWORK_SUGGESTIONS_ERROR_INTERNAL,
+            STATUS_NETWORK_SUGGESTIONS_ERROR_APP_DISALLOWED,
+            STATUS_NETWORK_SUGGESTIONS_ERROR_ADD_DUPLICATE,
+            STATUS_NETWORK_SUGGESTIONS_ERROR_ADD_EXCEEDS_MAX_PER_APP,
+            STATUS_NETWORK_SUGGESTIONS_ERROR_REMOVE_INVALID,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface NetworkSuggestionsStatusCode {}
+
+    /**
+     * Broadcast intent action indicating whether Wi-Fi scanning is allowed currently
+     * @hide
+     */
+    public static final String WIFI_SCAN_AVAILABLE = "wifi_scan_available";
+
+    /**
+     * Extra int indicating scan availability, WIFI_STATE_ENABLED and WIFI_STATE_DISABLED
+     * @hide
+     */
+    public static final String EXTRA_SCAN_AVAILABLE = "scan_enabled";
+
+    /**
+     * Broadcast intent action indicating that the credential of a Wi-Fi network
+     * has been changed. One extra provides the ssid of the network. Another
+     * extra provides the event type, whether the credential is saved or forgot.
+     * @hide
+     */
+    @SystemApi
+    public static final String WIFI_CREDENTIAL_CHANGED_ACTION =
+            "android.net.wifi.WIFI_CREDENTIAL_CHANGED";
+    /** @hide */
+    @SystemApi
+    public static final String EXTRA_WIFI_CREDENTIAL_EVENT_TYPE = "et";
+    /** @hide */
+    @SystemApi
+    public static final String EXTRA_WIFI_CREDENTIAL_SSID = "ssid";
+    /** @hide */
+    @SystemApi
+    public static final int WIFI_CREDENTIAL_SAVED = 0;
+    /** @hide */
+    @SystemApi
+    public static final int WIFI_CREDENTIAL_FORGOT = 1;
+
+    /** @hide */
+    @SystemApi
+    public static final int PASSPOINT_HOME_NETWORK = 0;
+
+    /** @hide */
+    @SystemApi
+    public static final int PASSPOINT_ROAMING_NETWORK = 1;
+
+    /**
+     * Broadcast intent action indicating that a Passpoint provider icon has been received.
+     *
+     * Included extras:
+     * {@link #EXTRA_BSSID_LONG}
+     * {@link #EXTRA_FILENAME}
+     * {@link #EXTRA_ICON}
+     *
+     * Receiver Required Permission: android.Manifest.permission.ACCESS_WIFI_STATE
+     *
+     * <p>Note: The broadcast is only delivered to registered receivers - no manifest registered
+     * components will be launched.
+     *
+     * @hide
+     */
+    public static final String ACTION_PASSPOINT_ICON = "android.net.wifi.action.PASSPOINT_ICON";
+    /**
+     * BSSID of an AP in long representation.  The {@link #EXTRA_BSSID} contains BSSID in
+     * String representation.
+     *
+     * Retrieve with {@link android.content.Intent#getLongExtra(String, long)}.
+     *
+     * @hide
+     */
+    public static final String EXTRA_BSSID_LONG = "android.net.wifi.extra.BSSID_LONG";
+    /**
+     * Icon data.
+     *
+     * Retrieve with {@link android.content.Intent#getParcelableExtra(String)} and cast into
+     * {@link android.graphics.drawable.Icon}.
+     *
+     * @hide
+     */
+    public static final String EXTRA_ICON = "android.net.wifi.extra.ICON";
+    /**
+     * Name of a file.
+     *
+     * Retrieve with {@link android.content.Intent#getStringExtra(String)}.
+     *
+     * @hide
+     */
+    public static final String EXTRA_FILENAME = "android.net.wifi.extra.FILENAME";
+
+    /**
+     * Broadcast intent action indicating a Passpoint OSU Providers List element has been received.
+     *
+     * Included extras:
+     * {@link #EXTRA_BSSID_LONG}
+     * {@link #EXTRA_ANQP_ELEMENT_DATA}
+     *
+     * Receiver Required Permission: android.Manifest.permission.ACCESS_WIFI_STATE
+     *
+     * <p>Note: The broadcast is only delivered to registered receivers - no manifest registered
+     * components will be launched.
+     *
+     * @hide
+     */
+    public static final String ACTION_PASSPOINT_OSU_PROVIDERS_LIST =
+            "android.net.wifi.action.PASSPOINT_OSU_PROVIDERS_LIST";
+    /**
+     * Raw binary data of an ANQP (Access Network Query Protocol) element.
+     *
+     * Retrieve with {@link android.content.Intent#getByteArrayExtra(String)}.
+     *
+     * @hide
+     */
+    public static final String EXTRA_ANQP_ELEMENT_DATA =
+            "android.net.wifi.extra.ANQP_ELEMENT_DATA";
+
+    /**
+     * Broadcast intent action indicating that a Passpoint Deauth Imminent frame has been received.
+     *
+     * Included extras:
+     * {@link #EXTRA_BSSID_LONG}
+     * {@link #EXTRA_ESS}
+     * {@link #EXTRA_DELAY}
+     * {@link #EXTRA_URL}
+     *
+     * Receiver Required Permission: android.Manifest.permission.ACCESS_WIFI_STATE
+     *
+     * <p>Note: The broadcast is only delivered to registered receivers - no manifest registered
+     * components will be launched.
+     *
+     * @hide
+     */
+    public static final String ACTION_PASSPOINT_DEAUTH_IMMINENT =
+            "android.net.wifi.action.PASSPOINT_DEAUTH_IMMINENT";
+    /**
+     * Flag indicating BSS (Basic Service Set) or ESS (Extended Service Set). This will be set to
+     * {@code true} for ESS.
+     *
+     * Retrieve with {@link android.content.Intent#getBooleanExtra(String, boolean)}.
+     *
+     * @hide
+     */
+    public static final String EXTRA_ESS = "android.net.wifi.extra.ESS";
+    /**
+     * Delay in seconds.
+     *
+     * Retrieve with {@link android.content.Intent#getIntExtra(String, int)}.
+     *
+     * @hide
+     */
+    public static final String EXTRA_DELAY = "android.net.wifi.extra.DELAY";
+    /**
+     * String representation of an URL.
+     *
+     * Retrieve with {@link android.content.Intent#getStringExtra(String)}.
+     *
+     * @hide
+     */
+    public static final String EXTRA_URL = "android.net.wifi.extra.URL";
+
+    /**
+     * Broadcast intent action indicating a Passpoint subscription remediation frame has been
+     * received.
+     *
+     * Included extras:
+     * {@link #EXTRA_BSSID_LONG}
+     * {@link #EXTRA_SUBSCRIPTION_REMEDIATION_METHOD}
+     * {@link #EXTRA_URL}
+     *
+     * Receiver Required Permission: android.Manifest.permission.ACCESS_WIFI_STATE
+     *
+     * <p>Note: The broadcast is only delivered to registered receivers - no manifest registered
+     * components will be launched.
+     *
+     * @hide
+     */
+    public static final String ACTION_PASSPOINT_SUBSCRIPTION_REMEDIATION =
+            "android.net.wifi.action.PASSPOINT_SUBSCRIPTION_REMEDIATION";
+    /**
+     * The protocol supported by the subscription remediation server. The possible values are:
+     * 0 - OMA DM
+     * 1 - SOAP XML SPP
+     *
+     * Retrieve with {@link android.content.Intent#getIntExtra(String, int)}.
+     *
+     * @hide
+     */
+    public static final String EXTRA_SUBSCRIPTION_REMEDIATION_METHOD =
+            "android.net.wifi.extra.SUBSCRIPTION_REMEDIATION_METHOD";
+
+    /**
+     * Activity Action: lunch OSU (Online Sign Up) view.
+     * Included extras:
+     *
+     * {@link #EXTRA_OSU_NETWORK}: {@link Network} instance associated with OSU AP.
+     * {@link #EXTRA_URL}: String representation of a server URL used for OSU process.
+     *
+     * <p>Note: The broadcast is only delivered to registered receivers - no manifest registered
+     * components will be launched.
+     *
+     * @hide
+     */
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_PASSPOINT_LAUNCH_OSU_VIEW =
+            "android.net.wifi.action.PASSPOINT_LAUNCH_OSU_VIEW";
+
+    /**
+     * The lookup key for a {@link android.net.Network} associated with OSU server.
+     *
+     * Retrieve with {@link android.content.Intent#getParcelableExtra(String)}.
+     *
+     * @hide
+     */
+    public static final String EXTRA_OSU_NETWORK = "android.net.wifi.extra.OSU_NETWORK";
+
+    /**
+     * Broadcast intent action indicating that Wi-Fi has been enabled, disabled,
+     * enabling, disabling, or unknown. One extra provides this state as an int.
+     * Another extra provides the previous state, if available.
+     *
+     * @see #EXTRA_WIFI_STATE
+     * @see #EXTRA_PREVIOUS_WIFI_STATE
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String WIFI_STATE_CHANGED_ACTION =
+        "android.net.wifi.WIFI_STATE_CHANGED";
+    /**
+     * The lookup key for an int that indicates whether Wi-Fi is enabled,
+     * disabled, enabling, disabling, or unknown.  Retrieve it with
+     * {@link android.content.Intent#getIntExtra(String,int)}.
+     *
+     * @see #WIFI_STATE_DISABLED
+     * @see #WIFI_STATE_DISABLING
+     * @see #WIFI_STATE_ENABLED
+     * @see #WIFI_STATE_ENABLING
+     * @see #WIFI_STATE_UNKNOWN
+     */
+    public static final String EXTRA_WIFI_STATE = "wifi_state";
+    /**
+     * The previous Wi-Fi state.
+     *
+     * @see #EXTRA_WIFI_STATE
+     */
+    public static final String EXTRA_PREVIOUS_WIFI_STATE = "previous_wifi_state";
+
+    /**
+     * Wi-Fi is currently being disabled. The state will change to {@link #WIFI_STATE_DISABLED} if
+     * it finishes successfully.
+     *
+     * @see #WIFI_STATE_CHANGED_ACTION
+     * @see #getWifiState()
+     */
+    public static final int WIFI_STATE_DISABLING = 0;
+    /**
+     * Wi-Fi is disabled.
+     *
+     * @see #WIFI_STATE_CHANGED_ACTION
+     * @see #getWifiState()
+     */
+    public static final int WIFI_STATE_DISABLED = 1;
+    /**
+     * Wi-Fi is currently being enabled. The state will change to {@link #WIFI_STATE_ENABLED} if
+     * it finishes successfully.
+     *
+     * @see #WIFI_STATE_CHANGED_ACTION
+     * @see #getWifiState()
+     */
+    public static final int WIFI_STATE_ENABLING = 2;
+    /**
+     * Wi-Fi is enabled.
+     *
+     * @see #WIFI_STATE_CHANGED_ACTION
+     * @see #getWifiState()
+     */
+    public static final int WIFI_STATE_ENABLED = 3;
+    /**
+     * Wi-Fi is in an unknown state. This state will occur when an error happens while enabling
+     * or disabling.
+     *
+     * @see #WIFI_STATE_CHANGED_ACTION
+     * @see #getWifiState()
+     */
+    public static final int WIFI_STATE_UNKNOWN = 4;
+
+    /**
+     * Broadcast intent action indicating that Wi-Fi AP has been enabled, disabled,
+     * enabling, disabling, or failed.
+     *
+     * @hide
+     */
+    @SystemApi
+    public static final String WIFI_AP_STATE_CHANGED_ACTION =
+        "android.net.wifi.WIFI_AP_STATE_CHANGED";
+
+    /**
+     * The lookup key for an int that indicates whether Wi-Fi AP is enabled,
+     * disabled, enabling, disabling, or failed.  Retrieve it with
+     * {@link android.content.Intent#getIntExtra(String,int)}.
+     *
+     * @see #WIFI_AP_STATE_DISABLED
+     * @see #WIFI_AP_STATE_DISABLING
+     * @see #WIFI_AP_STATE_ENABLED
+     * @see #WIFI_AP_STATE_ENABLING
+     * @see #WIFI_AP_STATE_FAILED
+     *
+     * @hide
+     */
+    @SystemApi
+    public static final String EXTRA_WIFI_AP_STATE = "wifi_state";
+
+    /**
+     * The look up key for an int that indicates why softAP started failed
+     * currently support general and no_channel
+     * @see #SAP_START_FAILURE_GENERAL
+     * @see #SAP_START_FAILURE_NO_CHANNEL
+     *
+     * @hide
+     */
+    public static final String EXTRA_WIFI_AP_FAILURE_REASON = "wifi_ap_error_code";
+    /**
+     * The previous Wi-Fi state.
+     *
+     * @see #EXTRA_WIFI_AP_STATE
+     *
+     * @hide
+     */
+    @SystemApi
+    public static final String EXTRA_PREVIOUS_WIFI_AP_STATE = "previous_wifi_state";
+    /**
+     * The interface used for the softap.
+     *
+     * @hide
+     */
+    public static final String EXTRA_WIFI_AP_INTERFACE_NAME = "wifi_ap_interface_name";
+    /**
+     * The intended ip mode for this softap.
+     * @see #IFACE_IP_MODE_TETHERED
+     * @see #IFACE_IP_MODE_LOCAL_ONLY
+     *
+     * @hide
+     */
+    public static final String EXTRA_WIFI_AP_MODE = "wifi_ap_mode";
+
+    /** @hide */
+    @IntDef(flag = false, prefix = { "WIFI_AP_STATE_" }, value = {
+        WIFI_AP_STATE_DISABLING,
+        WIFI_AP_STATE_DISABLED,
+        WIFI_AP_STATE_ENABLING,
+        WIFI_AP_STATE_ENABLED,
+        WIFI_AP_STATE_FAILED,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface WifiApState {}
+
+    /**
+     * Wi-Fi AP is currently being disabled. The state will change to
+     * {@link #WIFI_AP_STATE_DISABLED} if it finishes successfully.
+     *
+     * @see #WIFI_AP_STATE_CHANGED_ACTION
+     * @see #getWifiApState()
+     *
+     * @hide
+     */
+    @SystemApi
+    public static final int WIFI_AP_STATE_DISABLING = 10;
+    /**
+     * Wi-Fi AP is disabled.
+     *
+     * @see #WIFI_AP_STATE_CHANGED_ACTION
+     * @see #getWifiState()
+     *
+     * @hide
+     */
+    @SystemApi
+    public static final int WIFI_AP_STATE_DISABLED = 11;
+    /**
+     * Wi-Fi AP is currently being enabled. The state will change to
+     * {@link #WIFI_AP_STATE_ENABLED} if it finishes successfully.
+     *
+     * @see #WIFI_AP_STATE_CHANGED_ACTION
+     * @see #getWifiApState()
+     *
+     * @hide
+     */
+    @SystemApi
+    public static final int WIFI_AP_STATE_ENABLING = 12;
+    /**
+     * Wi-Fi AP is enabled.
+     *
+     * @see #WIFI_AP_STATE_CHANGED_ACTION
+     * @see #getWifiApState()
+     *
+     * @hide
+     */
+    @SystemApi
+    public static final int WIFI_AP_STATE_ENABLED = 13;
+    /**
+     * Wi-Fi AP is in a failed state. This state will occur when an error occurs during
+     * enabling or disabling
+     *
+     * @see #WIFI_AP_STATE_CHANGED_ACTION
+     * @see #getWifiApState()
+     *
+     * @hide
+     */
+    @SystemApi
+    public static final int WIFI_AP_STATE_FAILED = 14;
+
+    /** @hide */
+    @IntDef(flag = false, prefix = { "SAP_START_FAILURE_" }, value = {
+        SAP_START_FAILURE_GENERAL,
+        SAP_START_FAILURE_NO_CHANNEL,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface SapStartFailure {}
+
+    /**
+     *  All other reasons for AP start failure besides {@link #SAP_START_FAILURE_NO_CHANNEL}.
+     *
+     *  @hide
+     */
+    public static final int SAP_START_FAILURE_GENERAL= 0;
+
+    /**
+     *  If Wi-Fi AP start failed, this reason code means that no legal channel exists on user
+     *  selected band due to regulatory constraints.
+     *
+     *  @hide
+     */
+    public static final int SAP_START_FAILURE_NO_CHANNEL = 1;
+
+    /**
+     * Interface IP mode unspecified.
+     *
+     * @see updateInterfaceIpState(String, int)
+     *
+     * @hide
+     */
+    public static final int IFACE_IP_MODE_UNSPECIFIED = -1;
+
+    /**
+     * Interface IP mode for configuration error.
+     *
+     * @see updateInterfaceIpState(String, int)
+     *
+     * @hide
+     */
+    public static final int IFACE_IP_MODE_CONFIGURATION_ERROR = 0;
+
+    /**
+     * Interface IP mode for tethering.
+     *
+     * @see updateInterfaceIpState(String, int)
+     *
+     * @hide
+     */
+    public static final int IFACE_IP_MODE_TETHERED = 1;
+
+    /**
+     * Interface IP mode for Local Only Hotspot.
+     *
+     * @see updateInterfaceIpState(String, int)
+     *
+     * @hide
+     */
+    public static final int IFACE_IP_MODE_LOCAL_ONLY = 2;
+
+    /**
+     * Broadcast intent action indicating that a connection to the supplicant has
+     * been established (and it is now possible
+     * to perform Wi-Fi operations) or the connection to the supplicant has been
+     * lost. One extra provides the connection state as a boolean, where {@code true}
+     * means CONNECTED.
+     * @deprecated This is no longer supported.
+     * @see #EXTRA_SUPPLICANT_CONNECTED
+     */
+    @Deprecated
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String SUPPLICANT_CONNECTION_CHANGE_ACTION =
+        "android.net.wifi.supplicant.CONNECTION_CHANGE";
+    /**
+     * The lookup key for a boolean that indicates whether a connection to
+     * the supplicant daemon has been gained or lost. {@code true} means
+     * a connection now exists.
+     * Retrieve it with {@link android.content.Intent#getBooleanExtra(String,boolean)}.
+     * @deprecated This is no longer supported.
+     */
+    @Deprecated
+    public static final String EXTRA_SUPPLICANT_CONNECTED = "connected";
+    /**
+     * Broadcast intent action indicating that the state of Wi-Fi connectivity
+     * has changed. An extra provides the new state
+     * in the form of a {@link android.net.NetworkInfo} object.
+     * @see #EXTRA_NETWORK_INFO
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String NETWORK_STATE_CHANGED_ACTION = "android.net.wifi.STATE_CHANGE";
+    /**
+     * The lookup key for a {@link android.net.NetworkInfo} object associated with the
+     * Wi-Fi network. Retrieve with
+     * {@link android.content.Intent#getParcelableExtra(String)}.
+     */
+    public static final String EXTRA_NETWORK_INFO = "networkInfo";
+    /**
+     * The lookup key for a String giving the BSSID of the access point to which
+     * we are connected. No longer used.
+     */
+    @Deprecated
+    public static final String EXTRA_BSSID = "bssid";
+    /**
+     * The lookup key for a {@link android.net.wifi.WifiInfo} object giving the
+     * information about the access point to which we are connected.
+     * No longer used.
+     */
+    @Deprecated
+    public static final String EXTRA_WIFI_INFO = "wifiInfo";
+    /**
+     * Broadcast intent action indicating that the state of establishing a connection to
+     * an access point has changed.One extra provides the new
+     * {@link SupplicantState}. Note that the supplicant state is Wi-Fi specific, and
+     * is not generally the most useful thing to look at if you are just interested in
+     * the overall state of connectivity.
+     * @see #EXTRA_NEW_STATE
+     * @see #EXTRA_SUPPLICANT_ERROR
+     * @deprecated This is no longer supported.
+     */
+    @Deprecated
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String SUPPLICANT_STATE_CHANGED_ACTION =
+        "android.net.wifi.supplicant.STATE_CHANGE";
+    /**
+     * The lookup key for a {@link SupplicantState} describing the new state
+     * Retrieve with
+     * {@link android.content.Intent#getParcelableExtra(String)}.
+     * @deprecated This is no longer supported.
+     */
+    @Deprecated
+    public static final String EXTRA_NEW_STATE = "newState";
+
+    /**
+     * The lookup key for a {@link SupplicantState} describing the supplicant
+     * error code if any
+     * Retrieve with
+     * {@link android.content.Intent#getIntExtra(String, int)}.
+     * @see #ERROR_AUTHENTICATING
+     * @deprecated This is no longer supported.
+     */
+    @Deprecated
+    public static final String EXTRA_SUPPLICANT_ERROR = "supplicantError";
+
+    /**
+     * The lookup key for a {@link SupplicantState} describing the supplicant
+     * error reason if any
+     * Retrieve with
+     * {@link android.content.Intent#getIntExtra(String, int)}.
+     * @see #ERROR_AUTH_FAILURE_#REASON_CODE
+     * @deprecated This is no longer supported.
+     * @hide
+     */
+    @Deprecated
+    public static final String EXTRA_SUPPLICANT_ERROR_REASON = "supplicantErrorReason";
+
+    /**
+     * Broadcast intent action indicating that the configured networks changed.
+     * This can be as a result of adding/updating/deleting a network. If
+     * {@link #EXTRA_MULTIPLE_NETWORKS_CHANGED} is set to true the new configuration
+     * can be retreived with the {@link #EXTRA_WIFI_CONFIGURATION} extra. If multiple
+     * Wi-Fi configurations changed, {@link #EXTRA_WIFI_CONFIGURATION} will not be present.
+     * @hide
+     */
+    @SystemApi
+    public static final String CONFIGURED_NETWORKS_CHANGED_ACTION =
+        "android.net.wifi.CONFIGURED_NETWORKS_CHANGE";
+    /**
+     * The lookup key for a (@link android.net.wifi.WifiConfiguration} object representing
+     * the changed Wi-Fi configuration when the {@link #CONFIGURED_NETWORKS_CHANGED_ACTION}
+     * broadcast is sent.
+     * @hide
+     */
+    @SystemApi
+    public static final String EXTRA_WIFI_CONFIGURATION = "wifiConfiguration";
+    /**
+     * Multiple network configurations have changed.
+     * @see #CONFIGURED_NETWORKS_CHANGED_ACTION
+     *
+     * @hide
+     */
+    @SystemApi
+    public static final String EXTRA_MULTIPLE_NETWORKS_CHANGED = "multipleChanges";
+    /**
+     * The lookup key for an integer indicating the reason a Wi-Fi network configuration
+     * has changed. Only present if {@link #EXTRA_MULTIPLE_NETWORKS_CHANGED} is {@code false}
+     * @see #CONFIGURED_NETWORKS_CHANGED_ACTION
+     * @hide
+     */
+    @SystemApi
+    public static final String EXTRA_CHANGE_REASON = "changeReason";
+    /**
+     * The configuration is new and was added.
+     * @hide
+     */
+    @SystemApi
+    public static final int CHANGE_REASON_ADDED = 0;
+    /**
+     * The configuration was removed and is no longer present in the system's list of
+     * configured networks.
+     * @hide
+     */
+    @SystemApi
+    public static final int CHANGE_REASON_REMOVED = 1;
+    /**
+     * The configuration has changed as a result of explicit action or because the system
+     * took an automated action such as disabling a malfunctioning configuration.
+     * @hide
+     */
+    @SystemApi
+    public static final int CHANGE_REASON_CONFIG_CHANGE = 2;
+    /**
+     * An access point scan has completed, and results are available.
+     * Call {@link #getScanResults()} to obtain the results.
+     * The broadcast intent may contain an extra field with the key {@link #EXTRA_RESULTS_UPDATED}
+     * and a {@code boolean} value indicating if the scan was successful.
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String SCAN_RESULTS_AVAILABLE_ACTION = "android.net.wifi.SCAN_RESULTS";
+
+    /**
+     * Lookup key for a {@code boolean} extra in intent {@link #SCAN_RESULTS_AVAILABLE_ACTION}
+     * representing if the scan was successful or not.
+     * Scans may fail for multiple reasons, these may include:
+     * <ol>
+     * <li>An app requested too many scans in a certain period of time.
+     * This may lead to additional scan request rejections via "scan throttling" for both
+     * foreground and background apps.
+     * Note: Apps holding android.Manifest.permission.NETWORK_SETTINGS permission are
+     * exempted from scan throttling.
+     * </li>
+     * <li>The device is idle and scanning is disabled.</li>
+     * <li>Wifi hardware reported a scan failure.</li>
+     * </ol>
+     * @return true scan was successful, results are updated
+     * @return false scan was not successful, results haven't been updated since previous scan
+     */
+    public static final String EXTRA_RESULTS_UPDATED = "resultsUpdated";
+
+    /**
+     * A batch of access point scans has been completed and the results areavailable.
+     * Call {@link #getBatchedScanResults()} to obtain the results.
+     * @deprecated This API is nolonger supported.
+     * Use {@link android.net.wifi.WifiScanner} API
+     * @hide
+     */
+    @Deprecated
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String BATCHED_SCAN_RESULTS_AVAILABLE_ACTION =
+            "android.net.wifi.BATCHED_RESULTS";
+
+    /**
+     * The RSSI (signal strength) has changed.
+     *
+     * Receiver Required Permission: android.Manifest.permission.ACCESS_WIFI_STATE
+     * @see {@link #EXTRA_NEW_RSSI}
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String RSSI_CHANGED_ACTION = "android.net.wifi.RSSI_CHANGED";
+    /**
+     * The lookup key for an {@code int} giving the new RSSI in dBm.
+     */
+    public static final String EXTRA_NEW_RSSI = "newRssi";
+
+    /**
+     * Broadcast intent action indicating that the link configuration
+     * changed on wifi.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public static final String LINK_CONFIGURATION_CHANGED_ACTION =
+        "android.net.wifi.LINK_CONFIGURATION_CHANGED";
+
+    /**
+     * The lookup key for a {@link android.net.LinkProperties} object associated with the
+     * Wi-Fi network. Retrieve with
+     * {@link android.content.Intent#getParcelableExtra(String)}.
+     * @hide
+     */
+    public static final String EXTRA_LINK_PROPERTIES = "linkProperties";
+
+    /**
+     * The lookup key for a {@link android.net.NetworkCapabilities} object associated with the
+     * Wi-Fi network. Retrieve with
+     * {@link android.content.Intent#getParcelableExtra(String)}.
+     * @hide
+     */
+    public static final String EXTRA_NETWORK_CAPABILITIES = "networkCapabilities";
+
+    /**
+     * The network IDs of the configured networks could have changed.
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String NETWORK_IDS_CHANGED_ACTION = "android.net.wifi.NETWORK_IDS_CHANGED";
+
+    /**
+     * Activity Action: Show a system activity that allows the user to enable
+     * scans to be available even with Wi-Fi turned off.
+     *
+     * <p>Notification of the result of this activity is posted using the
+     * {@link android.app.Activity#onActivityResult} callback. The
+     * <code>resultCode</code>
+     * will be {@link android.app.Activity#RESULT_OK} if scan always mode has
+     * been turned on or {@link android.app.Activity#RESULT_CANCELED} if the user
+     * has rejected the request or an error has occurred.
+     */
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_REQUEST_SCAN_ALWAYS_AVAILABLE =
+            "android.net.wifi.action.REQUEST_SCAN_ALWAYS_AVAILABLE";
+
+    /**
+     * Activity Action: Pick a Wi-Fi network to connect to.
+     * <p>Input: Nothing.
+     * <p>Output: Nothing.
+     */
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_PICK_WIFI_NETWORK = "android.net.wifi.PICK_WIFI_NETWORK";
+
+    /**
+     * Activity Action: Show UI to get user approval to enable WiFi.
+     * <p>Input: {@link android.content.Intent#EXTRA_PACKAGE_NAME} string extra with
+     *           the name of the app requesting the action.
+     * <p>Output: Nothing.
+     *
+     * @hide
+     */
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_REQUEST_ENABLE = "android.net.wifi.action.REQUEST_ENABLE";
+
+    /**
+     * Activity Action: Show UI to get user approval to disable WiFi.
+     * <p>Input: {@link android.content.Intent#EXTRA_PACKAGE_NAME} string extra with
+     *           the name of the app requesting the action.
+     * <p>Output: Nothing.
+     *
+     * @hide
+     */
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_REQUEST_DISABLE = "android.net.wifi.action.REQUEST_DISABLE";
+
+    /**
+     * Directed broadcast intent action indicating that the device has connected to one of the
+     * network suggestions provided by the app. This will be sent post connection to a network
+     * which was created with {@link WifiNetworkSuggestion.Builder#setIsAppInteractionRequired(
+     * boolean)}
+     * flag set.
+     * <p>
+     * Note: The broadcast is sent to the app only if it holds
+     * {@link android.Manifest.permission#ACCESS_FINE_LOCATION ACCESS_FINE_LOCATION} permission.
+     *
+     * @see #EXTRA_NETWORK_SUGGESTION
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String ACTION_WIFI_NETWORK_SUGGESTION_POST_CONNECTION =
+            "android.net.wifi.action.WIFI_NETWORK_SUGGESTION_POST_CONNECTION";
+    /**
+     * Sent as as a part of {@link #ACTION_WIFI_NETWORK_SUGGESTION_POST_CONNECTION} that holds
+     * an instance of {@link WifiNetworkSuggestion} corresponding to the connected network.
+     */
+    public static final String EXTRA_NETWORK_SUGGESTION =
+            "android.net.wifi.extra.NETWORK_SUGGESTION";
+
+    /**
+     * Internally used Wi-Fi lock mode representing the case were no locks are held.
+     * @hide
+     */
+    public static final int WIFI_MODE_NO_LOCKS_HELD = 0;
+
+    /**
+     * In this Wi-Fi lock mode, Wi-Fi will be kept active,
+     * and will behave normally, i.e., it will attempt to automatically
+     * establish a connection to a remembered access point that is
+     * within range, and will do periodic scans if there are remembered
+     * access points but none are in range.
+     *
+     * @deprecated This API is non-functional and will have no impact.
+     */
+    @Deprecated
+    public static final int WIFI_MODE_FULL = WifiProtoEnums.WIFI_MODE_FULL; // 1
+
+    /**
+     * In this Wi-Fi lock mode, Wi-Fi will be kept active,
+     * but the only operation that will be supported is initiation of
+     * scans, and the subsequent reporting of scan results. No attempts
+     * will be made to automatically connect to remembered access points,
+     * nor will periodic scans be automatically performed looking for
+     * remembered access points. Scans must be explicitly requested by
+     * an application in this mode.
+     *
+     * @deprecated This API is non-functional and will have no impact.
+     */
+    @Deprecated
+    public static final int WIFI_MODE_SCAN_ONLY = WifiProtoEnums.WIFI_MODE_SCAN_ONLY; // 2
+
+    /**
+     * In this Wi-Fi lock mode, Wi-Fi will not go to power save.
+     * This results in operating with low packet latency.
+     * The lock is only active when the device is connected to an access point.
+     * The lock is active even when the device screen is off or the acquiring application is
+     * running in the background.
+     * This mode will consume more power and hence should be used only
+     * when there is a need for this tradeoff.
+     * <p>
+     * An example use case is when a voice connection needs to be
+     * kept active even after the device screen goes off.
+     * Holding a {@link #WIFI_MODE_FULL_HIGH_PERF} lock for the
+     * duration of the voice call may improve the call quality.
+     * <p>
+     * When there is no support from the hardware, the {@link #WIFI_MODE_FULL_HIGH_PERF}
+     * lock will have no impact.
+     */
+    public static final int WIFI_MODE_FULL_HIGH_PERF = WifiProtoEnums.WIFI_MODE_FULL_HIGH_PERF; // 3
+
+    /**
+     * In this Wi-Fi lock mode, Wi-Fi will operate with a priority to achieve low latency.
+     * {@link #WIFI_MODE_FULL_LOW_LATENCY} lock has the following limitations:
+     * <ol>
+     * <li>The lock is only active when the device is connected to an access point.</li>
+     * <li>The lock is only active when the screen is on.</li>
+     * <li>The lock is only active when the acquiring app is running in the foreground.</li>
+     * </ol>
+     * Low latency mode optimizes for reduced packet latency,
+     * and as a result other performance measures may suffer when there are trade-offs to make:
+     * <ol>
+     * <li>Battery life may be reduced.</li>
+     * <li>Throughput may be reduced.</li>
+     * <li>Frequency of Wi-Fi scanning may be reduced. This may result in: </li>
+     * <ul>
+     * <li>The device may not roam or switch to the AP with highest signal quality.</li>
+     * <li>Location accuracy may be reduced.</li>
+     * </ul>
+     * </ol>
+     * <p>
+     * Example use cases are real time gaming or virtual reality applications where
+     * low latency is a key factor for user experience.
+     * <p>
+     * Note: For an app which acquires both {@link #WIFI_MODE_FULL_LOW_LATENCY} and
+     * {@link #WIFI_MODE_FULL_HIGH_PERF} locks, {@link #WIFI_MODE_FULL_LOW_LATENCY}
+     * lock will be effective when app is running in foreground and screen is on,
+     * while the {@link #WIFI_MODE_FULL_HIGH_PERF} lock will take effect otherwise.
+     */
+    public static final int WIFI_MODE_FULL_LOW_LATENCY =
+            WifiProtoEnums.WIFI_MODE_FULL_LOW_LATENCY; // 4
+
+    /** Anything worse than or equal to this will show 0 bars. */
+    @UnsupportedAppUsage
+    private static final int MIN_RSSI = -100;
+
+    /** Anything better than or equal to this will show the max bars. */
+    @UnsupportedAppUsage
+    private static final int MAX_RSSI = -55;
+
+    /**
+     * Number of RSSI levels used in the framework to initiate
+     * {@link #RSSI_CHANGED_ACTION} broadcast
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public static final int RSSI_LEVELS = 5;
+
+    /**
+     * Auto settings in the driver. The driver could choose to operate on both
+     * 2.4 GHz and 5 GHz or make a dynamic decision on selecting the band.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public static final int WIFI_FREQUENCY_BAND_AUTO = 0;
+
+    /**
+     * Operation on 5 GHz alone
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public static final int WIFI_FREQUENCY_BAND_5GHZ = 1;
+
+    /**
+     * Operation on 2.4 GHz alone
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public static final int WIFI_FREQUENCY_BAND_2GHZ = 2;
+
+    /** @hide */
+    public static final boolean DEFAULT_POOR_NETWORK_AVOIDANCE_ENABLED = false;
+
+    /* Maximum number of active locks we allow.
+     * This limit was added to prevent apps from creating a ridiculous number
+     * of locks and crashing the system by overflowing the global ref table.
+     */
+    private static final int MAX_ACTIVE_LOCKS = 50;
+
+    /* Number of currently active WifiLocks and MulticastLocks */
+    @UnsupportedAppUsage
+    private int mActiveLockCount;
+
+    private Context mContext;
+    @UnsupportedAppUsage
+    IWifiManager mService;
+    private final int mTargetSdkVersion;
+
+    private static final int INVALID_KEY = 0;
+    private int mListenerKey = 1;
+    private final SparseArray mListenerMap = new SparseArray();
+    private final Object mListenerMapLock = new Object();
+
+    private AsyncChannel mAsyncChannel;
+    private CountDownLatch mConnected;
+    private Looper mLooper;
+    private boolean mVerboseLoggingEnabled = false;
+
+    /* LocalOnlyHotspot callback message types */
+    /** @hide */
+    public static final int HOTSPOT_STARTED = 0;
+    /** @hide */
+    public static final int HOTSPOT_STOPPED = 1;
+    /** @hide */
+    public static final int HOTSPOT_FAILED = 2;
+    /** @hide */
+    public static final int HOTSPOT_OBSERVER_REGISTERED = 3;
+
+    private final Object mLock = new Object(); // lock guarding access to the following vars
+    @GuardedBy("mLock")
+    private LocalOnlyHotspotCallbackProxy mLOHSCallbackProxy;
+    @GuardedBy("mLock")
+    private LocalOnlyHotspotObserverProxy mLOHSObserverProxy;
+
+    /**
+     * Create a new WifiManager instance.
+     * Applications will almost always want to use
+     * {@link android.content.Context#getSystemService Context.getSystemService()} to retrieve
+     * the standard {@link android.content.Context#WIFI_SERVICE Context.WIFI_SERVICE}.
+     * @param context the application context
+     * @param service the Binder interface
+     * @hide - hide this because it takes in a parameter of type IWifiManager, which
+     * is a system private class.
+     */
+    public WifiManager(Context context, IWifiManager service, Looper looper) {
+        mContext = context;
+        mService = service;
+        mLooper = looper;
+        mTargetSdkVersion = context.getApplicationInfo().targetSdkVersion;
+        updateVerboseLoggingEnabledFromService();
+    }
+
+    /**
+     * Return a list of all the networks configured for the current foreground
+     * user.
+     *
+     * Not all fields of WifiConfiguration are returned. Only the following
+     * fields are filled in:
+     * <ul>
+     * <li>networkId</li>
+     * <li>SSID</li>
+     * <li>BSSID</li>
+     * <li>priority</li>
+     * <li>allowedProtocols</li>
+     * <li>allowedKeyManagement</li>
+     * <li>allowedAuthAlgorithms</li>
+     * <li>allowedPairwiseCiphers</li>
+     * <li>allowedGroupCiphers</li>
+     * </ul>
+     * @return a list of network configurations in the form of a list
+     * of {@link WifiConfiguration} objects.
+     *
+     * @deprecated
+     * a) See {@link WifiNetworkSpecifier.Builder#build()} for new
+     * mechanism to trigger connection to a Wi-Fi network.
+     * b) See {@link #addNetworkSuggestions(List)},
+     * {@link #removeNetworkSuggestions(List)} for new API to add Wi-Fi networks for consideration
+     * when auto-connecting to wifi.
+     * <b>Compatibility Note:</b> For applications targeting
+     * {@link android.os.Build.VERSION_CODES#Q} or above, this API will return an empty list,
+     * except for:
+     * <ul>
+     * <li>Device Owner (DO) & Profile Owner (PO) apps will have access to the full list.
+     * <li>Callers with Carrier privilege will receive a restricted list only containing
+     * configurations which they created.
+     * </ul>
+     */
+    @Deprecated
+    @RequiresPermission(allOf = {ACCESS_FINE_LOCATION, ACCESS_WIFI_STATE})
+    public List<WifiConfiguration> getConfiguredNetworks() {
+        try {
+            ParceledListSlice<WifiConfiguration> parceledList =
+                    mService.getConfiguredNetworks(mContext.getOpPackageName());
+            if (parceledList == null) {
+                return Collections.emptyList();
+            }
+            return parceledList.getList();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /** @hide */
+    @SystemApi
+    @RequiresPermission(allOf = {ACCESS_FINE_LOCATION, ACCESS_WIFI_STATE, READ_WIFI_CREDENTIAL})
+    public List<WifiConfiguration> getPrivilegedConfiguredNetworks() {
+        try {
+            ParceledListSlice<WifiConfiguration> parceledList =
+                    mService.getPrivilegedConfiguredNetworks(mContext.getOpPackageName());
+            if (parceledList == null) {
+                return Collections.emptyList();
+            }
+            return parceledList.getList();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns a list of all matching WifiConfigurations for a given list of ScanResult.
+     *
+     * An empty list will be returned when no configurations are installed or if no configurations
+     * match the ScanResult.
+     *
+     * @param scanResults a list of scanResult that represents the BSSID
+     * @return List that consists of {@link WifiConfiguration} and corresponding scanResults per
+     * network type({@link #PASSPOINT_HOME_NETWORK} and {@link #PASSPOINT_ROAMING_NETWORK}).
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.NETWORK_SETTINGS,
+            android.Manifest.permission.NETWORK_SETUP_WIZARD
+    })
+    @NonNull
+    public List<Pair<WifiConfiguration, Map<Integer, List<ScanResult>>>> getAllMatchingWifiConfigs(
+            @NonNull List<ScanResult> scanResults) {
+        List<Pair<WifiConfiguration, Map<Integer, List<ScanResult>>>> configs = new ArrayList<>();
+        try {
+            Map<String, Map<Integer, List<ScanResult>>> results =
+                    mService.getAllMatchingFqdnsForScanResults(
+                            scanResults);
+            if (results.isEmpty()) {
+                return configs;
+            }
+            List<WifiConfiguration> wifiConfigurations =
+                    mService.getWifiConfigsForPasspointProfiles(
+                            new ArrayList<>(results.keySet()));
+            for (WifiConfiguration configuration : wifiConfigurations) {
+                Map<Integer, List<ScanResult>> scanResultsPerNetworkType = results.get(
+                        configuration.FQDN);
+                if (scanResultsPerNetworkType != null) {
+                    configs.add(Pair.create(configuration, scanResultsPerNetworkType));
+                }
+            }
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+
+        return configs;
+    }
+
+    /**
+     * Returns a list of unique Hotspot 2.0 OSU (Online Sign-Up) providers associated with a given
+     * list of ScanResult.
+     *
+     * An empty list will be returned if no match is found.
+     *
+     * @param scanResults a list of ScanResult
+     * @return Map that consists {@link OsuProvider} and a list of matching {@link ScanResult}
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.NETWORK_SETTINGS,
+            android.Manifest.permission.NETWORK_SETUP_WIZARD
+    })
+    @NonNull
+    public Map<OsuProvider, List<ScanResult>> getMatchingOsuProviders(
+            @Nullable List<ScanResult> scanResults) {
+        if (scanResults == null) {
+            return new HashMap<>();
+        }
+        try {
+            return mService.getMatchingOsuProviders(scanResults);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns the matching Passpoint R2 configurations for given OSU (Online Sign-Up) providers.
+     *
+     * Given a list of OSU providers, this only returns OSU providers that already have Passpoint R2
+     * configurations in the device.
+     * An empty map will be returned when there is no matching Passpoint R2 configuration for the
+     * given OsuProviders.
+     *
+     * @param osuProviders a set of {@link OsuProvider}
+     * @return Map that consists of {@link OsuProvider} and matching {@link PasspointConfiguration}.
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.NETWORK_SETTINGS,
+            android.Manifest.permission.NETWORK_SETUP_WIZARD
+    })
+    @NonNull
+    public Map<OsuProvider, PasspointConfiguration> getMatchingPasspointConfigsForOsuProviders(
+            @NonNull Set<OsuProvider> osuProviders) {
+        try {
+            return mService.getMatchingPasspointConfigsForOsuProviders(
+                    new ArrayList<>(osuProviders));
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Add a new network description to the set of configured networks.
+     * The {@code networkId} field of the supplied configuration object
+     * is ignored.
+     * <p/>
+     * The new network will be marked DISABLED by default. To enable it,
+     * called {@link #enableNetwork}.
+     *
+     * @param config the set of variables that describe the configuration,
+     *            contained in a {@link WifiConfiguration} object.
+     *            If the {@link WifiConfiguration} has an Http Proxy set
+     *            the calling app must be System, or be provisioned as the Profile or Device Owner.
+     * @return the ID of the newly created network description. This is used in
+     *         other operations to specified the network to be acted upon.
+     *         Returns {@code -1} on failure.
+     *
+     * @deprecated
+     * a) See {@link WifiNetworkSpecifier.Builder#build()} for new
+     * mechanism to trigger connection to a Wi-Fi network.
+     * b) See {@link #addNetworkSuggestions(List)},
+     * {@link #removeNetworkSuggestions(List)} for new API to add Wi-Fi networks for consideration
+     * when auto-connecting to wifi.
+     * <b>Compatibility Note:</b> For applications targeting
+     * {@link android.os.Build.VERSION_CODES#Q} or above, this API will always return {@code -1}.
+     */
+    @Deprecated
+    public int addNetwork(WifiConfiguration config) {
+        if (config == null) {
+            return -1;
+        }
+        config.networkId = -1;
+        return addOrUpdateNetwork(config);
+    }
+
+    /**
+     * Update the network description of an existing configured network.
+     *
+     * @param config the set of variables that describe the configuration,
+     *            contained in a {@link WifiConfiguration} object. It may
+     *            be sparse, so that only the items that are being changed
+     *            are non-<code>null</code>. The {@code networkId} field
+     *            must be set to the ID of the existing network being updated.
+     *            If the {@link WifiConfiguration} has an Http Proxy set
+     *            the calling app must be System, or be provisioned as the Profile or Device Owner.
+     * @return Returns the {@code networkId} of the supplied
+     *         {@code WifiConfiguration} on success.
+     *         <br/>
+     *         Returns {@code -1} on failure, including when the {@code networkId}
+     *         field of the {@code WifiConfiguration} does not refer to an
+     *         existing network.
+     *
+     * @deprecated
+     * a) See {@link WifiNetworkSpecifier.Builder#build()} for new
+     * mechanism to trigger connection to a Wi-Fi network.
+     * b) See {@link #addNetworkSuggestions(List)},
+     * {@link #removeNetworkSuggestions(List)} for new API to add Wi-Fi networks for consideration
+     * when auto-connecting to wifi.
+     * <b>Compatibility Note:</b> For applications targeting
+     * {@link android.os.Build.VERSION_CODES#Q} or above, this API will always return {@code -1}.
+     */
+    @Deprecated
+    public int updateNetwork(WifiConfiguration config) {
+        if (config == null || config.networkId < 0) {
+            return -1;
+        }
+        return addOrUpdateNetwork(config);
+    }
+
+    /**
+     * Internal method for doing the RPC that creates a new network description
+     * or updates an existing one.
+     *
+     * @param config The possibly sparse object containing the variables that
+     *         are to set or updated in the network description.
+     * @return the ID of the network on success, {@code -1} on failure.
+     */
+    private int addOrUpdateNetwork(WifiConfiguration config) {
+        try {
+            return mService.addOrUpdateNetwork(config, mContext.getOpPackageName());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Interface for indicating user selection from the list of networks presented in the
+     * {@link NetworkRequestMatchCallback#onMatch(List)}.
+     *
+     * The platform will implement this callback and pass it along with the
+     * {@link NetworkRequestMatchCallback#onUserSelectionCallbackRegistration(
+     * NetworkRequestUserSelectionCallback)}. The UI component handling
+     * {@link NetworkRequestMatchCallback} will invoke {@link #select(WifiConfiguration)} or
+     * {@link #reject()} to return the user's selection back to the platform via this callback.
+     * @hide
+     */
+    public interface NetworkRequestUserSelectionCallback {
+        /**
+         * User selected this network to connect to.
+         * @param wifiConfiguration WifiConfiguration object corresponding to the network
+         *                          user selected.
+         */
+        void select(@NonNull WifiConfiguration wifiConfiguration);
+
+        /**
+         * User rejected the app's request.
+         */
+        void reject();
+    }
+
+    /**
+     * Interface for network request callback. Should be implemented by applications and passed when
+     * calling {@link #registerNetworkRequestMatchCallback(NetworkRequestMatchCallback, Handler)}.
+     *
+     * This is meant to be implemented by a UI component to present the user with a list of networks
+     * matching the app's request. The user is allowed to pick one of these networks to connect to
+     * or reject the request by the app.
+     * @hide
+     */
+    public interface NetworkRequestMatchCallback {
+        /**
+         * Invoked to register a callback to be invoked to convey user selection. The callback
+         * object paased in this method is to be invoked by the UI component after the service sends
+         * a list of matching scan networks using {@link #onMatch(List)} and user picks a network
+         * from that list.
+         *
+         * @param userSelectionCallback Callback object to send back the user selection.
+         */
+        void onUserSelectionCallbackRegistration(
+                @NonNull NetworkRequestUserSelectionCallback userSelectionCallback);
+
+        /**
+         * Invoked when the active network request is aborted, either because
+         * <li> The app released the request, OR</li>
+         * <li> Request was overridden by a new request</li>
+         * This signals the end of processing for the current request and should stop the UI
+         * component. No subsequent calls from the UI component will be handled by the platform.
+         */
+        void onAbort();
+
+        /**
+         * Invoked when a network request initiated by an app matches some networks in scan results.
+         * This may be invoked multiple times for a single network request as the platform finds new
+         * matching networks in scan results.
+         *
+         * @param scanResults List of {@link ScanResult} objects corresponding to the networks
+         *                    matching the request.
+         */
+        void onMatch(@NonNull List<ScanResult> scanResults);
+
+        /**
+         * Invoked on a successful connection with the network that the user selected
+         * via {@link NetworkRequestUserSelectionCallback}.
+         *
+         * @param wifiConfiguration WifiConfiguration object corresponding to the network that the
+         *                          user selected.
+         */
+        void onUserSelectionConnectSuccess(@NonNull WifiConfiguration wifiConfiguration);
+
+        /**
+         * Invoked on failure to establish connection with the network that the user selected
+         * via {@link NetworkRequestUserSelectionCallback}.
+         *
+         * @param wifiConfiguration WifiConfiguration object corresponding to the network
+         *                          user selected.
+         */
+        void onUserSelectionConnectFailure(@NonNull WifiConfiguration wifiConfiguration);
+    }
+
+    /**
+     * Callback proxy for NetworkRequestUserSelectionCallback objects.
+     * @hide
+     */
+    private class NetworkRequestUserSelectionCallbackProxy implements
+            NetworkRequestUserSelectionCallback {
+        private final INetworkRequestUserSelectionCallback mCallback;
+
+        NetworkRequestUserSelectionCallbackProxy(
+                INetworkRequestUserSelectionCallback callback) {
+            mCallback = callback;
+        }
+
+        @Override
+        public void select(@NonNull WifiConfiguration wifiConfiguration) {
+            if (mVerboseLoggingEnabled) {
+                Log.v(TAG, "NetworkRequestUserSelectionCallbackProxy: select "
+                        + "wificonfiguration: " + wifiConfiguration);
+            }
+            try {
+                mCallback.select(wifiConfiguration);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Failed to invoke onSelected", e);
+                throw e.rethrowFromSystemServer();
+            }
+        }
+
+        @Override
+        public void reject() {
+            if (mVerboseLoggingEnabled) {
+                Log.v(TAG, "NetworkRequestUserSelectionCallbackProxy: reject");
+            }
+            try {
+                mCallback.reject();
+            } catch (RemoteException e) {
+                Log.e(TAG, "Failed to invoke onRejected", e);
+                throw e.rethrowFromSystemServer();
+            }
+        }
+    }
+
+    /**
+     * Callback proxy for NetworkRequestMatchCallback objects.
+     * @hide
+     */
+    private class NetworkRequestMatchCallbackProxy extends INetworkRequestMatchCallback.Stub {
+        private final Handler mHandler;
+        private final NetworkRequestMatchCallback mCallback;
+
+        NetworkRequestMatchCallbackProxy(Looper looper, NetworkRequestMatchCallback callback) {
+            mHandler = new Handler(looper);
+            mCallback = callback;
+        }
+
+        @Override
+        public void onUserSelectionCallbackRegistration(
+                INetworkRequestUserSelectionCallback userSelectionCallback) {
+            if (mVerboseLoggingEnabled) {
+                Log.v(TAG, "NetworkRequestMatchCallbackProxy: "
+                        + "onUserSelectionCallbackRegistration callback: " + userSelectionCallback);
+            }
+            mHandler.post(() -> {
+                mCallback.onUserSelectionCallbackRegistration(
+                        new NetworkRequestUserSelectionCallbackProxy(userSelectionCallback));
+            });
+        }
+
+        @Override
+        public void onAbort() {
+            if (mVerboseLoggingEnabled) {
+                Log.v(TAG, "NetworkRequestMatchCallbackProxy: onAbort");
+            }
+            mHandler.post(() -> {
+                mCallback.onAbort();
+            });
+        }
+
+        @Override
+        public void onMatch(List<ScanResult> scanResults) {
+            if (mVerboseLoggingEnabled) {
+                Log.v(TAG, "NetworkRequestMatchCallbackProxy: onMatch scanResults: "
+                        + scanResults);
+            }
+            mHandler.post(() -> {
+                mCallback.onMatch(scanResults);
+            });
+        }
+
+        @Override
+        public void onUserSelectionConnectSuccess(WifiConfiguration wifiConfiguration) {
+            if (mVerboseLoggingEnabled) {
+                Log.v(TAG, "NetworkRequestMatchCallbackProxy: onUserSelectionConnectSuccess "
+                        + " wificonfiguration: " + wifiConfiguration);
+            }
+            mHandler.post(() -> {
+                mCallback.onUserSelectionConnectSuccess(wifiConfiguration);
+            });
+        }
+
+        @Override
+        public void onUserSelectionConnectFailure(WifiConfiguration wifiConfiguration) {
+            if (mVerboseLoggingEnabled) {
+                Log.v(TAG, "NetworkRequestMatchCallbackProxy: onUserSelectionConnectFailure"
+                        + " wificonfiguration: " + wifiConfiguration);
+            }
+            mHandler.post(() -> {
+                mCallback.onUserSelectionConnectFailure(wifiConfiguration);
+            });
+        }
+    }
+
+    /**
+     * Registers a callback for NetworkRequest matches. See {@link NetworkRequestMatchCallback}.
+     * Caller can unregister a previously registered callback using
+     * {@link #unregisterNetworkRequestMatchCallback(NetworkRequestMatchCallback)}
+     * <p>
+     * Applications should have the
+     * {@link android.Manifest.permission#NETWORK_SETTINGS} permission. Callers
+     * without the permission will trigger a {@link java.lang.SecurityException}.
+     * <p>
+     *
+     * @param callback Callback for network match events
+     * @param handler  The Handler on whose thread to execute the callbacks of the {@code callback}
+     *                 object. If null, then the application's main thread will be used.
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.NETWORK_SETTINGS)
+    public void registerNetworkRequestMatchCallback(@NonNull NetworkRequestMatchCallback callback,
+                                                    @Nullable Handler handler) {
+        if (callback == null) throw new IllegalArgumentException("callback cannot be null");
+        Log.v(TAG, "registerNetworkRequestMatchCallback: callback=" + callback
+                + ", handler=" + handler);
+
+        Looper looper = (handler == null) ? mContext.getMainLooper() : handler.getLooper();
+        Binder binder = new Binder();
+        try {
+            mService.registerNetworkRequestMatchCallback(
+                    binder, new NetworkRequestMatchCallbackProxy(looper, callback),
+                    callback.hashCode());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Unregisters a callback for NetworkRequest matches. See {@link NetworkRequestMatchCallback}.
+     * <p>
+     * Applications should have the
+     * {@link android.Manifest.permission#NETWORK_SETTINGS} permission. Callers
+     * without the permission will trigger a {@link java.lang.SecurityException}.
+     * <p>
+     *
+     * @param callback Callback for network match events
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.NETWORK_SETTINGS)
+    public void unregisterNetworkRequestMatchCallback(
+            @NonNull NetworkRequestMatchCallback callback) {
+        if (callback == null) throw new IllegalArgumentException("callback cannot be null");
+        Log.v(TAG, "unregisterNetworkRequestMatchCallback: callback=" + callback);
+
+        try {
+            mService.unregisterNetworkRequestMatchCallback(callback.hashCode());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Provide a list of network suggestions to the device. See {@link WifiNetworkSuggestion}
+     * for a detailed explanation of the parameters.
+     * When the device decides to connect to one of the provided network suggestions, platform sends
+     * a directed broadcast {@link #ACTION_WIFI_NETWORK_SUGGESTION_POST_CONNECTION} to the app if
+     * the network was created with {@link WifiNetworkSuggestion.Builder
+     * #setIsAppInteractionRequired()} flag set and the app holds
+     * {@link android.Manifest.permission#ACCESS_FINE_LOCATION ACCESS_FINE_LOCATION} permission.
+     *<p>
+     * NOTE:
+     * <li> These networks are just a suggestion to the platform. The platform will ultimately
+     * decide on which network the device connects to. </li>
+     * <li> When an app is uninstalled, all its suggested networks are discarded. If the device is
+     * currently connected to a suggested network which is being removed then the device will
+     * disconnect from that network.</li>
+     * <li> No in-place modification of existing suggestions are allowed. Apps are expected to
+     * remove suggestions using {@link #removeNetworkSuggestions(List)} and then add the modified
+     * suggestion back using this API.</li>
+     *
+     * @param networkSuggestions List of network suggestions provided by the app.
+     * @return Status code for the operation. One of the STATUS_NETWORK_SUGGESTIONS_ values.
+     * {@link WifiNetworkSuggestion#equals(Object)} any previously provided suggestions by the app.
+     * @throws {@link SecurityException} if the caller is missing required permissions.
+     */
+    @RequiresPermission(android.Manifest.permission.CHANGE_WIFI_STATE)
+    public @NetworkSuggestionsStatusCode int addNetworkSuggestions(
+            @NonNull List<WifiNetworkSuggestion> networkSuggestions) {
+        try {
+            return mService.addNetworkSuggestions(networkSuggestions, mContext.getOpPackageName());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Remove some or all of the network suggestions that were previously provided by the app.
+     * See {@link WifiNetworkSuggestion} for a detailed explanation of the parameters.
+     * See {@link WifiNetworkSuggestion#equals(Object)} for the equivalence evaluation used.
+     *
+     * @param networkSuggestions List of network suggestions to be removed. Pass an empty list
+     *                           to remove all the previous suggestions provided by the app.
+     * @return Status code for the operation. One of the STATUS_NETWORK_SUGGESTIONS_ values.
+     * Any matching suggestions are removed from the device and will not be considered for any
+     * further connection attempts.
+     */
+    @RequiresPermission(android.Manifest.permission.CHANGE_WIFI_STATE)
+    public @NetworkSuggestionsStatusCode int removeNetworkSuggestions(
+            @NonNull List<WifiNetworkSuggestion> networkSuggestions) {
+        try {
+            return mService.removeNetworkSuggestions(
+                    networkSuggestions, mContext.getOpPackageName());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns the max number of network suggestions that are allowed per app on the device.
+     * @see #addNetworkSuggestions(List)
+     * @see #removeNetworkSuggestions(List)
+     */
+    public int getMaxNumberOfNetworkSuggestionsPerApp() {
+        return NETWORK_SUGGESTIONS_MAX_PER_APP;
+    }
+
+    /**
+     * Add or update a Passpoint configuration.  The configuration provides a credential
+     * for connecting to Passpoint networks that are operated by the Passpoint
+     * service provider specified in the configuration.
+     *
+     * Each configuration is uniquely identified by its FQDN (Fully Qualified Domain
+     * Name).  In the case when there is an existing configuration with the same
+     * FQDN, the new configuration will replace the existing configuration.
+     *
+     * @param config The Passpoint configuration to be added
+     * @throws IllegalArgumentException if configuration is invalid or Passpoint is not enabled on
+     *                                  the device.
+     */
+    public void addOrUpdatePasspointConfiguration(PasspointConfiguration config) {
+        try {
+            if (!mService.addOrUpdatePasspointConfiguration(config, mContext.getOpPackageName())) {
+                throw new IllegalArgumentException();
+            }
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Remove the Passpoint configuration identified by its FQDN (Fully Qualified Domain Name).
+     *
+     * @param fqdn The FQDN of the Passpoint configuration to be removed
+     * @throws IllegalArgumentException if no configuration is associated with the given FQDN or
+     *                                  Passpoint is not enabled on the device.
+     * @deprecated This is no longer supported.
+     */
+    @Deprecated
+    @RequiresPermission(android.Manifest.permission.NETWORK_SETTINGS)
+    public void removePasspointConfiguration(String fqdn) {
+        try {
+            if (!mService.removePasspointConfiguration(fqdn, mContext.getOpPackageName())) {
+                throw new IllegalArgumentException();
+            }
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Return the list of installed Passpoint configurations.
+     *
+     * An empty list will be returned when no configurations are installed.
+     *
+     * @return A list of {@link PasspointConfiguration}
+     * @deprecated This is no longer supported.
+     */
+    @Deprecated
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.NETWORK_SETTINGS,
+            android.Manifest.permission.NETWORK_SETUP_WIZARD
+    })
+    public List<PasspointConfiguration> getPasspointConfigurations() {
+        try {
+            return mService.getPasspointConfigurations(mContext.getOpPackageName());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Query for a Hotspot 2.0 release 2 OSU icon file. An {@link #ACTION_PASSPOINT_ICON} intent
+     * will be broadcasted once the request is completed.  The presence of the intent extra
+     * {@link #EXTRA_ICON} will indicate the result of the request.
+     * A missing intent extra {@link #EXTRA_ICON} will indicate a failure.
+     *
+     * @param bssid The BSSID of the AP
+     * @param fileName Name of the icon file (remote file) to query from the AP
+     *
+     * @throws UnsupportedOperationException if Passpoint is not enabled on the device.
+     * @hide
+     */
+    public void queryPasspointIcon(long bssid, String fileName) {
+        try {
+            mService.queryPasspointIcon(bssid, fileName);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Match the currently associated network against the SP matching the given FQDN
+     * @param fqdn FQDN of the SP
+     * @return ordinal [HomeProvider, RoamingProvider, Incomplete, None, Declined]
+     * @hide
+     */
+    public int matchProviderWithCurrentNetwork(String fqdn) {
+        try {
+            return mService.matchProviderWithCurrentNetwork(fqdn);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Deauthenticate and set the re-authentication hold off time for the current network
+     * @param holdoff hold off time in milliseconds
+     * @param ess set if the hold off pertains to an ESS rather than a BSS
+     * @hide
+     */
+    public void deauthenticateNetwork(long holdoff, boolean ess) {
+        try {
+            mService.deauthenticateNetwork(holdoff, ess);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Remove the specified network from the list of configured networks.
+     * This may result in the asynchronous delivery of state change
+     * events.
+     *
+     * Applications are not allowed to remove networks created by other
+     * applications.
+     *
+     * @param netId the ID of the network as returned by {@link #addNetwork} or {@link
+     *        #getConfiguredNetworks}.
+     * @return {@code true} if the operation succeeded
+     *
+     * @deprecated
+     * a) See {@link WifiNetworkSpecifier.Builder#build()} for new
+     * mechanism to trigger connection to a Wi-Fi network.
+     * b) See {@link #addNetworkSuggestions(List)},
+     * {@link #removeNetworkSuggestions(List)} for new API to add Wi-Fi networks for consideration
+     * when auto-connecting to wifi.
+     * <b>Compatibility Note:</b> For applications targeting
+     * {@link android.os.Build.VERSION_CODES#Q} or above, this API will always return false.
+     */
+    @Deprecated
+    public boolean removeNetwork(int netId) {
+        try {
+            return mService.removeNetwork(netId, mContext.getOpPackageName());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Allow a previously configured network to be associated with. If
+     * <code>attemptConnect</code> is true, an attempt to connect to the selected
+     * network is initiated. This may result in the asynchronous delivery
+     * of state change events.
+     * <p>
+     * <b>Note:</b> Network communication may not use Wi-Fi even if Wi-Fi is connected;
+     * traffic may instead be sent through another network, such as cellular data,
+     * Bluetooth tethering, or Ethernet. For example, traffic will never use a
+     * Wi-Fi network that does not provide Internet access (e.g. a wireless
+     * printer), if another network that does offer Internet access (e.g.
+     * cellular data) is available. Applications that need to ensure that their
+     * network traffic uses Wi-Fi should use APIs such as
+     * {@link Network#bindSocket(java.net.Socket)},
+     * {@link Network#openConnection(java.net.URL)}, or
+     * {@link ConnectivityManager#bindProcessToNetwork} to do so.
+     *
+     * Applications are not allowed to enable networks created by other
+     * applications.
+     *
+     * @param netId the ID of the network as returned by {@link #addNetwork} or {@link
+     *        #getConfiguredNetworks}.
+     * @param attemptConnect The way to select a particular network to connect to is specify
+     *        {@code true} for this parameter.
+     * @return {@code true} if the operation succeeded
+     *
+     * @deprecated
+     * a) See {@link WifiNetworkSpecifier.Builder#build()} for new
+     * mechanism to trigger connection to a Wi-Fi network.
+     * b) See {@link #addNetworkSuggestions(List)},
+     * {@link #removeNetworkSuggestions(List)} for new API to add Wi-Fi networks for consideration
+     * when auto-connecting to wifi.
+     * <b>Compatibility Note:</b> For applications targeting
+     * {@link android.os.Build.VERSION_CODES#Q} or above, this API will always return false.
+     */
+    @Deprecated
+    public boolean enableNetwork(int netId, boolean attemptConnect) {
+        final boolean pin = attemptConnect && mTargetSdkVersion < Build.VERSION_CODES.LOLLIPOP;
+        if (pin) {
+            NetworkRequest request = new NetworkRequest.Builder()
+                    .clearCapabilities()
+                    .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
+                    .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+                    .build();
+            NetworkPinner.pin(mContext, request);
+        }
+
+        boolean success;
+        try {
+            success = mService.enableNetwork(netId, attemptConnect, mContext.getOpPackageName());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+
+        if (pin && !success) {
+            NetworkPinner.unpin();
+        }
+
+        return success;
+    }
+
+    /**
+     * Disable a configured network. The specified network will not be
+     * a candidate for associating. This may result in the asynchronous
+     * delivery of state change events.
+     *
+     * Applications are not allowed to disable networks created by other
+     * applications.
+     *
+     * @param netId the ID of the network as returned by {@link #addNetwork} or {@link
+     *        #getConfiguredNetworks}.
+     * @return {@code true} if the operation succeeded
+     *
+     * @deprecated
+     * a) See {@link WifiNetworkSpecifier.Builder#build()} for new
+     * mechanism to trigger connection to a Wi-Fi network.
+     * b) See {@link #addNetworkSuggestions(List)},
+     * {@link #removeNetworkSuggestions(List)} for new API to add Wi-Fi networks for consideration
+     * when auto-connecting to wifi.
+     * <b>Compatibility Note:</b> For applications targeting
+     * {@link android.os.Build.VERSION_CODES#Q} or above, this API will always return false.
+     */
+    @Deprecated
+    public boolean disableNetwork(int netId) {
+        try {
+            return mService.disableNetwork(netId, mContext.getOpPackageName());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Disassociate from the currently active access point. This may result
+     * in the asynchronous delivery of state change events.
+     * @return {@code true} if the operation succeeded
+     *
+     * @deprecated
+     * a) See {@link WifiNetworkSpecifier.Builder#build()} for new
+     * mechanism to trigger connection to a Wi-Fi network.
+     * b) See {@link #addNetworkSuggestions(List)},
+     * {@link #removeNetworkSuggestions(List)} for new API to add Wi-Fi networks for consideration
+     * when auto-connecting to wifi.
+     * <b>Compatibility Note:</b> For applications targeting
+     * {@link android.os.Build.VERSION_CODES#Q} or above, this API will always return false.
+     */
+    @Deprecated
+    public boolean disconnect() {
+        try {
+            return mService.disconnect(mContext.getOpPackageName());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Reconnect to the currently active access point, if we are currently
+     * disconnected. This may result in the asynchronous delivery of state
+     * change events.
+     * @return {@code true} if the operation succeeded
+     *
+     * @deprecated
+     * a) See {@link WifiNetworkSpecifier.Builder#build()} for new
+     * mechanism to trigger connection to a Wi-Fi network.
+     * b) See {@link #addNetworkSuggestions(List)},
+     * {@link #removeNetworkSuggestions(List)} for new API to add Wi-Fi networks for consideration
+     * when auto-connecting to wifi.
+     * <b>Compatibility Note:</b> For applications targeting
+     * {@link android.os.Build.VERSION_CODES#Q} or above, this API will always return false.
+     */
+    @Deprecated
+    public boolean reconnect() {
+        try {
+            return mService.reconnect(mContext.getOpPackageName());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Reconnect to the currently active access point, even if we are already
+     * connected. This may result in the asynchronous delivery of state
+     * change events.
+     * @return {@code true} if the operation succeeded
+     *
+     * @deprecated
+     * a) See {@link WifiNetworkSpecifier.Builder#build()} for new
+     * mechanism to trigger connection to a Wi-Fi network.
+     * b) See {@link #addNetworkSuggestions(List)},
+     * {@link #removeNetworkSuggestions(List)} for new API to add Wi-Fi networks for consideration
+     * when auto-connecting to wifi.
+     * <b>Compatibility Note:</b> For applications targeting
+     * {@link android.os.Build.VERSION_CODES#Q} or above, this API will always return false.
+     */
+    @Deprecated
+    public boolean reassociate() {
+        try {
+            return mService.reassociate(mContext.getOpPackageName());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Check that the supplicant daemon is responding to requests.
+     * @return {@code true} if we were able to communicate with the supplicant and
+     * it returned the expected response to the PING message.
+     * @deprecated Will return the output of {@link #isWifiEnabled()} instead.
+     */
+    @Deprecated
+    public boolean pingSupplicant() {
+        return isWifiEnabled();
+    }
+
+    /** @hide */
+    public static final int WIFI_FEATURE_INFRA            = 0x0001;  // Basic infrastructure mode
+    /** @hide */
+    public static final int WIFI_FEATURE_INFRA_5G         = 0x0002;  // Support for 5 GHz Band
+    /** @hide */
+    public static final int WIFI_FEATURE_PASSPOINT        = 0x0004;  // Support for GAS/ANQP
+    /** @hide */
+    public static final int WIFI_FEATURE_P2P              = 0x0008;  // Wifi-Direct
+    /** @hide */
+    public static final int WIFI_FEATURE_MOBILE_HOTSPOT   = 0x0010;  // Soft AP
+    /** @hide */
+    public static final int WIFI_FEATURE_SCANNER          = 0x0020;  // WifiScanner APIs
+    /** @hide */
+    public static final int WIFI_FEATURE_AWARE            = 0x0040;  // Wi-Fi AWare networking
+    /** @hide */
+    public static final int WIFI_FEATURE_D2D_RTT          = 0x0080;  // Device-to-device RTT
+    /** @hide */
+    public static final int WIFI_FEATURE_D2AP_RTT         = 0x0100;  // Device-to-AP RTT
+    /** @hide */
+    public static final int WIFI_FEATURE_BATCH_SCAN       = 0x0200;  // Batched Scan (deprecated)
+    /** @hide */
+    public static final int WIFI_FEATURE_PNO              = 0x0400;  // Preferred network offload
+    /** @hide */
+    public static final int WIFI_FEATURE_ADDITIONAL_STA   = 0x0800;  // Support for two STAs
+    /** @hide */
+    public static final int WIFI_FEATURE_TDLS             = 0x1000;  // Tunnel directed link setup
+    /** @hide */
+    public static final int WIFI_FEATURE_TDLS_OFFCHANNEL  = 0x2000;  // Support for TDLS off channel
+    /** @hide */
+    public static final int WIFI_FEATURE_EPR              = 0x4000;  // Enhanced power reporting
+    /** @hide */
+    public static final int WIFI_FEATURE_AP_STA           = 0x8000;  // AP STA Concurrency
+    /** @hide */
+    public static final int WIFI_FEATURE_LINK_LAYER_STATS = 0x10000; // Link layer stats collection
+    /** @hide */
+    public static final int WIFI_FEATURE_LOGGER           = 0x20000; // WiFi Logger
+    /** @hide */
+    public static final int WIFI_FEATURE_HAL_EPNO         = 0x40000; // Enhanced PNO
+    /** @hide */
+    public static final int WIFI_FEATURE_RSSI_MONITOR     = 0x80000; // RSSI Monitor
+    /** @hide */
+    public static final int WIFI_FEATURE_MKEEP_ALIVE      = 0x100000; // mkeep_alive
+    /** @hide */
+    public static final int WIFI_FEATURE_CONFIG_NDO       = 0x200000; // ND offload
+    /** @hide */
+    public static final int WIFI_FEATURE_TRANSMIT_POWER   = 0x400000; // Capture transmit power
+    /** @hide */
+    public static final int WIFI_FEATURE_CONTROL_ROAMING  = 0x800000; // Control firmware roaming
+    /** @hide */
+    public static final int WIFI_FEATURE_IE_WHITELIST     = 0x1000000; // Probe IE white listing
+    /** @hide */
+    public static final int WIFI_FEATURE_SCAN_RAND        = 0x2000000; // Random MAC & Probe seq
+    /** @hide */
+    public static final int WIFI_FEATURE_TX_POWER_LIMIT   = 0x4000000; // Set Tx power limit
+    /** @hide */
+    public static final int WIFI_FEATURE_WPA3_SAE         = 0x8000000; // WPA3-Personal SAE
+    /** @hide */
+    public static final int WIFI_FEATURE_WPA3_SUITE_B     = 0x10000000; // WPA3-Enterprise Suite-B
+    /** @hide */
+    public static final int WIFI_FEATURE_OWE              = 0x20000000; // Enhanced Open
+    /** @hide */
+    public static final int WIFI_FEATURE_LOW_LATENCY      = 0x40000000; // Low Latency modes
+    /** @hide */
+    public static final int WIFI_FEATURE_DPP              = 0x80000000; // DPP (Easy-Connect)
+    /** @hide */
+    public static final long WIFI_FEATURE_P2P_RAND_MAC    = 0x100000000L; // Random P2P MAC
+
+    private long getSupportedFeatures() {
+        try {
+            return mService.getSupportedFeatures();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    private boolean isFeatureSupported(long feature) {
+        return (getSupportedFeatures() & feature) == feature;
+    }
+    /**
+     * @return true if this adapter supports 5 GHz band
+     */
+    public boolean is5GHzBandSupported() {
+        return isFeatureSupported(WIFI_FEATURE_INFRA_5G);
+    }
+
+    /**
+     * @return true if this adapter supports Passpoint
+     * @hide
+     */
+    public boolean isPasspointSupported() {
+        return isFeatureSupported(WIFI_FEATURE_PASSPOINT);
+    }
+
+    /**
+     * @return true if this adapter supports WifiP2pManager (Wi-Fi Direct)
+     */
+    public boolean isP2pSupported() {
+        return isFeatureSupported(WIFI_FEATURE_P2P);
+    }
+
+    /**
+     * @return true if this adapter supports portable Wi-Fi hotspot
+     * @hide
+     */
+    @SystemApi
+    public boolean isPortableHotspotSupported() {
+        return isFeatureSupported(WIFI_FEATURE_MOBILE_HOTSPOT);
+    }
+
+    /**
+     * @return true if this adapter supports WifiScanner APIs
+     * @hide
+     */
+    @SystemApi
+    public boolean isWifiScannerSupported() {
+        return isFeatureSupported(WIFI_FEATURE_SCANNER);
+    }
+
+    /**
+     * @return true if this adapter supports Neighbour Awareness Network APIs
+     * @hide
+     */
+    public boolean isWifiAwareSupported() {
+        return isFeatureSupported(WIFI_FEATURE_AWARE);
+    }
+
+    /**
+     * @deprecated Please use {@link android.content.pm.PackageManager#hasSystemFeature(String)}
+     * with {@link android.content.pm.PackageManager#FEATURE_WIFI_RTT} and
+     * {@link android.content.pm.PackageManager#FEATURE_WIFI_AWARE}.
+     *
+     * @return true if this adapter supports Device-to-device RTT
+     * @hide
+     */
+    @Deprecated
+    @SystemApi
+    public boolean isDeviceToDeviceRttSupported() {
+        return isFeatureSupported(WIFI_FEATURE_D2D_RTT);
+    }
+
+    /**
+     * @deprecated Please use {@link android.content.pm.PackageManager#hasSystemFeature(String)}
+     * with {@link android.content.pm.PackageManager#FEATURE_WIFI_RTT}.
+     *
+     * @return true if this adapter supports Device-to-AP RTT
+     */
+    @Deprecated
+    public boolean isDeviceToApRttSupported() {
+        return isFeatureSupported(WIFI_FEATURE_D2AP_RTT);
+    }
+
+    /**
+     * @return true if this adapter supports offloaded connectivity scan
+     */
+    public boolean isPreferredNetworkOffloadSupported() {
+        return isFeatureSupported(WIFI_FEATURE_PNO);
+    }
+
+    /**
+     * @return true if this adapter supports multiple simultaneous connections
+     * @hide
+     */
+    public boolean isAdditionalStaSupported() {
+        return isFeatureSupported(WIFI_FEATURE_ADDITIONAL_STA);
+    }
+
+    /**
+     * @return true if this adapter supports Tunnel Directed Link Setup
+     */
+    public boolean isTdlsSupported() {
+        return isFeatureSupported(WIFI_FEATURE_TDLS);
+    }
+
+    /**
+     * @return true if this adapter supports Off Channel Tunnel Directed Link Setup
+     * @hide
+     */
+    public boolean isOffChannelTdlsSupported() {
+        return isFeatureSupported(WIFI_FEATURE_TDLS_OFFCHANNEL);
+    }
+
+    /**
+     * @return true if this adapter supports advanced power/performance counters
+     */
+    public boolean isEnhancedPowerReportingSupported() {
+        return isFeatureSupported(WIFI_FEATURE_LINK_LAYER_STATS);
+    }
+
+    /**
+     * Return the record of {@link WifiActivityEnergyInfo} object that
+     * has the activity and energy info. This can be used to ascertain what
+     * the controller has been up to, since the last sample.
+     *
+     * @return a record with {@link WifiActivityEnergyInfo} or null if
+     * report is unavailable or unsupported
+     * @hide
+     */
+    public WifiActivityEnergyInfo getControllerActivityEnergyInfo() {
+        if (mService == null) return null;
+        try {
+            synchronized(this) {
+                return mService.reportActivityInfo();
+            }
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Request a scan for access points. Returns immediately. The availability
+     * of the results is made known later by means of an asynchronous event sent
+     * on completion of the scan.
+     * <p>
+     * To initiate a Wi-Fi scan, declare the
+     * {@link android.Manifest.permission#CHANGE_WIFI_STATE}
+     * permission in the manifest, and perform these steps:
+     * </p>
+     * <ol style="1">
+     * <li>Invoke the following method:
+     * {@code ((WifiManager) getSystemService(WIFI_SERVICE)).startScan()}</li>
+     * <li>
+     * Register a BroadcastReceiver to listen to
+     * {@code SCAN_RESULTS_AVAILABLE_ACTION}.</li>
+     * <li>When a broadcast is received, call:
+     * {@code ((WifiManager) getSystemService(WIFI_SERVICE)).getScanResults()}</li>
+     * </ol>
+     * @return {@code true} if the operation succeeded, i.e., the scan was initiated.
+     * @deprecated The ability for apps to trigger scan requests will be removed in a future
+     * release.
+     */
+    @Deprecated
+    public boolean startScan() {
+        return startScan(null);
+    }
+
+    /** @hide */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.UPDATE_DEVICE_STATS)
+    public boolean startScan(WorkSource workSource) {
+        try {
+            String packageName = mContext.getOpPackageName();
+            return mService.startScan(packageName);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * WPS has been deprecated from Client mode operation.
+     *
+     * @return null
+     * @hide
+     * @deprecated This API is deprecated
+     */
+    public String getCurrentNetworkWpsNfcConfigurationToken() {
+        return null;
+    }
+
+    /**
+     * Return dynamic information about the current Wi-Fi connection, if any is active.
+     * <p>
+     * In the connected state, access to the SSID and BSSID requires
+     * the same permissions as {@link #getScanResults}. If such access is not allowed,
+     * {@link WifiInfo#getSSID} will return {@code "<unknown ssid>"} and
+     * {@link WifiInfo#getBSSID} will return {@code "02:00:00:00:00:00"}.
+     *
+     * @return the Wi-Fi information, contained in {@link WifiInfo}.
+     */
+    public WifiInfo getConnectionInfo() {
+        try {
+            return mService.getConnectionInfo(mContext.getOpPackageName());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Return the results of the latest access point scan.
+     * @return the list of access points found in the most recent scan. An app must hold
+     * {@link android.Manifest.permission#ACCESS_FINE_LOCATION ACCESS_FINE_LOCATION} permission
+     * in order to get valid results.
+     */
+    public List<ScanResult> getScanResults() {
+        try {
+            return mService.getScanResults(mContext.getOpPackageName());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Check if scanning is always available.
+     *
+     * If this return {@code true}, apps can issue {@link #startScan} and fetch scan results
+     * even when Wi-Fi is turned off.
+     *
+     * To change this setting, see {@link #ACTION_REQUEST_SCAN_ALWAYS_AVAILABLE}.
+     * @deprecated The ability for apps to trigger scan requests will be removed in a future
+     * release.
+     */
+    @Deprecated
+    public boolean isScanAlwaysAvailable() {
+        try {
+            return mService.isScanAlwaysAvailable();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Tell the device to persist the current list of configured networks.
+     * <p>
+     * Note: It is possible for this method to change the network IDs of
+     * existing networks. You should assume the network IDs can be different
+     * after calling this method.
+     *
+     * @return {@code false}.
+     * @deprecated There is no need to call this method -
+     * {@link #addNetwork(WifiConfiguration)}, {@link #updateNetwork(WifiConfiguration)}
+     * and {@link #removeNetwork(int)} already persist the configurations automatically.
+     */
+    @Deprecated
+    public boolean saveConfiguration() {
+        return false;
+    }
+
+    /**
+     * Set the country code.
+     * @param countryCode country code in ISO 3166 format.
+     *
+     * @hide
+     */
+    public void setCountryCode(@NonNull String country) {
+        try {
+            mService.setCountryCode(country);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+    * get the country code.
+    * @return the country code in ISO 3166 format.
+    *
+    * @hide
+    */
+    @UnsupportedAppUsage
+    public String getCountryCode() {
+       try {
+           String country = mService.getCountryCode();
+           return country;
+       } catch (RemoteException e) {
+           throw e.rethrowFromSystemServer();
+       }
+    }
+
+    /**
+     * Check if the chipset supports dual frequency band (2.4 GHz and 5 GHz)
+     * @return {@code true} if supported, {@code false} otherwise.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public boolean isDualBandSupported() {
+        try {
+            return mService.isDualBandSupported();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Check if the chipset requires conversion of 5GHz Only apBand to ANY.
+     * @return {@code true} if required, {@code false} otherwise.
+     * @hide
+     */
+    public boolean isDualModeSupported() {
+        try {
+            return mService.needs5GHzToAnyApBandConversion();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Return the DHCP-assigned addresses from the last successful DHCP request,
+     * if any.
+     * @return the DHCP information
+     */
+    public DhcpInfo getDhcpInfo() {
+        try {
+            return mService.getDhcpInfo();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Enable or disable Wi-Fi.
+     * <p>
+     * Applications must have the {@link android.Manifest.permission#CHANGE_WIFI_STATE}
+     * permission to toggle wifi.
+     *
+     * @param enabled {@code true} to enable, {@code false} to disable.
+     * @return {@code false} if the request cannot be satisfied; {@code true} indicates that wifi is
+     *         either already in the requested state, or in progress toward the requested state.
+     * @throws  {@link java.lang.SecurityException} if the caller is missing required permissions.
+     *
+     * @deprecated Starting with Build.VERSION_CODES#Q, applications are not allowed to
+     * enable/disable Wi-Fi.
+     * <b>Compatibility Note:</b> For applications targeting
+     * {@link android.os.Build.VERSION_CODES#Q} or above, this API will always return {@code false}
+     * and will have no effect. If apps are targeting an older SDK (
+     * {@link android.os.Build.VERSION_CODES#P} or below), they can continue to use this API.
+     */
+    @Deprecated
+    public boolean setWifiEnabled(boolean enabled) {
+        try {
+            return mService.setWifiEnabled(mContext.getOpPackageName(), enabled);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Gets the Wi-Fi enabled state.
+     * @return One of {@link #WIFI_STATE_DISABLED},
+     *         {@link #WIFI_STATE_DISABLING}, {@link #WIFI_STATE_ENABLED},
+     *         {@link #WIFI_STATE_ENABLING}, {@link #WIFI_STATE_UNKNOWN}
+     * @see #isWifiEnabled()
+     */
+    public int getWifiState() {
+        try {
+            return mService.getWifiEnabledState();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Return whether Wi-Fi is enabled or disabled.
+     * @return {@code true} if Wi-Fi is enabled
+     * @see #getWifiState()
+     */
+    public boolean isWifiEnabled() {
+        return getWifiState() == WIFI_STATE_ENABLED;
+    }
+
+    /**
+     * Return TX packet counter, for CTS test of WiFi watchdog.
+     * @param listener is the interface to receive result
+     *
+     * @hide for CTS test only
+     */
+    public void getTxPacketCount(TxPacketCountListener listener) {
+        getChannel().sendMessage(RSSI_PKTCNT_FETCH, 0, putListener(listener));
+    }
+
+    /**
+     * Calculates the level of the signal. This should be used any time a signal
+     * is being shown.
+     *
+     * @param rssi The power of the signal measured in RSSI.
+     * @param numLevels The number of levels to consider in the calculated
+     *            level.
+     * @return A level of the signal, given in the range of 0 to numLevels-1
+     *         (both inclusive).
+     */
+    public static int calculateSignalLevel(int rssi, int numLevels) {
+        if (rssi <= MIN_RSSI) {
+            return 0;
+        } else if (rssi >= MAX_RSSI) {
+            return numLevels - 1;
+        } else {
+            float inputRange = (MAX_RSSI - MIN_RSSI);
+            float outputRange = (numLevels - 1);
+            return (int)((float)(rssi - MIN_RSSI) * outputRange / inputRange);
+        }
+    }
+
+    /**
+     * Compares two signal strengths.
+     *
+     * @param rssiA The power of the first signal measured in RSSI.
+     * @param rssiB The power of the second signal measured in RSSI.
+     * @return Returns <0 if the first signal is weaker than the second signal,
+     *         0 if the two signals have the same strength, and >0 if the first
+     *         signal is stronger than the second signal.
+     */
+    public static int compareSignalLevel(int rssiA, int rssiB) {
+        return rssiA - rssiB;
+    }
+
+    /**
+     * Call allowing ConnectivityService to update WifiService with interface mode changes.
+     *
+     * The possible modes include: {@link IFACE_IP_MODE_TETHERED},
+     *                             {@link IFACE_IP_MODE_LOCAL_ONLY},
+     *                             {@link IFACE_IP_MODE_CONFIGURATION_ERROR}
+     *
+     * @param ifaceName String name of the updated interface
+     * @param mode int representing the new mode
+     *
+     * @hide
+     */
+    public void updateInterfaceIpState(String ifaceName, int mode) {
+        try {
+            mService.updateInterfaceIpState(ifaceName, mode);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Start SoftAp mode with the specified configuration.
+     * Note that starting in access point mode disables station
+     * mode operation
+     * @param wifiConfig SSID, security and channel details as
+     *        part of WifiConfiguration
+     * @return {@code true} if the operation succeeds, {@code false} otherwise
+     *
+     * @hide
+     */
+    public boolean startSoftAp(@Nullable WifiConfiguration wifiConfig) {
+        try {
+            return mService.startSoftAp(wifiConfig);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Stop SoftAp mode.
+     * Note that stopping softap mode will restore the previous wifi mode.
+     * @return {@code true} if the operation succeeds, {@code false} otherwise
+     *
+     * @hide
+     */
+    public boolean stopSoftAp() {
+        try {
+            return mService.stopSoftAp();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Request a local only hotspot that an application can use to communicate between co-located
+     * devices connected to the created WiFi hotspot.  The network created by this method will not
+     * have Internet access.  Each application can make a single request for the hotspot, but
+     * multiple applications could be requesting the hotspot at the same time.  When multiple
+     * applications have successfully registered concurrently, they will be sharing the underlying
+     * hotspot. {@link LocalOnlyHotspotCallback#onStarted(LocalOnlyHotspotReservation)} is called
+     * when the hotspot is ready for use by the application.
+     * <p>
+     * Each application can make a single active call to this method. The {@link
+     * LocalOnlyHotspotCallback#onStarted(LocalOnlyHotspotReservation)} callback supplies the
+     * requestor with a {@link LocalOnlyHotspotReservation} that contains a
+     * {@link WifiConfiguration} with the SSID, security type and credentials needed to connect
+     * to the hotspot.  Communicating this information is up to the application.
+     * <p>
+     * If the LocalOnlyHotspot cannot be created, the {@link LocalOnlyHotspotCallback#onFailed(int)}
+     * method will be called. Example failures include errors bringing up the network or if
+     * there is an incompatible operating mode.  For example, if the user is currently using Wifi
+     * Tethering to provide an upstream to another device, LocalOnlyHotspot will not start due to
+     * an incompatible mode. The possible error codes include:
+     * {@link LocalOnlyHotspotCallback#ERROR_NO_CHANNEL},
+     * {@link LocalOnlyHotspotCallback#ERROR_GENERIC},
+     * {@link LocalOnlyHotspotCallback#ERROR_INCOMPATIBLE_MODE} and
+     * {@link LocalOnlyHotspotCallback#ERROR_TETHERING_DISALLOWED}.
+     * <p>
+     * Internally, requests will be tracked to prevent the hotspot from being torn down while apps
+     * are still using it.  The {@link LocalOnlyHotspotReservation} object passed in the  {@link
+     * LocalOnlyHotspotCallback#onStarted(LocalOnlyHotspotReservation)} call should be closed when
+     * the LocalOnlyHotspot is no longer needed using {@link LocalOnlyHotspotReservation#close()}.
+     * Since the hotspot may be shared among multiple applications, removing the final registered
+     * application request will trigger the hotspot teardown.  This means that applications should
+     * not listen to broadcasts containing wifi state to determine if the hotspot was stopped after
+     * they are done using it. Additionally, once {@link LocalOnlyHotspotReservation#close()} is
+     * called, applications will not receive callbacks of any kind.
+     * <p>
+     * Applications should be aware that the user may also stop the LocalOnlyHotspot through the
+     * Settings UI; it is not guaranteed to stay up as long as there is a requesting application.
+     * The requestors will be notified of this case via
+     * {@link LocalOnlyHotspotCallback#onStopped()}.  Other cases may arise where the hotspot is
+     * torn down (Emergency mode, etc).  Application developers should be aware that it can stop
+     * unexpectedly, but they will receive a notification if they have properly registered.
+     * <p>
+     * Applications should also be aware that this network will be shared with other applications.
+     * Applications are responsible for protecting their data on this network (e.g., TLS).
+     * <p>
+     * Applications need to have the following permissions to start LocalOnlyHotspot: {@link
+     * android.Manifest.permission#CHANGE_WIFI_STATE} and {@link
+     * android.Manifest.permission#ACCESS_FINE_LOCATION ACCESS_FINE_LOCATION}.  Callers without
+     * the permissions will trigger a {@link java.lang.SecurityException}.
+     * <p>
+     * @param callback LocalOnlyHotspotCallback for the application to receive updates about
+     * operating status.
+     * @param handler Handler to be used for callbacks.  If the caller passes a null Handler, the
+     * main thread will be used.
+     */
+    public void startLocalOnlyHotspot(LocalOnlyHotspotCallback callback,
+            @Nullable Handler handler) {
+        synchronized (mLock) {
+            Looper looper = (handler == null) ? mContext.getMainLooper() : handler.getLooper();
+            LocalOnlyHotspotCallbackProxy proxy =
+                    new LocalOnlyHotspotCallbackProxy(this, looper, callback);
+            try {
+                String packageName = mContext.getOpPackageName();
+                int returnCode = mService.startLocalOnlyHotspot(
+                        proxy.getMessenger(), new Binder(), packageName);
+                if (returnCode != LocalOnlyHotspotCallback.REQUEST_REGISTERED) {
+                    // Send message to the proxy to make sure we call back on the correct thread
+                    proxy.notifyFailed(returnCode);
+                    return;
+                }
+                mLOHSCallbackProxy = proxy;
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+    }
+
+    /**
+     * Cancels a pending local only hotspot request.  This can be used by the calling application to
+     * cancel the existing request if the provided callback has not been triggered.  Calling this
+     * method will be equivalent to closing the returned LocalOnlyHotspotReservation, but it is not
+     * explicitly required.
+     * <p>
+     * When cancelling this request, application developers should be aware that there may still be
+     * outstanding local only hotspot requests and the hotspot may still start, or continue running.
+     * Additionally, if a callback was registered, it will no longer be triggered after calling
+     * cancel.
+     *
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public void cancelLocalOnlyHotspotRequest() {
+        synchronized (mLock) {
+            stopLocalOnlyHotspot();
+        }
+    }
+
+    /**
+     *  Method used to inform WifiService that the LocalOnlyHotspot is no longer needed.  This
+     *  method is used by WifiManager to release LocalOnlyHotspotReservations held by calling
+     *  applications and removes the internal tracking for the hotspot request.  When all requesting
+     *  applications are finished using the hotspot, it will be stopped and WiFi will return to the
+     *  previous operational mode.
+     *
+     *  This method should not be called by applications.  Instead, they should call the close()
+     *  method on their LocalOnlyHotspotReservation.
+     */
+    private void stopLocalOnlyHotspot() {
+        synchronized (mLock) {
+            if (mLOHSCallbackProxy == null) {
+                // nothing to do, the callback was already cleaned up.
+                return;
+            }
+            mLOHSCallbackProxy = null;
+            try {
+                mService.stopLocalOnlyHotspot();
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+    }
+
+    /**
+     * Allow callers (Settings UI) to watch LocalOnlyHotspot state changes.  Callers will
+     * receive a {@link LocalOnlyHotspotSubscription} object as a parameter of the
+     * {@link LocalOnlyHotspotObserver#onRegistered(LocalOnlyHotspotSubscription)}. The registered
+     * callers will receive the {@link LocalOnlyHotspotObserver#onStarted(WifiConfiguration)} and
+     * {@link LocalOnlyHotspotObserver#onStopped()} callbacks.
+     * <p>
+     * Applications should have the
+     * {@link android.Manifest.permission#ACCESS_FINE_LOCATION ACCESS_FINE_LOCATION}
+     * permission.  Callers without the permission will trigger a
+     * {@link java.lang.SecurityException}.
+     * <p>
+     * @param observer LocalOnlyHotspotObserver callback.
+     * @param handler Handler to use for callbacks
+     *
+     * @hide
+     */
+    public void watchLocalOnlyHotspot(LocalOnlyHotspotObserver observer,
+            @Nullable Handler handler) {
+        synchronized (mLock) {
+            Looper looper = (handler == null) ? mContext.getMainLooper() : handler.getLooper();
+            mLOHSObserverProxy = new LocalOnlyHotspotObserverProxy(this, looper, observer);
+            try {
+                mService.startWatchLocalOnlyHotspot(
+                        mLOHSObserverProxy.getMessenger(), new Binder());
+                mLOHSObserverProxy.registered();
+            } catch (RemoteException e) {
+                mLOHSObserverProxy = null;
+                throw e.rethrowFromSystemServer();
+            }
+        }
+    }
+
+    /**
+     * Allow callers to stop watching LocalOnlyHotspot state changes.  After calling this method,
+     * applications will no longer receive callbacks.
+     *
+     * @hide
+     */
+    public void unregisterLocalOnlyHotspotObserver() {
+        synchronized (mLock) {
+            if (mLOHSObserverProxy == null) {
+                // nothing to do, the callback was already cleaned up
+                return;
+            }
+            mLOHSObserverProxy = null;
+            try {
+                mService.stopWatchLocalOnlyHotspot();
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+    }
+
+    /**
+     * Gets the Wi-Fi enabled state.
+     * @return One of {@link #WIFI_AP_STATE_DISABLED},
+     *         {@link #WIFI_AP_STATE_DISABLING}, {@link #WIFI_AP_STATE_ENABLED},
+     *         {@link #WIFI_AP_STATE_ENABLING}, {@link #WIFI_AP_STATE_FAILED}
+     * @see #isWifiApEnabled()
+     *
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.ACCESS_WIFI_STATE)
+    public int getWifiApState() {
+        try {
+            return mService.getWifiApEnabledState();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Return whether Wi-Fi AP is enabled or disabled.
+     * @return {@code true} if Wi-Fi AP is enabled
+     * @see #getWifiApState()
+     *
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.ACCESS_WIFI_STATE)
+    public boolean isWifiApEnabled() {
+        return getWifiApState() == WIFI_AP_STATE_ENABLED;
+    }
+
+    /**
+     * Gets the Wi-Fi AP Configuration.
+     * @return AP details in WifiConfiguration
+     *
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.ACCESS_WIFI_STATE)
+    public WifiConfiguration getWifiApConfiguration() {
+        try {
+            return mService.getWifiApConfiguration();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Sets the Wi-Fi AP Configuration.  The AP configuration must either be open or
+     * WPA2 PSK networks.
+     * @return {@code true} if the operation succeeded, {@code false} otherwise
+     *
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.CHANGE_WIFI_STATE)
+    public boolean setWifiApConfiguration(WifiConfiguration wifiConfig) {
+        try {
+            return mService.setWifiApConfiguration(wifiConfig, mContext.getOpPackageName());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Method that triggers a notification to the user about a conversion to their saved AP config.
+     *
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.NETWORK_SETTINGS)
+    public void notifyUserOfApBandConversion() {
+        Log.d(TAG, "apBand was converted, notify the user");
+        try {
+            mService.notifyUserOfApBandConversion(mContext.getOpPackageName());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Enable/Disable TDLS on a specific local route.
+     *
+     * <p>
+     * TDLS enables two wireless endpoints to talk to each other directly
+     * without going through the access point that is managing the local
+     * network. It saves bandwidth and improves quality of the link.
+     * </p>
+     * <p>
+     * This API enables/disables the option of using TDLS. If enabled, the
+     * underlying hardware is free to use TDLS or a hop through the access
+     * point. If disabled, existing TDLS session is torn down and
+     * hardware is restricted to use access point for transferring wireless
+     * packets. Default value for all routes is 'disabled', meaning restricted
+     * to use access point for transferring packets.
+     * </p>
+     *
+     * @param remoteIPAddress IP address of the endpoint to setup TDLS with
+     * @param enable true = setup and false = tear down TDLS
+     */
+    public void setTdlsEnabled(InetAddress remoteIPAddress, boolean enable) {
+        try {
+            mService.enableTdls(remoteIPAddress.getHostAddress(), enable);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Similar to {@link #setTdlsEnabled(InetAddress, boolean) }, except
+     * this version allows you to specify remote endpoint with a MAC address.
+     * @param remoteMacAddress MAC address of the remote endpoint such as 00:00:0c:9f:f2:ab
+     * @param enable true = setup and false = tear down TDLS
+     */
+    public void setTdlsEnabledWithMacAddress(String remoteMacAddress, boolean enable) {
+        try {
+            mService.enableTdlsWithMacAddress(remoteMacAddress, enable);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /* TODO: deprecate synchronous API and open up the following API */
+
+    private static final int BASE = Protocol.BASE_WIFI_MANAGER;
+
+    /* Commands to WifiService */
+    /** @hide */
+    public static final int CONNECT_NETWORK                 = BASE + 1;
+    /** @hide */
+    public static final int CONNECT_NETWORK_FAILED          = BASE + 2;
+    /** @hide */
+    public static final int CONNECT_NETWORK_SUCCEEDED       = BASE + 3;
+
+    /** @hide */
+    public static final int FORGET_NETWORK                  = BASE + 4;
+    /** @hide */
+    public static final int FORGET_NETWORK_FAILED           = BASE + 5;
+    /** @hide */
+    public static final int FORGET_NETWORK_SUCCEEDED        = BASE + 6;
+
+    /** @hide */
+    public static final int SAVE_NETWORK                    = BASE + 7;
+    /** @hide */
+    public static final int SAVE_NETWORK_FAILED             = BASE + 8;
+    /** @hide */
+    public static final int SAVE_NETWORK_SUCCEEDED          = BASE + 9;
+
+    /** @hide
+     * @deprecated This is deprecated
+     */
+    public static final int START_WPS                       = BASE + 10;
+    /** @hide
+     * @deprecated This is deprecated
+     */
+    public static final int START_WPS_SUCCEEDED             = BASE + 11;
+    /** @hide
+     * @deprecated This is deprecated
+     */
+    public static final int WPS_FAILED                      = BASE + 12;
+    /** @hide
+     * @deprecated This is deprecated
+     */
+    public static final int WPS_COMPLETED                   = BASE + 13;
+
+    /** @hide
+     * @deprecated This is deprecated
+     */
+    public static final int CANCEL_WPS                      = BASE + 14;
+    /** @hide
+     * @deprecated This is deprecated
+     */
+    public static final int CANCEL_WPS_FAILED               = BASE + 15;
+    /** @hide
+     * @deprecated This is deprecated
+     */
+    public static final int CANCEL_WPS_SUCCEDED             = BASE + 16;
+
+    /** @hide */
+    public static final int DISABLE_NETWORK                 = BASE + 17;
+    /** @hide */
+    public static final int DISABLE_NETWORK_FAILED          = BASE + 18;
+    /** @hide */
+    public static final int DISABLE_NETWORK_SUCCEEDED       = BASE + 19;
+
+    /** @hide */
+    public static final int RSSI_PKTCNT_FETCH               = BASE + 20;
+    /** @hide */
+    public static final int RSSI_PKTCNT_FETCH_SUCCEEDED     = BASE + 21;
+    /** @hide */
+    public static final int RSSI_PKTCNT_FETCH_FAILED        = BASE + 22;
+
+    /**
+     * Passed with {@link ActionListener#onFailure}.
+     * Indicates that the operation failed due to an internal error.
+     * @hide
+     */
+    public static final int ERROR                       = 0;
+
+    /**
+     * Passed with {@link ActionListener#onFailure}.
+     * Indicates that the operation is already in progress
+     * @hide
+     */
+    public static final int IN_PROGRESS                 = 1;
+
+    /**
+     * Passed with {@link ActionListener#onFailure}.
+     * Indicates that the operation failed because the framework is busy and
+     * unable to service the request
+     * @hide
+     */
+    public static final int BUSY                        = 2;
+
+    /* WPS specific errors */
+    /** WPS overlap detected
+     * @deprecated This is deprecated
+     */
+    public static final int WPS_OVERLAP_ERROR           = 3;
+    /** WEP on WPS is prohibited
+     * @deprecated This is deprecated
+     */
+    public static final int WPS_WEP_PROHIBITED          = 4;
+    /** TKIP only prohibited
+     * @deprecated This is deprecated
+     */
+    public static final int WPS_TKIP_ONLY_PROHIBITED    = 5;
+    /** Authentication failure on WPS
+     * @deprecated This is deprecated
+     */
+    public static final int WPS_AUTH_FAILURE            = 6;
+    /** WPS timed out
+     * @deprecated This is deprecated
+     */
+    public static final int WPS_TIMED_OUT               = 7;
+
+    /**
+     * Passed with {@link ActionListener#onFailure}.
+     * Indicates that the operation failed due to invalid inputs
+     * @hide
+     */
+    public static final int INVALID_ARGS                = 8;
+
+    /**
+     * Passed with {@link ActionListener#onFailure}.
+     * Indicates that the operation failed due to user permissions.
+     * @hide
+     */
+    public static final int NOT_AUTHORIZED              = 9;
+
+    /**
+     * Interface for callback invocation on an application action
+     * @hide
+     */
+    @SystemApi
+    public interface ActionListener {
+        /**
+         * The operation succeeded.
+         * This is called when the scan request has been validated and ready
+         * to sent to driver.
+         */
+        public void onSuccess();
+        /**
+         * The operation failed.
+         * This is called when the scan request failed.
+         * @param reason The reason for failure could be one of the following:
+         * {@link #REASON_INVALID_REQUEST}} is specified when scan request parameters are invalid.
+         * {@link #REASON_NOT_AUTHORIZED} is specified when requesting app doesn't have the required
+         * permission to request a scan.
+         * {@link #REASON_UNSPECIFIED} is specified when driver reports a scan failure.
+         */
+        public void onFailure(int reason);
+    }
+
+    /** Interface for callback invocation on a start WPS action
+     * @deprecated This is deprecated
+     */
+    public static abstract class WpsCallback {
+
+        /** WPS start succeeded
+         * @deprecated This API is deprecated
+         */
+        public abstract void onStarted(String pin);
+
+        /** WPS operation completed successfully
+         * @deprecated This API is deprecated
+         */
+        public abstract void onSucceeded();
+
+        /**
+         * WPS operation failed
+         * @param reason The reason for failure could be one of
+         * {@link #WPS_TKIP_ONLY_PROHIBITED}, {@link #WPS_OVERLAP_ERROR},
+         * {@link #WPS_WEP_PROHIBITED}, {@link #WPS_TIMED_OUT} or {@link #WPS_AUTH_FAILURE}
+         * and some generic errors.
+         * @deprecated This API is deprecated
+         */
+        public abstract void onFailed(int reason);
+    }
+
+    /** Interface for callback invocation on a TX packet count poll action {@hide} */
+    public interface TxPacketCountListener {
+        /**
+         * The operation succeeded
+         * @param count TX packet counter
+         */
+        public void onSuccess(int count);
+        /**
+         * The operation failed
+         * @param reason The reason for failure could be one of
+         * {@link #ERROR}, {@link #IN_PROGRESS} or {@link #BUSY}
+         */
+        public void onFailure(int reason);
+    }
+
+    /**
+     * Base class for soft AP callback. Should be extended by applications and set when calling
+     * {@link WifiManager#registerSoftApCallback(SoftApCallback, Handler)}.
+     *
+     * @hide
+     */
+    public interface SoftApCallback {
+        /**
+         * Called when soft AP state changes.
+         *
+         * @param state new new AP state. One of {@link #WIFI_AP_STATE_DISABLED},
+         *        {@link #WIFI_AP_STATE_DISABLING}, {@link #WIFI_AP_STATE_ENABLED},
+         *        {@link #WIFI_AP_STATE_ENABLING}, {@link #WIFI_AP_STATE_FAILED}
+         * @param failureReason reason when in failed state. One of
+         *        {@link #SAP_START_FAILURE_GENERAL}, {@link #SAP_START_FAILURE_NO_CHANNEL}
+         */
+        public abstract void onStateChanged(@WifiApState int state,
+                @SapStartFailure int failureReason);
+
+        /**
+         * Called when number of connected clients to soft AP changes.
+         *
+         * @param numClients number of connected clients
+         */
+        public abstract void onNumClientsChanged(int numClients);
+    }
+
+    /**
+     * Callback proxy for SoftApCallback objects.
+     *
+     * @hide
+     */
+    private class SoftApCallbackProxy extends ISoftApCallback.Stub {
+        private final Handler mHandler;
+        private final SoftApCallback mCallback;
+
+        SoftApCallbackProxy(Looper looper, SoftApCallback callback) {
+            mHandler = new Handler(looper);
+            mCallback = callback;
+        }
+
+        @Override
+        public void onStateChanged(int state, int failureReason) {
+            if (mVerboseLoggingEnabled) {
+                Log.v(TAG, "SoftApCallbackProxy: onStateChanged: state=" + state
+                        + ", failureReason=" + failureReason);
+            }
+            mHandler.post(() -> {
+                mCallback.onStateChanged(state, failureReason);
+            });
+        }
+
+        @Override
+        public void onNumClientsChanged(int numClients) {
+            if (mVerboseLoggingEnabled) {
+                Log.v(TAG, "SoftApCallbackProxy: onNumClientsChanged: numClients=" + numClients);
+            }
+            mHandler.post(() -> {
+                mCallback.onNumClientsChanged(numClients);
+            });
+        }
+    }
+
+    /**
+     * Registers a callback for Soft AP. See {@link SoftApCallback}. Caller will receive the current
+     * soft AP state and number of connected devices immediately after a successful call to this API
+     * via callback. Note that receiving an immediate WIFI_AP_STATE_FAILED value for soft AP state
+     * indicates that the latest attempt to start soft AP has failed. Caller can unregister a
+     * previously registered callback using {@link unregisterSoftApCallback}
+     * <p>
+     * Applications should have the
+     * {@link android.Manifest.permission#NETWORK_SETTINGS NETWORK_SETTINGS} permission. Callers
+     * without the permission will trigger a {@link java.lang.SecurityException}.
+     * <p>
+     *
+     * @param callback Callback for soft AP events
+     * @param handler  The Handler on whose thread to execute the callbacks of the {@code callback}
+     *                 object. If null, then the application's main thread will be used.
+     *
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.NETWORK_SETTINGS)
+    public void registerSoftApCallback(@NonNull SoftApCallback callback,
+                                       @Nullable Handler handler) {
+        if (callback == null) throw new IllegalArgumentException("callback cannot be null");
+        Log.v(TAG, "registerSoftApCallback: callback=" + callback + ", handler=" + handler);
+
+        Looper looper = (handler == null) ? mContext.getMainLooper() : handler.getLooper();
+        Binder binder = new Binder();
+        try {
+            mService.registerSoftApCallback(binder, new SoftApCallbackProxy(looper, callback),
+                    callback.hashCode());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Allow callers to unregister a previously registered callback. After calling this method,
+     * applications will no longer receive soft AP events.
+     *
+     * @param callback Callback to unregister for soft AP events
+     *
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.NETWORK_SETTINGS)
+    public void unregisterSoftApCallback(@NonNull SoftApCallback callback) {
+        if (callback == null) throw new IllegalArgumentException("callback cannot be null");
+        Log.v(TAG, "unregisterSoftApCallback: callback=" + callback);
+
+        try {
+            mService.unregisterSoftApCallback(callback.hashCode());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * LocalOnlyHotspotReservation that contains the {@link WifiConfiguration} for the active
+     * LocalOnlyHotspot request.
+     * <p>
+     * Applications requesting LocalOnlyHotspot for sharing will receive an instance of the
+     * LocalOnlyHotspotReservation in the
+     * {@link LocalOnlyHotspotCallback#onStarted(LocalOnlyHotspotReservation)} call.  This
+     * reservation contains the relevant {@link WifiConfiguration}.
+     * When an application is done with the LocalOnlyHotspot, they should call {@link
+     * LocalOnlyHotspotReservation#close()}.  Once this happens, the application will not receive
+     * any further callbacks. If the LocalOnlyHotspot is stopped due to a
+     * user triggered mode change, applications will be notified via the {@link
+     * LocalOnlyHotspotCallback#onStopped()} callback.
+     */
+    public class LocalOnlyHotspotReservation implements AutoCloseable {
+
+        private final CloseGuard mCloseGuard = CloseGuard.get();
+        private final WifiConfiguration mConfig;
+
+        /** @hide */
+        @VisibleForTesting
+        public LocalOnlyHotspotReservation(WifiConfiguration config) {
+            mConfig = config;
+            mCloseGuard.open("close");
+        }
+
+        public WifiConfiguration getWifiConfiguration() {
+            return mConfig;
+        }
+
+        @Override
+        public void close() {
+            try {
+                stopLocalOnlyHotspot();
+                mCloseGuard.close();
+            } catch (Exception e) {
+                Log.e(TAG, "Failed to stop Local Only Hotspot.");
+            }
+        }
+
+        @Override
+        protected void finalize() throws Throwable {
+            try {
+                if (mCloseGuard != null) {
+                    mCloseGuard.warnIfOpen();
+                }
+                close();
+            } finally {
+                super.finalize();
+            }
+        }
+    }
+
+    /**
+     * Callback class for applications to receive updates about the LocalOnlyHotspot status.
+     */
+    public static class LocalOnlyHotspotCallback {
+        /** @hide */
+        public static final int REQUEST_REGISTERED = 0;
+
+        public static final int ERROR_NO_CHANNEL = 1;
+        public static final int ERROR_GENERIC = 2;
+        public static final int ERROR_INCOMPATIBLE_MODE = 3;
+        public static final int ERROR_TETHERING_DISALLOWED = 4;
+
+        /** LocalOnlyHotspot start succeeded. */
+        public void onStarted(LocalOnlyHotspotReservation reservation) {};
+
+        /**
+         * LocalOnlyHotspot stopped.
+         * <p>
+         * The LocalOnlyHotspot can be disabled at any time by the user.  When this happens,
+         * applications will be notified that it was stopped. This will not be invoked when an
+         * application calls {@link LocalOnlyHotspotReservation#close()}.
+         */
+        public void onStopped() {};
+
+        /**
+         * LocalOnlyHotspot failed to start.
+         * <p>
+         * Applications can attempt to call
+         * {@link WifiManager#startLocalOnlyHotspot(LocalOnlyHotspotCallback, Handler)} again at
+         * a later time.
+         * <p>
+         * @param reason The reason for failure could be one of: {@link
+         * #ERROR_TETHERING_DISALLOWED}, {@link #ERROR_INCOMPATIBLE_MODE},
+         * {@link #ERROR_NO_CHANNEL}, or {@link #ERROR_GENERIC}.
+         */
+        public void onFailed(int reason) { };
+    }
+
+    /**
+     * Callback proxy for LocalOnlyHotspotCallback objects.
+     */
+    private static class LocalOnlyHotspotCallbackProxy {
+        private final Handler mHandler;
+        private final WeakReference<WifiManager> mWifiManager;
+        private final Looper mLooper;
+        private final Messenger mMessenger;
+
+        /**
+         * Constructs a {@link LocalOnlyHotspotCallback} using the specified looper.  All callbacks
+         * will be delivered on the thread of the specified looper.
+         *
+         * @param manager WifiManager
+         * @param looper Looper for delivering callbacks
+         * @param callback LocalOnlyHotspotCallback to notify the calling application.
+         */
+        LocalOnlyHotspotCallbackProxy(WifiManager manager, Looper looper,
+                final LocalOnlyHotspotCallback callback) {
+            mWifiManager = new WeakReference<>(manager);
+            mLooper = looper;
+
+            mHandler = new Handler(looper) {
+                @Override
+                public void handleMessage(Message msg) {
+                    Log.d(TAG, "LocalOnlyHotspotCallbackProxy: handle message what: "
+                            + msg.what + " msg: " + msg);
+
+                    WifiManager manager = mWifiManager.get();
+                    if (manager == null) {
+                        Log.w(TAG, "LocalOnlyHotspotCallbackProxy: handle message post GC");
+                        return;
+                    }
+
+                    switch (msg.what) {
+                        case HOTSPOT_STARTED:
+                            WifiConfiguration config = (WifiConfiguration) msg.obj;
+                            if (config == null) {
+                                Log.e(TAG, "LocalOnlyHotspotCallbackProxy: config cannot be null.");
+                                callback.onFailed(LocalOnlyHotspotCallback.ERROR_GENERIC);
+                                return;
+                            }
+                            callback.onStarted(manager.new LocalOnlyHotspotReservation(config));
+                            break;
+                        case HOTSPOT_STOPPED:
+                            Log.w(TAG, "LocalOnlyHotspotCallbackProxy: hotspot stopped");
+                            callback.onStopped();
+                            break;
+                        case HOTSPOT_FAILED:
+                            int reasonCode = msg.arg1;
+                            Log.w(TAG, "LocalOnlyHotspotCallbackProxy: failed to start.  reason: "
+                                    + reasonCode);
+                            callback.onFailed(reasonCode);
+                            Log.w(TAG, "done with the callback...");
+                            break;
+                        default:
+                            Log.e(TAG, "LocalOnlyHotspotCallbackProxy unhandled message.  type: "
+                                    + msg.what);
+                    }
+                }
+            };
+            mMessenger = new Messenger(mHandler);
+        }
+
+        public Messenger getMessenger() {
+            return mMessenger;
+        }
+
+        /**
+         * Helper method allowing the the incoming application call to move the onFailed callback
+         * over to the desired callback thread.
+         *
+         * @param reason int representing the error type
+         */
+        public void notifyFailed(int reason) throws RemoteException {
+            Message msg = Message.obtain();
+            msg.what = HOTSPOT_FAILED;
+            msg.arg1 = reason;
+            mMessenger.send(msg);
+        }
+    }
+
+    /**
+     * LocalOnlyHotspotSubscription that is an AutoCloseable object for tracking applications
+     * watching for LocalOnlyHotspot changes.
+     *
+     * @hide
+     */
+    public class LocalOnlyHotspotSubscription implements AutoCloseable {
+        private final CloseGuard mCloseGuard = CloseGuard.get();
+
+        /** @hide */
+        @VisibleForTesting
+        public LocalOnlyHotspotSubscription() {
+            mCloseGuard.open("close");
+        }
+
+        @Override
+        public void close() {
+            try {
+                unregisterLocalOnlyHotspotObserver();
+                mCloseGuard.close();
+            } catch (Exception e) {
+                Log.e(TAG, "Failed to unregister LocalOnlyHotspotObserver.");
+            }
+        }
+
+        @Override
+        protected void finalize() throws Throwable {
+            try {
+                if (mCloseGuard != null) {
+                    mCloseGuard.warnIfOpen();
+                }
+                close();
+            } finally {
+                super.finalize();
+            }
+        }
+    }
+
+    /**
+     * Class to notify calling applications that watch for changes in LocalOnlyHotspot of updates.
+     *
+     * @hide
+     */
+    public static class LocalOnlyHotspotObserver {
+        /**
+         * Confirm registration for LocalOnlyHotspotChanges by returning a
+         * LocalOnlyHotspotSubscription.
+         */
+        public void onRegistered(LocalOnlyHotspotSubscription subscription) {};
+
+        /**
+         * LocalOnlyHotspot started with the supplied config.
+         */
+        public void onStarted(WifiConfiguration config) {};
+
+        /**
+         * LocalOnlyHotspot stopped.
+         */
+        public void onStopped() {};
+    }
+
+    /**
+     * Callback proxy for LocalOnlyHotspotObserver objects.
+     */
+    private static class LocalOnlyHotspotObserverProxy {
+        private final Handler mHandler;
+        private final WeakReference<WifiManager> mWifiManager;
+        private final Looper mLooper;
+        private final Messenger mMessenger;
+
+        /**
+         * Constructs a {@link LocalOnlyHotspotObserverProxy} using the specified looper.
+         * All callbacks will be delivered on the thread of the specified looper.
+         *
+         * @param manager WifiManager
+         * @param looper Looper for delivering callbacks
+         * @param observer LocalOnlyHotspotObserver to notify the calling application.
+         */
+        LocalOnlyHotspotObserverProxy(WifiManager manager, Looper looper,
+                final LocalOnlyHotspotObserver observer) {
+            mWifiManager = new WeakReference<>(manager);
+            mLooper = looper;
+
+            mHandler = new Handler(looper) {
+                @Override
+                public void handleMessage(Message msg) {
+                    Log.d(TAG, "LocalOnlyHotspotObserverProxy: handle message what: "
+                            + msg.what + " msg: " + msg);
+
+                    WifiManager manager = mWifiManager.get();
+                    if (manager == null) {
+                        Log.w(TAG, "LocalOnlyHotspotObserverProxy: handle message post GC");
+                        return;
+                    }
+
+                    switch (msg.what) {
+                        case HOTSPOT_OBSERVER_REGISTERED:
+                            observer.onRegistered(manager.new LocalOnlyHotspotSubscription());
+                            break;
+                        case HOTSPOT_STARTED:
+                            WifiConfiguration config = (WifiConfiguration) msg.obj;
+                            if (config == null) {
+                                Log.e(TAG, "LocalOnlyHotspotObserverProxy: config cannot be null.");
+                                return;
+                            }
+                            observer.onStarted(config);
+                            break;
+                        case HOTSPOT_STOPPED:
+                            observer.onStopped();
+                            break;
+                        default:
+                            Log.e(TAG, "LocalOnlyHotspotObserverProxy unhandled message.  type: "
+                                    + msg.what);
+                    }
+                }
+            };
+            mMessenger = new Messenger(mHandler);
+        }
+
+        public Messenger getMessenger() {
+            return mMessenger;
+        }
+
+        public void registered() throws RemoteException {
+            Message msg = Message.obtain();
+            msg.what = HOTSPOT_OBSERVER_REGISTERED;
+            mMessenger.send(msg);
+        }
+    }
+
+    // Ensure that multiple ServiceHandler threads do not interleave message dispatch.
+    private static final Object sServiceHandlerDispatchLock = new Object();
+
+    private class ServiceHandler extends Handler {
+        ServiceHandler(Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(Message message) {
+            synchronized (sServiceHandlerDispatchLock) {
+                dispatchMessageToListeners(message);
+            }
+        }
+
+        private void dispatchMessageToListeners(Message message) {
+            Object listener = removeListener(message.arg2);
+            switch (message.what) {
+                case AsyncChannel.CMD_CHANNEL_HALF_CONNECTED:
+                    if (message.arg1 == AsyncChannel.STATUS_SUCCESSFUL) {
+                        mAsyncChannel.sendMessage(AsyncChannel.CMD_CHANNEL_FULL_CONNECTION);
+                    } else {
+                        Log.e(TAG, "Failed to set up channel connection");
+                        // This will cause all further async API calls on the WifiManager
+                        // to fail and throw an exception
+                        mAsyncChannel = null;
+                    }
+                    mConnected.countDown();
+                    break;
+                case AsyncChannel.CMD_CHANNEL_FULLY_CONNECTED:
+                    // Ignore
+                    break;
+                case AsyncChannel.CMD_CHANNEL_DISCONNECTED:
+                    Log.e(TAG, "Channel connection lost");
+                    // This will cause all further async API calls on the WifiManager
+                    // to fail and throw an exception
+                    mAsyncChannel = null;
+                    getLooper().quit();
+                    break;
+                    /* ActionListeners grouped together */
+                case WifiManager.CONNECT_NETWORK_FAILED:
+                case WifiManager.FORGET_NETWORK_FAILED:
+                case WifiManager.SAVE_NETWORK_FAILED:
+                case WifiManager.DISABLE_NETWORK_FAILED:
+                    if (listener != null) {
+                        ((ActionListener) listener).onFailure(message.arg1);
+                    }
+                    break;
+                    /* ActionListeners grouped together */
+                case WifiManager.CONNECT_NETWORK_SUCCEEDED:
+                case WifiManager.FORGET_NETWORK_SUCCEEDED:
+                case WifiManager.SAVE_NETWORK_SUCCEEDED:
+                case WifiManager.DISABLE_NETWORK_SUCCEEDED:
+                    if (listener != null) {
+                        ((ActionListener) listener).onSuccess();
+                    }
+                    break;
+                case WifiManager.RSSI_PKTCNT_FETCH_SUCCEEDED:
+                    if (listener != null) {
+                        RssiPacketCountInfo info = (RssiPacketCountInfo) message.obj;
+                        if (info != null)
+                            ((TxPacketCountListener) listener).onSuccess(info.txgood + info.txbad);
+                        else
+                            ((TxPacketCountListener) listener).onFailure(ERROR);
+                    }
+                    break;
+                case WifiManager.RSSI_PKTCNT_FETCH_FAILED:
+                    if (listener != null) {
+                        ((TxPacketCountListener) listener).onFailure(message.arg1);
+                    }
+                    break;
+                default:
+                    //ignore
+                    break;
+            }
+        }
+    }
+
+    private int putListener(Object listener) {
+        if (listener == null) return INVALID_KEY;
+        int key;
+        synchronized (mListenerMapLock) {
+            do {
+                key = mListenerKey++;
+            } while (key == INVALID_KEY);
+            mListenerMap.put(key, listener);
+        }
+        return key;
+    }
+
+    private Object removeListener(int key) {
+        if (key == INVALID_KEY) return null;
+        synchronized (mListenerMapLock) {
+            Object listener = mListenerMap.get(key);
+            mListenerMap.remove(key);
+            return listener;
+        }
+    }
+
+    private synchronized AsyncChannel getChannel() {
+        if (mAsyncChannel == null) {
+            Messenger messenger = getWifiServiceMessenger();
+            if (messenger == null) {
+                throw new IllegalStateException(
+                        "getWifiServiceMessenger() returned null!  This is invalid.");
+            }
+
+            mAsyncChannel = new AsyncChannel();
+            mConnected = new CountDownLatch(1);
+
+            Handler handler = new ServiceHandler(mLooper);
+            mAsyncChannel.connect(mContext, handler, messenger);
+            try {
+                mConnected.await();
+            } catch (InterruptedException e) {
+                Log.e(TAG, "interrupted wait at init");
+            }
+        }
+        return mAsyncChannel;
+    }
+
+    /**
+     * Connect to a network with the given configuration. The network also
+     * gets added to the list of configured networks for the foreground user.
+     *
+     * For a new network, this function is used instead of a
+     * sequence of addNetwork(), enableNetwork(), and reconnect()
+     *
+     * @param config the set of variables that describe the configuration,
+     *            contained in a {@link WifiConfiguration} object.
+     * @param listener for callbacks on success or failure. Can be null.
+     * @throws IllegalStateException if the WifiManager instance needs to be
+     * initialized again
+     *
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.NETWORK_SETTINGS,
+            android.Manifest.permission.NETWORK_SETUP_WIZARD,
+            android.Manifest.permission.NETWORK_STACK
+    })
+    public void connect(@NonNull WifiConfiguration config, @Nullable ActionListener listener) {
+        if (config == null) throw new IllegalArgumentException("config cannot be null");
+        // Use INVALID_NETWORK_ID for arg1 when passing a config object
+        // arg1 is used to pass network id when the network already exists
+        getChannel().sendMessage(CONNECT_NETWORK, WifiConfiguration.INVALID_NETWORK_ID,
+                putListener(listener), config);
+    }
+
+    /**
+     * Connect to a network with the given networkId.
+     *
+     * This function is used instead of a enableNetwork() and reconnect()
+     *
+     * @param networkId the ID of the network as returned by {@link #addNetwork} or {@link
+     *        getConfiguredNetworks}.
+     * @param listener for callbacks on success or failure. Can be null.
+     * @throws IllegalStateException if the WifiManager instance needs to be
+     * initialized again
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.NETWORK_SETTINGS,
+            android.Manifest.permission.NETWORK_SETUP_WIZARD,
+            android.Manifest.permission.NETWORK_STACK
+    })
+    public void connect(int networkId, @Nullable ActionListener listener) {
+        if (networkId < 0) throw new IllegalArgumentException("Network id cannot be negative");
+        getChannel().sendMessage(CONNECT_NETWORK, networkId, putListener(listener));
+    }
+
+    /**
+     * Save the given network to the list of configured networks for the
+     * foreground user. If the network already exists, the configuration
+     * is updated. Any new network is enabled by default.
+     *
+     * For a new network, this function is used instead of a
+     * sequence of addNetwork() and enableNetwork().
+     *
+     * For an existing network, it accomplishes the task of updateNetwork()
+     *
+     * This API will cause reconnect if the crecdentials of the current active
+     * connection has been changed.
+     *
+     * @param config the set of variables that describe the configuration,
+     *            contained in a {@link WifiConfiguration} object.
+     * @param listener for callbacks on success or failure. Can be null.
+     * @throws IllegalStateException if the WifiManager instance needs to be
+     * initialized again
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.NETWORK_SETTINGS,
+            android.Manifest.permission.NETWORK_SETUP_WIZARD,
+            android.Manifest.permission.NETWORK_STACK
+    })
+    public void save(@NonNull WifiConfiguration config, @Nullable ActionListener listener) {
+        if (config == null) throw new IllegalArgumentException("config cannot be null");
+        getChannel().sendMessage(SAVE_NETWORK, 0, putListener(listener), config);
+    }
+
+    /**
+     * Delete the network from the list of configured networks for the
+     * foreground user.
+     *
+     * This function is used instead of a sequence of removeNetwork()
+     *
+     * @param config the set of variables that describe the configuration,
+     *            contained in a {@link WifiConfiguration} object.
+     * @param listener for callbacks on success or failure. Can be null.
+     * @throws IllegalStateException if the WifiManager instance needs to be
+     * initialized again
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.NETWORK_SETTINGS,
+            android.Manifest.permission.NETWORK_SETUP_WIZARD,
+            android.Manifest.permission.NETWORK_STACK
+    })
+    public void forget(int netId, @Nullable ActionListener listener) {
+        if (netId < 0) throw new IllegalArgumentException("Network id cannot be negative");
+        getChannel().sendMessage(FORGET_NETWORK, netId, putListener(listener));
+    }
+
+    /**
+     * Disable network
+     *
+     * @param netId is the network Id
+     * @param listener for callbacks on success or failure. Can be null.
+     * @throws IllegalStateException if the WifiManager instance needs to be
+     * initialized again
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.NETWORK_SETTINGS,
+            android.Manifest.permission.NETWORK_SETUP_WIZARD,
+            android.Manifest.permission.NETWORK_STACK
+    })
+    public void disable(int netId, @Nullable ActionListener listener) {
+        if (netId < 0) throw new IllegalArgumentException("Network id cannot be negative");
+        getChannel().sendMessage(DISABLE_NETWORK, netId, putListener(listener));
+    }
+
+    /**
+     * Disable ephemeral Network
+     *
+     * @param SSID, in the format of WifiConfiguration's SSID.
+     * @hide
+     */
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.NETWORK_SETTINGS,
+            android.Manifest.permission.NETWORK_STACK
+    })
+    public void disableEphemeralNetwork(String SSID) {
+        if (SSID == null) throw new IllegalArgumentException("SSID cannot be null");
+        try {
+            mService.disableEphemeralNetwork(SSID, mContext.getOpPackageName());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * WPS suport has been deprecated from Client mode and this method will immediately trigger
+     * {@link WpsCallback#onFailed(int)} with a generic error.
+     *
+     * @param config WPS configuration (does not support {@link WpsInfo#LABEL})
+     * @param listener for callbacks on success or failure. Can be null.
+     * @throws IllegalStateException if the WifiManager instance needs to be initialized again
+     * @deprecated This API is deprecated
+     */
+    public void startWps(WpsInfo config, WpsCallback listener) {
+        if (listener != null ) {
+            listener.onFailed(ERROR);
+        }
+    }
+
+    /**
+     * WPS support has been deprecated from Client mode and this method will immediately trigger
+     * {@link WpsCallback#onFailed(int)} with a generic error.
+     *
+     * @param listener for callbacks on success or failure. Can be null.
+     * @throws IllegalStateException if the WifiManager instance needs to be initialized again
+     * @deprecated This API is deprecated
+     */
+    public void cancelWps(WpsCallback listener) {
+        if (listener != null) {
+            listener.onFailed(ERROR);
+        }
+    }
+
+    /**
+     * Get a reference to WifiService handler. This is used by a client to establish
+     * an AsyncChannel communication with WifiService
+     *
+     * @return Messenger pointing to the WifiService handler
+     */
+    @UnsupportedAppUsage
+    private Messenger getWifiServiceMessenger() {
+        try {
+            return mService.getWifiServiceMessenger(mContext.getOpPackageName());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+
+    /**
+     * Allows an application to keep the Wi-Fi radio awake.
+     * Normally the Wi-Fi radio may turn off when the user has not used the device in a while.
+     * Acquiring a WifiLock will keep the radio on until the lock is released.  Multiple
+     * applications may hold WifiLocks, and the radio will only be allowed to turn off when no
+     * WifiLocks are held in any application.
+     * <p>
+     * Before using a WifiLock, consider carefully if your application requires Wi-Fi access, or
+     * could function over a mobile network, if available.  A program that needs to download large
+     * files should hold a WifiLock to ensure that the download will complete, but a program whose
+     * network usage is occasional or low-bandwidth should not hold a WifiLock to avoid adversely
+     * affecting battery life.
+     * <p>
+     * Note that WifiLocks cannot override the user-level "Wi-Fi Enabled" setting, nor Airplane
+     * Mode.  They simply keep the radio from turning off when Wi-Fi is already on but the device
+     * is idle.
+     * <p>
+     * Any application using a WifiLock must request the {@code android.permission.WAKE_LOCK}
+     * permission in an {@code <uses-permission>} element of the application's manifest.
+     */
+    public class WifiLock {
+        private String mTag;
+        private final IBinder mBinder;
+        private int mRefCount;
+        int mLockType;
+        private boolean mRefCounted;
+        private boolean mHeld;
+        private WorkSource mWorkSource;
+
+        private WifiLock(int lockType, String tag) {
+            mTag = tag;
+            mLockType = lockType;
+            mBinder = new Binder();
+            mRefCount = 0;
+            mRefCounted = true;
+            mHeld = false;
+        }
+
+        /**
+         * Locks the Wi-Fi radio on until {@link #release} is called.
+         *
+         * If this WifiLock is reference-counted, each call to {@code acquire} will increment the
+         * reference count, and the radio will remain locked as long as the reference count is
+         * above zero.
+         *
+         * If this WifiLock is not reference-counted, the first call to {@code acquire} will lock
+         * the radio, but subsequent calls will be ignored.  Only one call to {@link #release}
+         * will be required, regardless of the number of times that {@code acquire} is called.
+         */
+        public void acquire() {
+            synchronized (mBinder) {
+                if (mRefCounted ? (++mRefCount == 1) : (!mHeld)) {
+                    try {
+                        mService.acquireWifiLock(mBinder, mLockType, mTag, mWorkSource);
+                        synchronized (WifiManager.this) {
+                            if (mActiveLockCount >= MAX_ACTIVE_LOCKS) {
+                                mService.releaseWifiLock(mBinder);
+                                throw new UnsupportedOperationException(
+                                            "Exceeded maximum number of wifi locks");
+                            }
+                            mActiveLockCount++;
+                        }
+                    } catch (RemoteException e) {
+                        throw e.rethrowFromSystemServer();
+                    }
+                    mHeld = true;
+                }
+            }
+        }
+
+        /**
+         * Unlocks the Wi-Fi radio, allowing it to turn off when the device is idle.
+         *
+         * If this WifiLock is reference-counted, each call to {@code release} will decrement the
+         * reference count, and the radio will be unlocked only when the reference count reaches
+         * zero.  If the reference count goes below zero (that is, if {@code release} is called
+         * a greater number of times than {@link #acquire}), an exception is thrown.
+         *
+         * If this WifiLock is not reference-counted, the first call to {@code release} (after
+         * the radio was locked using {@link #acquire}) will unlock the radio, and subsequent
+         * calls will be ignored.
+         */
+        public void release() {
+            synchronized (mBinder) {
+                if (mRefCounted ? (--mRefCount == 0) : (mHeld)) {
+                    try {
+                        mService.releaseWifiLock(mBinder);
+                        synchronized (WifiManager.this) {
+                            mActiveLockCount--;
+                        }
+                    } catch (RemoteException e) {
+                        throw e.rethrowFromSystemServer();
+                    }
+                    mHeld = false;
+                }
+                if (mRefCount < 0) {
+                    throw new RuntimeException("WifiLock under-locked " + mTag);
+                }
+            }
+        }
+
+        /**
+         * Controls whether this is a reference-counted or non-reference-counted WifiLock.
+         *
+         * Reference-counted WifiLocks keep track of the number of calls to {@link #acquire} and
+         * {@link #release}, and only allow the radio to sleep when every call to {@link #acquire}
+         * has been balanced with a call to {@link #release}.  Non-reference-counted WifiLocks
+         * lock the radio whenever {@link #acquire} is called and it is unlocked, and unlock the
+         * radio whenever {@link #release} is called and it is locked.
+         *
+         * @param refCounted true if this WifiLock should keep a reference count
+         */
+        public void setReferenceCounted(boolean refCounted) {
+            mRefCounted = refCounted;
+        }
+
+        /**
+         * Checks whether this WifiLock is currently held.
+         *
+         * @return true if this WifiLock is held, false otherwise
+         */
+        public boolean isHeld() {
+            synchronized (mBinder) {
+                return mHeld;
+            }
+        }
+
+        public void setWorkSource(WorkSource ws) {
+            synchronized (mBinder) {
+                if (ws != null && ws.isEmpty()) {
+                    ws = null;
+                }
+                boolean changed = true;
+                if (ws == null) {
+                    mWorkSource = null;
+                } else {
+                    ws.clearNames();
+                    if (mWorkSource == null) {
+                        changed = mWorkSource != null;
+                        mWorkSource = new WorkSource(ws);
+                    } else {
+                        changed = !mWorkSource.equals(ws);
+                        if (changed) {
+                            mWorkSource.set(ws);
+                        }
+                    }
+                }
+                if (changed && mHeld) {
+                    try {
+                        mService.updateWifiLockWorkSource(mBinder, mWorkSource);
+                    } catch (RemoteException e) {
+                        throw e.rethrowFromSystemServer();
+                    }
+                }
+            }
+        }
+
+        public String toString() {
+            String s1, s2, s3;
+            synchronized (mBinder) {
+                s1 = Integer.toHexString(System.identityHashCode(this));
+                s2 = mHeld ? "held; " : "";
+                if (mRefCounted) {
+                    s3 = "refcounted: refcount = " + mRefCount;
+                } else {
+                    s3 = "not refcounted";
+                }
+                return "WifiLock{ " + s1 + "; " + s2 + s3 + " }";
+            }
+        }
+
+        @Override
+        protected void finalize() throws Throwable {
+            super.finalize();
+            synchronized (mBinder) {
+                if (mHeld) {
+                    try {
+                        mService.releaseWifiLock(mBinder);
+                        synchronized (WifiManager.this) {
+                            mActiveLockCount--;
+                        }
+                    } catch (RemoteException e) {
+                        throw e.rethrowFromSystemServer();
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Creates a new WifiLock.
+     *
+     * @param lockType the type of lock to create. See {@link #WIFI_MODE_FULL_HIGH_PERF}
+     * and {@link #WIFI_MODE_FULL_LOW_LATENCY} for descriptions of the types of Wi-Fi locks.
+     * @param tag a tag for the WifiLock to identify it in debugging messages.  This string is
+     *            never shown to the user under normal conditions, but should be descriptive
+     *            enough to identify your application and the specific WifiLock within it, if it
+     *            holds multiple WifiLocks.
+     *
+     * @return a new, unacquired WifiLock with the given tag.
+     *
+     * @see WifiLock
+     */
+    public WifiLock createWifiLock(int lockType, String tag) {
+        return new WifiLock(lockType, tag);
+    }
+
+    /**
+     * Creates a new WifiLock.
+     *
+     * @param tag a tag for the WifiLock to identify it in debugging messages.  This string is
+     *            never shown to the user under normal conditions, but should be descriptive
+     *            enough to identify your application and the specific WifiLock within it, if it
+     *            holds multiple WifiLocks.
+     *
+     * @return a new, unacquired WifiLock with the given tag.
+     *
+     * @see WifiLock
+     *
+     * @deprecated This API is non-functional.
+     */
+    @Deprecated
+    public WifiLock createWifiLock(String tag) {
+        return new WifiLock(WIFI_MODE_FULL, tag);
+    }
+
+    /**
+     * Create a new MulticastLock
+     *
+     * @param tag a tag for the MulticastLock to identify it in debugging
+     *            messages.  This string is never shown to the user under
+     *            normal conditions, but should be descriptive enough to
+     *            identify your application and the specific MulticastLock
+     *            within it, if it holds multiple MulticastLocks.
+     *
+     * @return a new, unacquired MulticastLock with the given tag.
+     *
+     * @see MulticastLock
+     */
+    public MulticastLock createMulticastLock(String tag) {
+        return new MulticastLock(tag);
+    }
+
+    /**
+     * Allows an application to receive Wifi Multicast packets.
+     * Normally the Wifi stack filters out packets not explicitly
+     * addressed to this device.  Acquring a MulticastLock will
+     * cause the stack to receive packets addressed to multicast
+     * addresses.  Processing these extra packets can cause a noticeable
+     * battery drain and should be disabled when not needed.
+     */
+    public class MulticastLock {
+        private String mTag;
+        private final IBinder mBinder;
+        private int mRefCount;
+        private boolean mRefCounted;
+        private boolean mHeld;
+
+        private MulticastLock(String tag) {
+            mTag = tag;
+            mBinder = new Binder();
+            mRefCount = 0;
+            mRefCounted = true;
+            mHeld = false;
+        }
+
+        /**
+         * Locks Wifi Multicast on until {@link #release} is called.
+         *
+         * If this MulticastLock is reference-counted each call to
+         * {@code acquire} will increment the reference count, and the
+         * wifi interface will receive multicast packets as long as the
+         * reference count is above zero.
+         *
+         * If this MulticastLock is not reference-counted, the first call to
+         * {@code acquire} will turn on the multicast packets, but subsequent
+         * calls will be ignored.  Only one call to {@link #release} will
+         * be required, regardless of the number of times that {@code acquire}
+         * is called.
+         *
+         * Note that other applications may also lock Wifi Multicast on.
+         * Only they can relinquish their lock.
+         *
+         * Also note that applications cannot leave Multicast locked on.
+         * When an app exits or crashes, any Multicast locks will be released.
+         */
+        public void acquire() {
+            synchronized (mBinder) {
+                if (mRefCounted ? (++mRefCount == 1) : (!mHeld)) {
+                    try {
+                        mService.acquireMulticastLock(mBinder, mTag);
+                        synchronized (WifiManager.this) {
+                            if (mActiveLockCount >= MAX_ACTIVE_LOCKS) {
+                                mService.releaseMulticastLock(mTag);
+                                throw new UnsupportedOperationException(
+                                        "Exceeded maximum number of wifi locks");
+                            }
+                            mActiveLockCount++;
+                        }
+                    } catch (RemoteException e) {
+                        throw e.rethrowFromSystemServer();
+                    }
+                    mHeld = true;
+                }
+            }
+        }
+
+        /**
+         * Unlocks Wifi Multicast, restoring the filter of packets
+         * not addressed specifically to this device and saving power.
+         *
+         * If this MulticastLock is reference-counted, each call to
+         * {@code release} will decrement the reference count, and the
+         * multicast packets will only stop being received when the reference
+         * count reaches zero.  If the reference count goes below zero (that
+         * is, if {@code release} is called a greater number of times than
+         * {@link #acquire}), an exception is thrown.
+         *
+         * If this MulticastLock is not reference-counted, the first call to
+         * {@code release} (after the radio was multicast locked using
+         * {@link #acquire}) will unlock the multicast, and subsequent calls
+         * will be ignored.
+         *
+         * Note that if any other Wifi Multicast Locks are still outstanding
+         * this {@code release} call will not have an immediate effect.  Only
+         * when all applications have released all their Multicast Locks will
+         * the Multicast filter be turned back on.
+         *
+         * Also note that when an app exits or crashes all of its Multicast
+         * Locks will be automatically released.
+         */
+        public void release() {
+            synchronized (mBinder) {
+                if (mRefCounted ? (--mRefCount == 0) : (mHeld)) {
+                    try {
+                        mService.releaseMulticastLock(mTag);
+                        synchronized (WifiManager.this) {
+                            mActiveLockCount--;
+                        }
+                    } catch (RemoteException e) {
+                        throw e.rethrowFromSystemServer();
+                    }
+                    mHeld = false;
+                }
+                if (mRefCount < 0) {
+                    throw new RuntimeException("MulticastLock under-locked "
+                            + mTag);
+                }
+            }
+        }
+
+        /**
+         * Controls whether this is a reference-counted or non-reference-
+         * counted MulticastLock.
+         *
+         * Reference-counted MulticastLocks keep track of the number of calls
+         * to {@link #acquire} and {@link #release}, and only stop the
+         * reception of multicast packets when every call to {@link #acquire}
+         * has been balanced with a call to {@link #release}.  Non-reference-
+         * counted MulticastLocks allow the reception of multicast packets
+         * whenever {@link #acquire} is called and stop accepting multicast
+         * packets whenever {@link #release} is called.
+         *
+         * @param refCounted true if this MulticastLock should keep a reference
+         * count
+         */
+        public void setReferenceCounted(boolean refCounted) {
+            mRefCounted = refCounted;
+        }
+
+        /**
+         * Checks whether this MulticastLock is currently held.
+         *
+         * @return true if this MulticastLock is held, false otherwise
+         */
+        public boolean isHeld() {
+            synchronized (mBinder) {
+                return mHeld;
+            }
+        }
+
+        public String toString() {
+            String s1, s2, s3;
+            synchronized (mBinder) {
+                s1 = Integer.toHexString(System.identityHashCode(this));
+                s2 = mHeld ? "held; " : "";
+                if (mRefCounted) {
+                    s3 = "refcounted: refcount = " + mRefCount;
+                } else {
+                    s3 = "not refcounted";
+                }
+                return "MulticastLock{ " + s1 + "; " + s2 + s3 + " }";
+            }
+        }
+
+        @Override
+        protected void finalize() throws Throwable {
+            super.finalize();
+            setReferenceCounted(false);
+            release();
+        }
+    }
+
+    /**
+     * Check multicast filter status.
+     *
+     * @return true if multicast packets are allowed.
+     *
+     * @hide pending API council approval
+     */
+    public boolean isMulticastEnabled() {
+        try {
+            return mService.isMulticastEnabled();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Initialize the multicast filtering to 'on'
+     * @hide no intent to publish
+     */
+    @UnsupportedAppUsage
+    public boolean initializeMulticastFiltering() {
+        try {
+            mService.initializeMulticastFiltering();
+            return true;
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    protected void finalize() throws Throwable {
+        try {
+            if (mAsyncChannel != null) {
+                mAsyncChannel.disconnect();
+            }
+        } finally {
+            super.finalize();
+        }
+    }
+
+    /**
+     * Set wifi verbose log. Called from developer settings.
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.NETWORK_SETTINGS)
+    @UnsupportedAppUsage
+    public void enableVerboseLogging (int verbose) {
+        try {
+            mService.enableVerboseLogging(verbose);
+        } catch (Exception e) {
+            //ignore any failure here
+            Log.e(TAG, "enableVerboseLogging " + e.toString());
+        }
+    }
+
+    /**
+     * Get the WiFi verbose logging level.This is used by settings
+     * to decide what to show within the picker.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public int getVerboseLoggingLevel() {
+        try {
+            return mService.getVerboseLoggingLevel();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Removes all saved wifi networks.
+     *
+     * @hide
+     */
+    public void factoryReset() {
+        try {
+            mService.factoryReset(mContext.getOpPackageName());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Get Network object of current wifi network
+     * @return Get Network object of current wifi network
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public Network getCurrentNetwork() {
+        try {
+            return mService.getCurrentNetwork();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Deprecated
+     * returns false
+     * @hide
+     * @deprecated
+     */
+    public boolean setEnableAutoJoinWhenAssociated(boolean enabled) {
+        return false;
+    }
+
+    /**
+     * Deprecated
+     * returns false
+     * @hide
+     * @deprecated
+     */
+    public boolean getEnableAutoJoinWhenAssociated() {
+        return false;
+    }
+
+    /**
+     * Enable/disable WifiConnectivityManager
+     * @hide
+     */
+    public void enableWifiConnectivityManager(boolean enabled) {
+        try {
+            mService.enableWifiConnectivityManager(enabled);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Retrieve the data to be backed to save the current state.
+     * @hide
+     */
+    public byte[] retrieveBackupData() {
+        try {
+            return mService.retrieveBackupData();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Restore state from the backed up data.
+     * @hide
+     */
+    public void restoreBackupData(byte[] data) {
+        try {
+            mService.restoreBackupData(data);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Restore state from the older version of back up data.
+     * The old backup data was essentially a backup of wpa_supplicant.conf
+     * and ipconfig.txt file.
+     * @deprecated this is no longer supported.
+     * @hide
+     */
+    @Deprecated
+    public void restoreSupplicantBackupData(byte[] supplicantData, byte[] ipConfigData) {
+        try {
+            mService.restoreSupplicantBackupData(supplicantData, ipConfigData);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Start subscription provisioning flow
+     *
+     * @param provider {@link OsuProvider} to provision with
+     * @param executor the Executor on which to run the callback.
+     * @param callback {@link ProvisioningCallback} for updates regarding provisioning flow
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.NETWORK_SETTINGS,
+            android.Manifest.permission.NETWORK_SETUP_WIZARD
+    })
+    public void startSubscriptionProvisioning(@NonNull OsuProvider provider,
+            @NonNull @CallbackExecutor Executor executor, @NonNull ProvisioningCallback callback) {
+        // Verify arguments
+        if (executor == null) {
+            throw new IllegalArgumentException("executor must not be null");
+        }
+        if (callback == null) {
+            throw new IllegalArgumentException("callback must not be null");
+        }
+        try {
+            mService.startSubscriptionProvisioning(provider,
+                    new ProvisioningCallbackProxy(executor, callback));
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Helper class to support OSU Provisioning callbacks
+     */
+    private static class ProvisioningCallbackProxy extends IProvisioningCallback.Stub {
+        private final Executor mExecutor;
+        private final ProvisioningCallback mCallback;
+
+        ProvisioningCallbackProxy(Executor executor, ProvisioningCallback callback) {
+            mExecutor = executor;
+            mCallback = callback;
+        }
+
+        @Override
+        public void onProvisioningStatus(int status) {
+            mExecutor.execute(() -> mCallback.onProvisioningStatus(status));
+        }
+
+        @Override
+        public void onProvisioningFailure(int status) {
+            mExecutor.execute(() -> mCallback.onProvisioningFailure(status));
+        }
+
+        @Override
+        public void onProvisioningComplete() {
+            mExecutor.execute(() -> mCallback.onProvisioningComplete());
+        }
+    }
+
+    /**
+     * Base class for Traffic state callback. Should be extended by applications and set when
+     * calling {@link WifiManager#registerTrafficStateCallback(TrafficStateCallback, Handler)}.
+     * @hide
+     */
+    public interface TrafficStateCallback {
+        /**
+         * Lowest bit indicates data reception and the second lowest
+         * bit indicates data transmitted
+         */
+        /** @hide */
+        int DATA_ACTIVITY_NONE         = 0x00;
+        /** @hide */
+        int DATA_ACTIVITY_IN           = 0x01;
+        /** @hide */
+        int DATA_ACTIVITY_OUT          = 0x02;
+        /** @hide */
+        int DATA_ACTIVITY_INOUT        = 0x03;
+
+        /**
+         * Callback invoked to inform clients about the current traffic state.
+         *
+         * @param state One of the values: {@link #DATA_ACTIVITY_NONE}, {@link #DATA_ACTIVITY_IN},
+         * {@link #DATA_ACTIVITY_OUT} & {@link #DATA_ACTIVITY_INOUT}.
+         * @hide
+         */
+        void onStateChanged(int state);
+    }
+
+    /**
+     * Callback proxy for TrafficStateCallback objects.
+     *
+     * @hide
+     */
+    private class TrafficStateCallbackProxy extends ITrafficStateCallback.Stub {
+        private final Handler mHandler;
+        private final TrafficStateCallback mCallback;
+
+        TrafficStateCallbackProxy(Looper looper, TrafficStateCallback callback) {
+            mHandler = new Handler(looper);
+            mCallback = callback;
+        }
+
+        @Override
+        public void onStateChanged(int state) {
+            if (mVerboseLoggingEnabled) {
+                Log.v(TAG, "TrafficStateCallbackProxy: onStateChanged state=" + state);
+            }
+            mHandler.post(() -> {
+                mCallback.onStateChanged(state);
+            });
+        }
+    }
+
+    /**
+     * Registers a callback for monitoring traffic state. See {@link TrafficStateCallback}. These
+     * callbacks will be invoked periodically by platform to inform clients about the current
+     * traffic state. Caller can unregister a previously registered callback using
+     * {@link #unregisterTrafficStateCallback(TrafficStateCallback)}
+     * <p>
+     * Applications should have the
+     * {@link android.Manifest.permission#NETWORK_SETTINGS NETWORK_SETTINGS} permission. Callers
+     * without the permission will trigger a {@link java.lang.SecurityException}.
+     * <p>
+     *
+     * @param callback Callback for traffic state events
+     * @param handler  The Handler on whose thread to execute the callbacks of the {@code callback}
+     *                 object. If null, then the application's main thread will be used.
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.NETWORK_SETTINGS)
+    public void registerTrafficStateCallback(@NonNull TrafficStateCallback callback,
+                                             @Nullable Handler handler) {
+        if (callback == null) throw new IllegalArgumentException("callback cannot be null");
+        Log.v(TAG, "registerTrafficStateCallback: callback=" + callback + ", handler=" + handler);
+
+        Looper looper = (handler == null) ? mContext.getMainLooper() : handler.getLooper();
+        Binder binder = new Binder();
+        try {
+            mService.registerTrafficStateCallback(
+                    binder, new TrafficStateCallbackProxy(looper, callback), callback.hashCode());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Allow callers to unregister a previously registered callback. After calling this method,
+     * applications will no longer receive traffic state notifications.
+     *
+     * @param callback Callback to unregister for traffic state events
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.NETWORK_SETTINGS)
+    public void unregisterTrafficStateCallback(@NonNull TrafficStateCallback callback) {
+        if (callback == null) throw new IllegalArgumentException("callback cannot be null");
+        Log.v(TAG, "unregisterTrafficStateCallback: callback=" + callback);
+
+        try {
+            mService.unregisterTrafficStateCallback(callback.hashCode());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Helper method to update the local verbose logging flag based on the verbose logging
+     * level from wifi service.
+     */
+    private void updateVerboseLoggingEnabledFromService() {
+        mVerboseLoggingEnabled = getVerboseLoggingLevel() > 0;
+    }
+
+    /**
+     * @return true if this device supports WPA3-Personal SAE
+     */
+    public boolean isWpa3SaeSupported() {
+        return isFeatureSupported(WIFI_FEATURE_WPA3_SAE);
+    }
+
+    /**
+     * @return true if this device supports WPA3-Enterprise Suite-B-192
+     */
+    public boolean isWpa3SuiteBSupported() {
+        return isFeatureSupported(WIFI_FEATURE_WPA3_SUITE_B);
+    }
+
+    /**
+     * @return true if this device supports Wi-Fi Enhanced Open (OWE)
+     */
+    public boolean isEnhancedOpenSupported() {
+        return isFeatureSupported(WIFI_FEATURE_OWE);
+    }
+
+    /**
+     * Wi-Fi Easy Connect (DPP) introduces standardized mechanisms to simplify the provisioning and
+     * configuration of Wi-Fi devices.
+     * For more details, visit <a href="https://www.wi-fi.org/">https://www.wi-fi.org/</a> and
+     * search for "Easy Connect" or "Device Provisioning Protocol specification".
+     *
+     * @return true if this device supports Wi-Fi Easy-connect (Device Provisioning Protocol)
+     */
+    public boolean isEasyConnectSupported() {
+        return isFeatureSupported(WIFI_FEATURE_DPP);
+    }
+
+    /**
+     * Gets the factory Wi-Fi MAC addresses.
+     * @return Array of String representing Wi-Fi MAC addresses sorted lexically or an empty Array
+     * if failed.
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.NETWORK_SETTINGS)
+    public String[] getFactoryMacAddresses() {
+        try {
+            return mService.getFactoryMacAddresses();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = {"DEVICE_MOBILITY_STATE_"}, value = {
+            DEVICE_MOBILITY_STATE_UNKNOWN,
+            DEVICE_MOBILITY_STATE_HIGH_MVMT,
+            DEVICE_MOBILITY_STATE_LOW_MVMT,
+            DEVICE_MOBILITY_STATE_STATIONARY})
+    public @interface DeviceMobilityState {}
+
+    /**
+     * Unknown device mobility state
+     *
+     * @see #setDeviceMobilityState(int)
+     *
+     * @hide
+     */
+    @SystemApi
+    public static final int DEVICE_MOBILITY_STATE_UNKNOWN = 0;
+
+    /**
+     * High movement device mobility state.
+     * e.g. on a bike, in a motor vehicle
+     *
+     * @see #setDeviceMobilityState(int)
+     *
+     * @hide
+     */
+    @SystemApi
+    public static final int DEVICE_MOBILITY_STATE_HIGH_MVMT = 1;
+
+    /**
+     * Low movement device mobility state.
+     * e.g. walking, running
+     *
+     * @see #setDeviceMobilityState(int)
+     *
+     * @hide
+     */
+    @SystemApi
+    public static final int DEVICE_MOBILITY_STATE_LOW_MVMT = 2;
+
+    /**
+     * Stationary device mobility state
+     *
+     * @see #setDeviceMobilityState(int)
+     *
+     * @hide
+     */
+    @SystemApi
+    public static final int DEVICE_MOBILITY_STATE_STATIONARY = 3;
+
+    /**
+     * Updates the device mobility state. Wifi uses this information to adjust the interval between
+     * Wifi scans in order to balance power consumption with scan accuracy.
+     * The default mobility state when the device boots is {@link #DEVICE_MOBILITY_STATE_UNKNOWN}.
+     * This API should be called whenever there is a change in the mobility state.
+     * @param state the updated device mobility state
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.WIFI_SET_DEVICE_MOBILITY_STATE)
+    public void setDeviceMobilityState(@DeviceMobilityState int state) {
+        try {
+            mService.setDeviceMobilityState(state);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /* Easy Connect - AKA Device Provisioning Protocol (DPP) */
+
+    /**
+     * Easy Connect Network role: Station.
+     *
+     * @hide
+     */
+    @SystemApi
+    public static final int EASY_CONNECT_NETWORK_ROLE_STA = 0;
+
+    /**
+     * Easy Connect Network role: Access Point.
+     *
+     * @hide
+     */
+    @SystemApi
+    public static final int EASY_CONNECT_NETWORK_ROLE_AP = 1;
+
+    /** @hide */
+    @IntDef(prefix = {"EASY_CONNECT_NETWORK_ROLE_"}, value = {
+            EASY_CONNECT_NETWORK_ROLE_STA,
+            EASY_CONNECT_NETWORK_ROLE_AP,
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface EasyConnectNetworkRole {
+    }
+
+    /**
+     * Start Easy Connect (DPP) in Configurator-Initiator role. The current device will initiate
+     * Easy Connect bootstrapping with a peer, and configure the peer with the SSID and password of
+     * the specified network using the Easy Connect protocol on an encrypted link.
+     *
+     * @param enrolleeUri         URI of the Enrollee obtained separately (e.g. QR code scanning)
+     * @param selectedNetworkId   Selected network ID to be sent to the peer
+     * @param enrolleeNetworkRole The network role of the enrollee
+     * @param callback            Callback for status updates
+     * @param executor            The Executor on which to run the callback.
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.NETWORK_SETTINGS,
+            android.Manifest.permission.NETWORK_SETUP_WIZARD})
+    public void startEasyConnectAsConfiguratorInitiator(@NonNull String enrolleeUri,
+            int selectedNetworkId, @EasyConnectNetworkRole int enrolleeNetworkRole,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull EasyConnectStatusCallback callback) {
+        Binder binder = new Binder();
+        try {
+            mService.startDppAsConfiguratorInitiator(binder, enrolleeUri, selectedNetworkId,
+                    enrolleeNetworkRole, new EasyConnectCallbackProxy(executor, callback));
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Start Easy Connect (DPP) in Enrollee-Initiator role. The current device will initiate Easy
+     * Connect bootstrapping with a peer, and receive the SSID and password from the peer
+     * configurator.
+     *
+     * @param configuratorUri URI of the Configurator obtained separately (e.g. QR code scanning)
+     * @param callback        Callback for status updates
+     * @param executor        The Executor on which to run the callback.
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.NETWORK_SETTINGS,
+            android.Manifest.permission.NETWORK_SETUP_WIZARD})
+    public void startEasyConnectAsEnrolleeInitiator(@NonNull String configuratorUri,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull EasyConnectStatusCallback callback) {
+        Binder binder = new Binder();
+        try {
+            mService.startDppAsEnrolleeInitiator(binder, configuratorUri,
+                    new EasyConnectCallbackProxy(executor, callback));
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Stop or abort a current Easy Connect (DPP) session. This call, once processed, will
+     * terminate any ongoing transaction, and clean up all associated resources. Caller should not
+     * expect any callbacks once this call is made. However, due to the asynchronous nature of
+     * this call, a callback may be fired if it was already pending in the queue.
+     *
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(anyOf = {
+            android.Manifest.permission.NETWORK_SETTINGS,
+            android.Manifest.permission.NETWORK_SETUP_WIZARD})
+    public void stopEasyConnectSession() {
+        try {
+            /* Request lower layers to stop/abort and clear resources */
+            mService.stopDppSession();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Helper class to support Easy Connect (DPP) callbacks
+     *
+     * @hide
+     */
+    private static class EasyConnectCallbackProxy extends IDppCallback.Stub {
+        private final Executor mExecutor;
+        private final EasyConnectStatusCallback mEasyConnectStatusCallback;
+
+        EasyConnectCallbackProxy(Executor executor,
+                EasyConnectStatusCallback easyConnectStatusCallback) {
+            mExecutor = executor;
+            mEasyConnectStatusCallback = easyConnectStatusCallback;
+        }
+
+        @Override
+        public void onSuccessConfigReceived(int newNetworkId) {
+            Log.d(TAG, "Easy Connect onSuccessConfigReceived callback");
+            mExecutor.execute(() -> {
+                mEasyConnectStatusCallback.onEnrolleeSuccess(newNetworkId);
+            });
+        }
+
+        @Override
+        public void onSuccess(int status) {
+            Log.d(TAG, "Easy Connect onSuccess callback");
+            mExecutor.execute(() -> {
+                mEasyConnectStatusCallback.onConfiguratorSuccess(status);
+            });
+        }
+
+        @Override
+        public void onFailure(int status) {
+            Log.d(TAG, "Easy Connect onFailure callback");
+            mExecutor.execute(() -> {
+                mEasyConnectStatusCallback.onFailure(status);
+            });
+        }
+
+        @Override
+        public void onProgress(int status) {
+            Log.d(TAG, "Easy Connect onProgress callback");
+            mExecutor.execute(() -> {
+                mEasyConnectStatusCallback.onProgress(status);
+            });
+        }
+    }
+
+    /**
+     * Interface for Wi-Fi usability statistics listener. Should be implemented by applications and
+     * set when calling {@link WifiManager#addOnWifiUsabilityStatsListener(Executor,
+     * OnWifiUsabilityStatsListener)}.
+     *
+     * @hide
+     */
+    @SystemApi
+    public interface OnWifiUsabilityStatsListener {
+        /**
+         * Called when Wi-Fi usability statistics is updated.
+         *
+         * @param seqNum The sequence number of statistics, used to derive the timing of updated
+         *               Wi-Fi usability statistics, set by framework and incremented by one after
+         *               each update.
+         * @param isSameBssidAndFreq The flag to indicate whether the BSSID and the frequency of
+         *                           network stays the same or not relative to the last update of
+         *                           Wi-Fi usability stats.
+         * @param stats The updated Wi-Fi usability statistics.
+         */
+        void onWifiUsabilityStats(int seqNum, boolean isSameBssidAndFreq,
+                @NonNull WifiUsabilityStatsEntry stats);
+    }
+
+    /**
+     * Adds a listener for Wi-Fi usability statistics. See {@link OnWifiUsabilityStatsListener}.
+     * Multiple listeners can be added. Callers will be invoked periodically by framework to
+     * inform clients about the current Wi-Fi usability statistics. Callers can remove a previously
+     * added listener using {@link removeOnWifiUsabilityStatsListener}.
+     *
+     * @param executor The executor on which callback will be invoked.
+     * @param listener Listener for Wifi usability statistics.
+     *
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.WIFI_UPDATE_USABILITY_STATS_SCORE)
+    public void addOnWifiUsabilityStatsListener(@NonNull @CallbackExecutor Executor executor,
+            @NonNull OnWifiUsabilityStatsListener listener) {
+        if (executor == null) throw new IllegalArgumentException("executor cannot be null");
+        if (listener == null) throw new IllegalArgumentException("listener cannot be null");
+        if (mVerboseLoggingEnabled) {
+            Log.v(TAG, "addOnWifiUsabilityStatsListener: listener=" + listener);
+        }
+        try {
+            mService.addOnWifiUsabilityStatsListener(new Binder(),
+                    new IOnWifiUsabilityStatsListener.Stub() {
+                        @Override
+                        public void onWifiUsabilityStats(int seqNum, boolean isSameBssidAndFreq,
+                                WifiUsabilityStatsEntry stats) {
+                            if (mVerboseLoggingEnabled) {
+                                Log.v(TAG, "OnWifiUsabilityStatsListener: "
+                                        + "onWifiUsabilityStats: seqNum=" + seqNum);
+                            }
+                            Binder.withCleanCallingIdentity(() ->
+                                    executor.execute(() -> listener.onWifiUsabilityStats(seqNum,
+                                            isSameBssidAndFreq, stats)));
+                        }
+                    },
+                    listener.hashCode()
+            );
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Allow callers to remove a previously registered listener. After calling this method,
+     * applications will no longer receive Wi-Fi usability statistics.
+     *
+     * @param listener Listener to remove the Wi-Fi usability statistics.
+     *
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.WIFI_UPDATE_USABILITY_STATS_SCORE)
+    public void removeOnWifiUsabilityStatsListener(@NonNull OnWifiUsabilityStatsListener listener) {
+        if (listener == null) throw new IllegalArgumentException("listener cannot be null");
+        if (mVerboseLoggingEnabled) {
+            Log.v(TAG, "removeOnWifiUsabilityStatsListener: listener=" + listener);
+        }
+        try {
+            mService.removeOnWifiUsabilityStatsListener(listener.hashCode());
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Provide a Wi-Fi usability score information to be recorded (but not acted upon) by the
+     * framework. The Wi-Fi usability score is derived from {@link OnWifiUsabilityStatsListener}
+     * where a score is matched to Wi-Fi usability statistics using the sequence number. The score
+     * is used to quantify whether Wi-Fi is usable in a future time.
+     *
+     * @param seqNum Sequence number of the Wi-Fi usability score.
+     * @param score The Wi-Fi usability score, expected range: [0, 100].
+     * @param predictionHorizonSec Prediction horizon of the Wi-Fi usability score in second,
+     *                             expected range: [0, 30].
+     *
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.WIFI_UPDATE_USABILITY_STATS_SCORE)
+    public void updateWifiUsabilityScore(int seqNum, int score, int predictionHorizonSec) {
+        try {
+            mService.updateWifiUsabilityScore(seqNum, score, predictionHorizonSec);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+}
diff --git a/android/net/wifi/WifiNetworkAgentSpecifier.java b/android/net/wifi/WifiNetworkAgentSpecifier.java
new file mode 100644
index 0000000..24aa23a
--- /dev/null
+++ b/android/net/wifi/WifiNetworkAgentSpecifier.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright (C) 2018 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.wifi;
+
+import static com.android.internal.util.Preconditions.checkNotNull;
+import static com.android.internal.util.Preconditions.checkState;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.MacAddress;
+import android.net.MatchAllNetworkSpecifier;
+import android.net.NetworkAgent;
+import android.net.NetworkRequest;
+import android.net.NetworkSpecifier;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import java.util.Objects;
+
+/**
+ * Network specifier object used by wifi's {@link android.net.NetworkAgent}.
+ * @hide
+ */
+public final class WifiNetworkAgentSpecifier extends NetworkSpecifier implements Parcelable {
+    /**
+     * Security credentials for the currently connected network.
+     */
+    private final WifiConfiguration mWifiConfiguration;
+
+    /**
+     * The UID of the app that requested a specific wifi network using {@link WifiNetworkSpecifier}.
+     *
+     * Will only be filled when the device connects to a wifi network as a result of a
+     * {@link NetworkRequest} with {@link WifiNetworkSpecifier}. Will be set to -1 if the device
+     * auto-connected to a wifi network.
+     */
+    private final int mOriginalRequestorUid;
+
+    /**
+     * The package name of the app that requested a specific wifi network using
+     * {@link WifiNetworkSpecifier}.
+     *
+     * Will only be filled when the device connects to a wifi network as a result of a
+     * {@link NetworkRequest} with {@link WifiNetworkSpecifier}. Will be set to null if the device
+     * auto-connected to a wifi network.
+     */
+    private final String mOriginalRequestorPackageName;
+
+    public WifiNetworkAgentSpecifier(@NonNull WifiConfiguration wifiConfiguration,
+                                     int originalRequestorUid,
+                                     @Nullable String originalRequestorPackageName) {
+        checkNotNull(wifiConfiguration);
+
+        mWifiConfiguration = wifiConfiguration;
+        mOriginalRequestorUid = originalRequestorUid;
+        mOriginalRequestorPackageName = originalRequestorPackageName;
+    }
+
+    /**
+     * @hide
+     */
+    public static final @android.annotation.NonNull Creator<WifiNetworkAgentSpecifier> CREATOR =
+            new Creator<WifiNetworkAgentSpecifier>() {
+                @Override
+                public WifiNetworkAgentSpecifier createFromParcel(@NonNull Parcel in) {
+                    WifiConfiguration wifiConfiguration = in.readParcelable(null);
+                    int originalRequestorUid = in.readInt();
+                    String originalRequestorPackageName = in.readString();
+                    return new WifiNetworkAgentSpecifier(
+                            wifiConfiguration, originalRequestorUid, originalRequestorPackageName);
+                }
+
+                @Override
+                public WifiNetworkAgentSpecifier[] newArray(int size) {
+                    return new WifiNetworkAgentSpecifier[size];
+                }
+            };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(@NonNull Parcel dest, int flags) {
+        dest.writeParcelable(mWifiConfiguration, flags);
+        dest.writeInt(mOriginalRequestorUid);
+        dest.writeString(mOriginalRequestorPackageName);
+    }
+
+    @Override
+    public boolean satisfiedBy(@Nullable NetworkSpecifier other) {
+        if (this == other) {
+            return true;
+        }
+        // Any generic requests should be satisifed by a specific wifi network.
+        if (other == null || other instanceof MatchAllNetworkSpecifier) {
+            return true;
+        }
+        if (other instanceof WifiNetworkSpecifier) {
+            return satisfiesNetworkSpecifier((WifiNetworkSpecifier) other);
+        }
+        return equals(other);
+    }
+
+    /**
+     * Match {@link WifiNetworkSpecifier} in app's {@link NetworkRequest} with the
+     * {@link WifiNetworkAgentSpecifier} in wifi platform's {@link NetworkAgent}.
+     */
+    public boolean satisfiesNetworkSpecifier(@NonNull WifiNetworkSpecifier ns) {
+        // None of these should be null by construction.
+        // {@link WifiNetworkSpecifier.Builder} enforces non-null in {@link WifiNetworkSpecifier}.
+        // {@link WifiNetworkFactory} ensures non-null in {@link WifiNetworkAgentSpecifier}.
+        checkNotNull(ns);
+        checkNotNull(ns.ssidPatternMatcher);
+        checkNotNull(ns.bssidPatternMatcher);
+        checkNotNull(ns.wifiConfiguration.allowedKeyManagement);
+        checkNotNull(this.mWifiConfiguration.SSID);
+        checkNotNull(this.mWifiConfiguration.BSSID);
+        checkNotNull(this.mWifiConfiguration.allowedKeyManagement);
+
+        final String ssidWithQuotes = this.mWifiConfiguration.SSID;
+        checkState(ssidWithQuotes.startsWith("\"") && ssidWithQuotes.endsWith("\""));
+        final String ssidWithoutQuotes = ssidWithQuotes.substring(1, ssidWithQuotes.length() - 1);
+        if (!ns.ssidPatternMatcher.match(ssidWithoutQuotes)) {
+            return false;
+        }
+        final MacAddress bssid = MacAddress.fromString(this.mWifiConfiguration.BSSID);
+        final MacAddress matchBaseAddress = ns.bssidPatternMatcher.first;
+        final MacAddress matchMask = ns.bssidPatternMatcher.second;
+        if (!bssid.matches(matchBaseAddress, matchMask))  {
+            return false;
+        }
+        if (!ns.wifiConfiguration.allowedKeyManagement.equals(
+                this.mWifiConfiguration.allowedKeyManagement)) {
+            return false;
+        }
+        if (ns.requestorUid != this.mOriginalRequestorUid) {
+            return false;
+        }
+        if (!TextUtils.equals(ns.requestorPackageName, this.mOriginalRequestorPackageName)) {
+            return false;
+        }
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(
+                mWifiConfiguration.SSID,
+                mWifiConfiguration.BSSID,
+                mWifiConfiguration.allowedKeyManagement,
+                mOriginalRequestorUid,
+                mOriginalRequestorPackageName);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (!(obj instanceof WifiNetworkAgentSpecifier)) {
+            return false;
+        }
+        WifiNetworkAgentSpecifier lhs = (WifiNetworkAgentSpecifier) obj;
+        return Objects.equals(this.mWifiConfiguration.SSID, lhs.mWifiConfiguration.SSID)
+                && Objects.equals(this.mWifiConfiguration.BSSID, lhs.mWifiConfiguration.BSSID)
+                && Objects.equals(this.mWifiConfiguration.allowedKeyManagement,
+                    lhs.mWifiConfiguration.allowedKeyManagement)
+                && mOriginalRequestorUid == lhs.mOriginalRequestorUid
+                && TextUtils.equals(mOriginalRequestorPackageName,
+                lhs.mOriginalRequestorPackageName);
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder("WifiNetworkAgentSpecifier [");
+        sb.append("WifiConfiguration=")
+                .append(", SSID=").append(mWifiConfiguration.SSID)
+                .append(", BSSID=").append(mWifiConfiguration.BSSID)
+                .append(", mOriginalRequestorUid=").append(mOriginalRequestorUid)
+                .append(", mOriginalRequestorPackageName=").append(mOriginalRequestorPackageName)
+                .append("]");
+        return sb.toString();
+    }
+
+    @Override
+    public void assertValidFromUid(int requestorUid) {
+        throw new IllegalStateException("WifiNetworkAgentSpecifier should never be used "
+                + "for requests.");
+    }
+
+    @Override
+    public NetworkSpecifier redact() {
+        return null;
+    }
+}
diff --git a/android/net/wifi/WifiNetworkConnectionStatistics.java b/android/net/wifi/WifiNetworkConnectionStatistics.java
new file mode 100644
index 0000000..8262a7a
--- /dev/null
+++ b/android/net/wifi/WifiNetworkConnectionStatistics.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi;
+
+import android.annotation.SystemApi;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * Connection Statistics For a WiFi Network.
+ * @hide
+ */
+@SystemApi
+public class WifiNetworkConnectionStatistics implements Parcelable {
+    private static final String TAG = "WifiNetworkConnnectionStatistics";
+
+    public int numConnection;
+    public int numUsage;
+
+    public WifiNetworkConnectionStatistics(int connection, int usage) {
+        numConnection = connection;
+        numUsage = usage;
+    }
+
+    public WifiNetworkConnectionStatistics() { }
+
+
+    @Override
+    public String toString() {
+        StringBuilder sbuf = new StringBuilder();
+        sbuf.append("c=").append(numConnection);
+        sbuf.append(" u=").append(numUsage);
+        return sbuf.toString();
+    }
+
+
+    /** copy constructor*/
+    public WifiNetworkConnectionStatistics(WifiNetworkConnectionStatistics source) {
+        numConnection = source.numConnection;
+        numUsage = source.numUsage;
+    }
+
+    /** Implement the Parcelable interface */
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Implement the Parcelable interface */
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(numConnection);
+        dest.writeInt(numUsage);
+    }
+
+    /** Implement the Parcelable interface */
+    public static final @android.annotation.NonNull Creator<WifiNetworkConnectionStatistics> CREATOR =
+        new Creator<WifiNetworkConnectionStatistics>() {
+            public WifiNetworkConnectionStatistics createFromParcel(Parcel in) {
+                int numConnection = in.readInt();
+                int numUsage = in.readInt();
+                WifiNetworkConnectionStatistics stats =
+                        new WifiNetworkConnectionStatistics(numConnection, numUsage);
+                return stats;
+            }
+
+            public WifiNetworkConnectionStatistics[] newArray(int size) {
+                return new WifiNetworkConnectionStatistics[size];
+            }
+        };
+}
diff --git a/android/net/wifi/WifiNetworkScoreCache.java b/android/net/wifi/WifiNetworkScoreCache.java
new file mode 100644
index 0000000..b22ae07
--- /dev/null
+++ b/android/net/wifi/WifiNetworkScoreCache.java
@@ -0,0 +1,315 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi;
+
+import android.Manifest.permission;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.net.INetworkScoreCache;
+import android.net.NetworkKey;
+import android.net.ScoredNetwork;
+import android.os.Handler;
+import android.os.Process;
+import android.util.Log;
+import android.util.LruCache;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.util.List;
+
+/**
+ * {@link INetworkScoreCache} implementation for Wifi Networks.
+ *
+ * @hide
+ */
+public class WifiNetworkScoreCache extends INetworkScoreCache.Stub {
+    private static final String TAG = "WifiNetworkScoreCache";
+    private static final boolean DBG = Log.isLoggable(TAG, Log.DEBUG);
+
+    // A Network scorer returns a score in the range [-128, +127]
+    // We treat the lowest possible score as though there were no score, effectively allowing the
+    // scorer to provide an RSSI threshold below which a network should not be used.
+    public static final int INVALID_NETWORK_SCORE = Byte.MIN_VALUE;
+
+    /** Default number entries to be stored in the {@link LruCache}. */
+    private static final int DEFAULT_MAX_CACHE_SIZE = 100;
+
+    // See {@link #CacheListener}.
+    @Nullable
+    @GuardedBy("mLock")
+    private CacheListener mListener;
+
+    private final Context mContext;
+    private final Object mLock = new Object();
+
+    // The key is of the form "<ssid>"<bssid>
+    // TODO: What about SSIDs that can't be encoded as UTF-8?
+    @GuardedBy("mLock")
+    private final LruCache<String, ScoredNetwork> mCache;
+
+    public WifiNetworkScoreCache(Context context) {
+        this(context, null /* listener */);
+    }
+
+    /**
+     * Instantiates a WifiNetworkScoreCache.
+     *
+     * @param context Application context
+     * @param listener CacheListener for cache updates
+     */
+    public WifiNetworkScoreCache(Context context, @Nullable CacheListener listener) {
+        this(context, listener, DEFAULT_MAX_CACHE_SIZE);
+    }
+
+    public WifiNetworkScoreCache(
+            Context context, @Nullable CacheListener listener, int maxCacheSize) {
+        mContext = context.getApplicationContext();
+        mListener = listener;
+        mCache = new LruCache<>(maxCacheSize);
+    }
+
+    @Override public final void updateScores(List<ScoredNetwork> networks) {
+        if (networks == null || networks.isEmpty()) {
+           return;
+        }
+        if (DBG) {
+            Log.d(TAG, "updateScores list size=" + networks.size());
+        }
+
+        boolean changed = false;
+
+        synchronized(mLock) {
+            for (ScoredNetwork network : networks) {
+                String networkKey = buildNetworkKey(network);
+                if (networkKey == null) {
+                    if (DBG) {
+                        Log.d(TAG, "Failed to build network key for ScoredNetwork" + network);
+                    }
+                    continue;
+                }
+                mCache.put(networkKey, network);
+                changed = true;
+            }
+
+            if (mListener != null && changed) {
+                mListener.post(networks);
+            }
+        }
+    }
+
+    @Override public final void clearScores() {
+        synchronized (mLock) {
+            mCache.evictAll();
+        }
+    }
+
+    /**
+     * Returns whether there is any score info for the given ScanResult.
+     *
+     * This includes null-score info, so it should only be used when determining whether to request
+     * scores from the network scorer.
+     */
+    public boolean isScoredNetwork(ScanResult result) {
+        return getScoredNetwork(result) != null;
+    }
+
+    /**
+     * Returns whether there is a non-null score curve for the given ScanResult.
+     *
+     * A null score curve has special meaning - we should never connect to an ephemeral network if
+     * the score curve is null.
+     */
+    public boolean hasScoreCurve(ScanResult result) {
+        ScoredNetwork network = getScoredNetwork(result);
+        return network != null && network.rssiCurve != null;
+    }
+
+    public int getNetworkScore(ScanResult result) {
+        int score = INVALID_NETWORK_SCORE;
+
+        ScoredNetwork network = getScoredNetwork(result);
+        if (network != null && network.rssiCurve != null) {
+            score = network.rssiCurve.lookupScore(result.level);
+            if (DBG) {
+                Log.d(TAG, "getNetworkScore found scored network " + network.networkKey
+                        + " score " + Integer.toString(score)
+                        + " RSSI " + result.level);
+            }
+        }
+        return score;
+    }
+
+    /**
+     * Returns the ScoredNetwork metered hint for a given ScanResult.
+     *
+     * If there is no ScoredNetwork associated with the ScanResult then false will be returned.
+     */
+    public boolean getMeteredHint(ScanResult result) {
+        ScoredNetwork network = getScoredNetwork(result);
+        return network != null && network.meteredHint;
+    }
+
+    public int getNetworkScore(ScanResult result, boolean isActiveNetwork) {
+        int score = INVALID_NETWORK_SCORE;
+
+        ScoredNetwork network = getScoredNetwork(result);
+        if (network != null && network.rssiCurve != null) {
+            score = network.rssiCurve.lookupScore(result.level, isActiveNetwork);
+            if (DBG) {
+                Log.d(TAG, "getNetworkScore found scored network " + network.networkKey
+                        + " score " + Integer.toString(score)
+                        + " RSSI " + result.level
+                        + " isActiveNetwork " + isActiveNetwork);
+            }
+        }
+        return score;
+    }
+
+    @Nullable
+    public ScoredNetwork getScoredNetwork(ScanResult result) {
+        String key = buildNetworkKey(result);
+        if (key == null) return null;
+
+        synchronized(mLock) {
+            ScoredNetwork network = mCache.get(key);
+            return network;
+        }
+    }
+
+    /** Returns the ScoredNetwork for the given key. */
+    @Nullable
+    public ScoredNetwork getScoredNetwork(NetworkKey networkKey) {
+        String key = buildNetworkKey(networkKey);
+        if (key == null) {
+            if (DBG) {
+                Log.d(TAG, "Could not build key string for Network Key: " + networkKey);
+            }
+            return null;
+        }
+        synchronized (mLock) {
+            return mCache.get(key);
+        }
+    }
+
+    private String buildNetworkKey(ScoredNetwork network) {
+        if (network == null) {
+            return null;
+        }
+        return buildNetworkKey(network.networkKey);
+    }
+
+    private String buildNetworkKey(NetworkKey networkKey) {
+        if (networkKey == null) {
+            return null;
+        }
+        if (networkKey.wifiKey == null) return null;
+        if (networkKey.type == NetworkKey.TYPE_WIFI) {
+            String key = networkKey.wifiKey.ssid;
+            if (key == null) return null;
+            if (networkKey.wifiKey.bssid != null) {
+                key = key + networkKey.wifiKey.bssid;
+            }
+            return key;
+        }
+        return null;
+    }
+
+    private String buildNetworkKey(ScanResult result) {
+        if (result == null || result.SSID == null) {
+            return null;
+        }
+        StringBuilder key = new StringBuilder("\"");
+        key.append(result.SSID);
+        key.append("\"");
+        if (result.BSSID != null) {
+            key.append(result.BSSID);
+        }
+        return key.toString();
+    }
+
+    @Override protected final void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
+        mContext.enforceCallingOrSelfPermission(permission.DUMP, TAG);
+        String header = String.format("WifiNetworkScoreCache (%s/%d)",
+                mContext.getPackageName(), Process.myUid());
+        writer.println(header);
+        writer.println("  All score curves:");
+        synchronized (mLock) {
+            for (ScoredNetwork score : mCache.snapshot().values()) {
+                writer.println("    " + score);
+            }
+            writer.println("  Network scores for latest ScanResults:");
+            WifiManager wifiManager = (WifiManager) mContext.getSystemService(Context.WIFI_SERVICE);
+            for (ScanResult scanResult : wifiManager.getScanResults()) {
+                writer.println(
+                        "    " + buildNetworkKey(scanResult) + ": " + getNetworkScore(scanResult));
+            }
+        }
+    }
+
+    /** Registers a CacheListener instance, replacing the previous listener if it existed. */
+    public void registerListener(CacheListener listener) {
+        synchronized (mLock) {
+            mListener = listener;
+        }
+    }
+
+    /** Removes the registered CacheListener. */
+    public void unregisterListener() {
+        synchronized (mLock) {
+            mListener = null;
+        }
+    }
+
+    /** Listener for updates to the cache inside WifiNetworkScoreCache. */
+    public abstract static class CacheListener {
+        private Handler mHandler;
+
+        /**
+         * Constructor for CacheListener.
+         *
+         * @param handler the Handler on which to invoke the {@link #networkCacheUpdated} method.
+         *          This cannot be null.
+         */
+        public CacheListener(@NonNull Handler handler) {
+            Preconditions.checkNotNull(handler);
+            mHandler = handler;
+        }
+
+        /** Invokes the {@link #networkCacheUpdated(List<ScoredNetwork>)} method on the handler. */
+        void post(List<ScoredNetwork> updatedNetworks) {
+            mHandler.post(new Runnable() {
+                @Override
+                public void run() {
+                    networkCacheUpdated(updatedNetworks);
+                }
+            });
+        }
+
+        /**
+         * Invoked whenever the cache is updated.
+         *
+         * <p>Clearing the cache does not invoke this method.
+         *
+         * @param updatedNetworks the networks that were updated
+         */
+        public abstract void networkCacheUpdated(List<ScoredNetwork> updatedNetworks);
+    }
+}
diff --git a/android/net/wifi/WifiNetworkSpecifier.java b/android/net/wifi/WifiNetworkSpecifier.java
new file mode 100644
index 0000000..6c2d7ff
--- /dev/null
+++ b/android/net/wifi/WifiNetworkSpecifier.java
@@ -0,0 +1,609 @@
+/*
+ * Copyright (C) 2018 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.wifi;
+
+import static com.android.internal.util.Preconditions.checkNotNull;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityThread;
+import android.net.MacAddress;
+import android.net.MatchAllNetworkSpecifier;
+import android.net.NetworkRequest;
+import android.net.NetworkSpecifier;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.PatternMatcher;
+import android.os.Process;
+import android.text.TextUtils;
+import android.util.Pair;
+
+import java.nio.charset.CharsetEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Objects;
+
+/**
+ * Network specifier object used to request a local Wi-Fi network. Apps should use the
+ * {@link WifiNetworkSpecifier.Builder} class to create an instance.
+ */
+public final class WifiNetworkSpecifier extends NetworkSpecifier implements Parcelable {
+
+    /**
+     * Builder used to create {@link WifiNetworkSpecifier} objects.
+     */
+    public static final class Builder {
+        private static final String MATCH_ALL_SSID_PATTERN_PATH = ".*";
+        private static final String MATCH_EMPTY_SSID_PATTERN_PATH = "";
+        private static final Pair<MacAddress, MacAddress> MATCH_NO_BSSID_PATTERN1 =
+                new Pair(MacAddress.BROADCAST_ADDRESS, MacAddress.BROADCAST_ADDRESS);
+        private static final Pair<MacAddress, MacAddress> MATCH_NO_BSSID_PATTERN2 =
+                new Pair(MacAddress.ALL_ZEROS_ADDRESS, MacAddress.BROADCAST_ADDRESS);
+        private static final Pair<MacAddress, MacAddress> MATCH_ALL_BSSID_PATTERN =
+                new Pair(MacAddress.ALL_ZEROS_ADDRESS, MacAddress.ALL_ZEROS_ADDRESS);
+        private static final MacAddress MATCH_EXACT_BSSID_PATTERN_MASK =
+                MacAddress.BROADCAST_ADDRESS;
+
+        /**
+         * SSID pattern match specified by the app.
+         */
+        private @Nullable PatternMatcher mSsidPatternMatcher;
+        /**
+         * BSSID pattern match specified by the app.
+         * Pair of <BaseAddress, Mask>.
+         */
+        private @Nullable Pair<MacAddress, MacAddress> mBssidPatternMatcher;
+        /**
+         * Whether this is an OWE network or not.
+         */
+        private boolean mIsEnhancedOpen;
+        /**
+         * Pre-shared key for use with WPA-PSK networks.
+         */
+        private @Nullable String mWpa2PskPassphrase;
+        /**
+         * Pre-shared key for use with WPA3-SAE networks.
+         */
+        private @Nullable String mWpa3SaePassphrase;
+        /**
+         * The enterprise configuration details specifying the EAP method,
+         * certificates and other settings associated with the WPA-EAP networks.
+         */
+        private @Nullable WifiEnterpriseConfig mWpa2EnterpriseConfig;
+        /**
+         * The enterprise configuration details specifying the EAP method,
+         * certificates and other settings associated with the SuiteB networks.
+         */
+        private @Nullable WifiEnterpriseConfig mWpa3EnterpriseConfig;
+        /**
+         * This is a network that does not broadcast its SSID, so an
+         * SSID-specific probe request must be used for scans.
+         */
+        private boolean mIsHiddenSSID;
+
+        public Builder() {
+            mSsidPatternMatcher = null;
+            mBssidPatternMatcher = null;
+            mIsEnhancedOpen = false;
+            mWpa2PskPassphrase = null;
+            mWpa3SaePassphrase = null;
+            mWpa2EnterpriseConfig = null;
+            mWpa3EnterpriseConfig = null;
+            mIsHiddenSSID = false;
+        }
+
+        /**
+         * Set the unicode SSID match pattern to use for filtering networks from scan results.
+         * <p>
+         * <li>Overrides any previous value set using {@link #setSsid(String)} or
+         * {@link #setSsidPattern(PatternMatcher)}.</li>
+         *
+         * @param ssidPattern Instance of {@link PatternMatcher} containing the UTF-8 encoded
+         *                    string pattern to use for matching the network's SSID.
+         * @return Instance of {@link Builder} to enable chaining of the builder method.
+         */
+        public @NonNull Builder setSsidPattern(@NonNull PatternMatcher ssidPattern) {
+            checkNotNull(ssidPattern);
+            mSsidPatternMatcher = ssidPattern;
+            return this;
+        }
+
+        /**
+         * Set the unicode SSID for the network.
+         * <p>
+         * <li>Sets the SSID to use for filtering networks from scan results. Will only match
+         * networks whose SSID is identical to the UTF-8 encoding of the specified value.</li>
+         * <li>Overrides any previous value set using {@link #setSsid(String)} or
+         * {@link #setSsidPattern(PatternMatcher)}.</li>
+         *
+         * @param ssid The SSID of the network. It must be valid Unicode.
+         * @return Instance of {@link Builder} to enable chaining of the builder method.
+         * @throws IllegalArgumentException if the SSID is not valid unicode.
+         */
+        public @NonNull Builder setSsid(@NonNull String ssid) {
+            checkNotNull(ssid);
+            final CharsetEncoder unicodeEncoder = StandardCharsets.UTF_8.newEncoder();
+            if (!unicodeEncoder.canEncode(ssid)) {
+                throw new IllegalArgumentException("SSID is not a valid unicode string");
+            }
+            mSsidPatternMatcher = new PatternMatcher(ssid, PatternMatcher.PATTERN_LITERAL);
+            return this;
+        }
+
+        /**
+         * Set the BSSID match pattern to use for filtering networks from scan results.
+         * Will match all networks with BSSID which satisfies the following:
+         * {@code BSSID & mask == baseAddress}.
+         * <p>
+         * <li>Overrides any previous value set using {@link #setBssid(MacAddress)} or
+         * {@link #setBssidPattern(MacAddress, MacAddress)}.</li>
+         *
+         * @param baseAddress Base address for BSSID pattern.
+         * @param mask Mask for BSSID pattern.
+         * @return Instance of {@link Builder} to enable chaining of the builder method.
+         */
+        public @NonNull Builder setBssidPattern(
+                @NonNull MacAddress baseAddress, @NonNull MacAddress mask) {
+            checkNotNull(baseAddress, mask);
+            mBssidPatternMatcher = Pair.create(baseAddress, mask);
+            return this;
+        }
+
+        /**
+         * Set the BSSID to use for filtering networks from scan results. Will only match network
+         * whose BSSID is identical to the specified value.
+         * <p>
+         * <li>Sets the BSSID to use for filtering networks from scan results. Will only match
+         * networks whose BSSID is identical to specified value.</li>
+         * <li>Overrides any previous value set using {@link #setBssid(MacAddress)} or
+         * {@link #setBssidPattern(MacAddress, MacAddress)}.</li>
+         *
+         * @param bssid BSSID of the network.
+         * @return Instance of {@link Builder} to enable chaining of the builder method.
+         */
+        public @NonNull Builder setBssid(@NonNull MacAddress bssid) {
+            checkNotNull(bssid);
+            mBssidPatternMatcher = Pair.create(bssid, MATCH_EXACT_BSSID_PATTERN_MASK);
+            return this;
+        }
+
+        /**
+         * Specifies whether this represents an Enhanced Open (OWE) network.
+         *
+         * @param isEnhancedOpen {@code true} to indicate that the network uses enhanced open,
+         *                       {@code false} otherwise.
+         * @return Instance of {@link Builder} to enable chaining of the builder method.
+         */
+        public @NonNull Builder setIsEnhancedOpen(boolean isEnhancedOpen) {
+            mIsEnhancedOpen = isEnhancedOpen;
+            return this;
+        }
+
+        /**
+         * Set the ASCII WPA2 passphrase for this network. Needed for authenticating to
+         * WPA2-PSK networks.
+         *
+         * @param passphrase passphrase of the network.
+         * @return Instance of {@link Builder} to enable chaining of the builder method.
+         * @throws IllegalArgumentException if the passphrase is not ASCII encodable.
+         */
+        public @NonNull Builder setWpa2Passphrase(@NonNull String passphrase) {
+            checkNotNull(passphrase);
+            final CharsetEncoder asciiEncoder = StandardCharsets.US_ASCII.newEncoder();
+            if (!asciiEncoder.canEncode(passphrase)) {
+                throw new IllegalArgumentException("passphrase not ASCII encodable");
+            }
+            mWpa2PskPassphrase = passphrase;
+            return this;
+        }
+
+        /**
+         * Set the ASCII WPA3 passphrase for this network. Needed for authenticating to WPA3-SAE
+         * networks.
+         *
+         * @param passphrase passphrase of the network.
+         * @return Instance of {@link Builder} to enable chaining of the builder method.
+         * @throws IllegalArgumentException if the passphrase is not ASCII encodable.
+         */
+        public @NonNull Builder setWpa3Passphrase(@NonNull String passphrase) {
+            checkNotNull(passphrase);
+            final CharsetEncoder asciiEncoder = StandardCharsets.US_ASCII.newEncoder();
+            if (!asciiEncoder.canEncode(passphrase)) {
+                throw new IllegalArgumentException("passphrase not ASCII encodable");
+            }
+            mWpa3SaePassphrase = passphrase;
+            return this;
+        }
+
+        /**
+         * Set the associated enterprise configuration for this network. Needed for authenticating
+         * to WPA2-EAP networks. See {@link WifiEnterpriseConfig} for description.
+         *
+         * @param enterpriseConfig Instance of {@link WifiEnterpriseConfig}.
+         * @return Instance of {@link Builder} to enable chaining of the builder method.
+         */
+        public @NonNull Builder setWpa2EnterpriseConfig(
+                @NonNull WifiEnterpriseConfig enterpriseConfig) {
+            checkNotNull(enterpriseConfig);
+            mWpa2EnterpriseConfig = new WifiEnterpriseConfig(enterpriseConfig);
+            return this;
+        }
+
+        /**
+         * Set the associated enterprise configuration for this network. Needed for authenticating
+         * to WPA3-SuiteB networks. See {@link WifiEnterpriseConfig} for description.
+         *
+         * @param enterpriseConfig Instance of {@link WifiEnterpriseConfig}.
+         * @return Instance of {@link Builder} to enable chaining of the builder method.
+         */
+        public @NonNull Builder setWpa3EnterpriseConfig(
+                @NonNull WifiEnterpriseConfig enterpriseConfig) {
+            checkNotNull(enterpriseConfig);
+            mWpa3EnterpriseConfig = new WifiEnterpriseConfig(enterpriseConfig);
+            return this;
+        }
+
+        /**
+         * Specifies whether this represents a hidden network.
+         * <p>
+         * <li>Setting this disallows the usage of {@link #setSsidPattern(PatternMatcher)} since
+         * hidden networks need to be explicitly probed for.</li>
+         * <li>If not set, defaults to false (i.e not a hidden network).</li>
+         *
+         * @param isHiddenSsid {@code true} to indicate that the network is hidden, {@code false}
+         *                     otherwise.
+         * @return Instance of {@link Builder} to enable chaining of the builder method.
+         */
+        public @NonNull Builder setIsHiddenSsid(boolean isHiddenSsid) {
+            mIsHiddenSSID = isHiddenSsid;
+            return this;
+        }
+
+        private void setSecurityParamsInWifiConfiguration(
+                @NonNull WifiConfiguration configuration) {
+            if (!TextUtils.isEmpty(mWpa2PskPassphrase)) { // WPA-PSK network.
+                configuration.setSecurityParams(WifiConfiguration.SECURITY_TYPE_PSK);
+                // WifiConfiguration.preSharedKey needs quotes around ASCII password.
+                configuration.preSharedKey = "\"" + mWpa2PskPassphrase + "\"";
+            } else if (!TextUtils.isEmpty(mWpa3SaePassphrase)) { // WPA3-SAE network.
+                configuration.setSecurityParams(WifiConfiguration.SECURITY_TYPE_SAE);
+                // WifiConfiguration.preSharedKey needs quotes around ASCII password.
+                configuration.preSharedKey = "\"" + mWpa3SaePassphrase + "\"";
+            } else if (mWpa2EnterpriseConfig != null) { // WPA-EAP network
+                configuration.setSecurityParams(WifiConfiguration.SECURITY_TYPE_EAP);
+                configuration.enterpriseConfig = mWpa2EnterpriseConfig;
+            } else if (mWpa3EnterpriseConfig != null) { // WPA3-SuiteB network
+                configuration.setSecurityParams(WifiConfiguration.SECURITY_TYPE_EAP_SUITE_B);
+                configuration.enterpriseConfig = mWpa3EnterpriseConfig;
+            } else if (mIsEnhancedOpen) { // OWE network
+                configuration.setSecurityParams(WifiConfiguration.SECURITY_TYPE_OWE);
+            } else { // Open network
+                configuration.setSecurityParams(WifiConfiguration.SECURITY_TYPE_OPEN);
+            }
+        }
+
+        /**
+         * Helper method to build WifiConfiguration object from the builder.
+         * @return Instance of {@link WifiConfiguration}.
+         */
+        private WifiConfiguration buildWifiConfiguration() {
+            final WifiConfiguration wifiConfiguration = new WifiConfiguration();
+            // WifiConfiguration.SSID needs quotes around unicode SSID.
+            if (mSsidPatternMatcher.getType() == PatternMatcher.PATTERN_LITERAL) {
+                wifiConfiguration.SSID = "\"" + mSsidPatternMatcher.getPath() + "\"";
+            }
+            if (mBssidPatternMatcher.second == MATCH_EXACT_BSSID_PATTERN_MASK) {
+                wifiConfiguration.BSSID = mBssidPatternMatcher.first.toString();
+            }
+            setSecurityParamsInWifiConfiguration(wifiConfiguration);
+            wifiConfiguration.hiddenSSID = mIsHiddenSSID;
+            return wifiConfiguration;
+        }
+
+        private boolean hasSetAnyPattern() {
+            return mSsidPatternMatcher != null || mBssidPatternMatcher != null;
+        }
+
+        private void setMatchAnyPatternIfUnset() {
+            if (mSsidPatternMatcher == null) {
+                mSsidPatternMatcher = new PatternMatcher(MATCH_ALL_SSID_PATTERN_PATH,
+                        PatternMatcher.PATTERN_SIMPLE_GLOB);
+            }
+            if (mBssidPatternMatcher == null) {
+                mBssidPatternMatcher = MATCH_ALL_BSSID_PATTERN;
+            }
+        }
+
+        private boolean hasSetMatchNonePattern() {
+            if (mSsidPatternMatcher.getType() != PatternMatcher.PATTERN_PREFIX
+                    && mSsidPatternMatcher.getPath().equals(MATCH_EMPTY_SSID_PATTERN_PATH)) {
+                return true;
+            }
+            if (mBssidPatternMatcher.equals(MATCH_NO_BSSID_PATTERN1)) {
+                return true;
+            }
+            if (mBssidPatternMatcher.equals(MATCH_NO_BSSID_PATTERN2)) {
+                return true;
+            }
+            return false;
+        }
+
+        private boolean hasSetMatchAllPattern() {
+            if ((mSsidPatternMatcher.match(MATCH_EMPTY_SSID_PATTERN_PATH))
+                    && mBssidPatternMatcher.equals(MATCH_ALL_BSSID_PATTERN)) {
+                return true;
+            }
+            return false;
+        }
+
+        private void validateSecurityParams() {
+            int numSecurityTypes = 0;
+            numSecurityTypes += mIsEnhancedOpen ? 1 : 0;
+            numSecurityTypes += !TextUtils.isEmpty(mWpa2PskPassphrase) ? 1 : 0;
+            numSecurityTypes += !TextUtils.isEmpty(mWpa3SaePassphrase) ? 1 : 0;
+            numSecurityTypes += mWpa2EnterpriseConfig != null ? 1 : 0;
+            numSecurityTypes += mWpa3EnterpriseConfig != null ? 1 : 0;
+            if (numSecurityTypes > 1) {
+                throw new IllegalStateException("only one of setIsEnhancedOpen, setWpa2Passphrase,"
+                        + "setWpa3Passphrase, setWpa2EnterpriseConfig or setWpa3EnterpriseConfig"
+                        + " can be invoked for network specifier");
+            }
+        }
+
+        /**
+         * Create a specifier object used to request a local Wi-Fi network. The generated
+         * {@link NetworkSpecifier} should be used in
+         * {@link NetworkRequest.Builder#setNetworkSpecifier(NetworkSpecifier)} when building
+         * the {@link NetworkRequest}. These specifiers can only be used to request a local wifi
+         * network (i.e no internet capability). So, the device will not switch it's default route
+         * to wifi if there are other transports (cellular for example) available.
+         *<p>
+         * Note: Apps can set a combination of network match params:
+         * <li> SSID Pattern using {@link #setSsidPattern(PatternMatcher)} OR Specific SSID using
+         * {@link #setSsid(String)}. </li>
+         * AND/OR
+         * <li> BSSID Pattern using {@link #setBssidPattern(MacAddress, MacAddress)} OR Specific
+         * BSSID using {@link #setBssid(MacAddress)} </li>
+         * to trigger connection to a network that matches the set params.
+         * The system will find the set of networks matching the request and present the user
+         * with a system dialog which will allow the user to select a specific Wi-Fi network to
+         * connect to or to deny the request.
+         *</p>
+         *
+         * For example:
+         * To connect to an open network with a SSID prefix of "test" and a BSSID OUI of "10:03:23":
+         * {@code
+         * final NetworkSpecifier specifier =
+         *      new Builder()
+         *      .setSsidPattern(new PatternMatcher("test", PatterMatcher.PATTERN_PREFIX))
+         *      .setBssidPattern(MacAddress.fromString("10:03:23:00:00:00"),
+         *                       MacAddress.fromString("ff:ff:ff:00:00:00"))
+         *      .build()
+         * final NetworkRequest request =
+         *      new NetworkRequest.Builder()
+         *      .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
+         *      .removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
+         *      .setNetworkSpecifier(specifier)
+         *      .build();
+         * final ConnectivityManager connectivityManager =
+         *      context.getSystemService(Context.CONNECTIVITY_SERVICE);
+         * final NetworkCallback networkCallback = new NetworkCallback() {
+         *      ...
+         *      {@literal @}Override
+         *      void onAvailable(...) {}
+         *      // etc.
+         * };
+         * connectivityManager.requestNetwork(request, networkCallback);
+         * }
+         *
+         * @return Instance of {@link NetworkSpecifier}.
+         * @throws IllegalStateException on invalid params set.
+         */
+        public @NonNull WifiNetworkSpecifier build() {
+            if (!hasSetAnyPattern()) {
+                throw new IllegalStateException("one of setSsidPattern/setSsid/setBssidPattern/"
+                        + "setBssid should be invoked for specifier");
+            }
+            setMatchAnyPatternIfUnset();
+            if (hasSetMatchNonePattern()) {
+                throw new IllegalStateException("cannot set match-none pattern for specifier");
+            }
+            if (hasSetMatchAllPattern()) {
+                throw new IllegalStateException("cannot set match-all pattern for specifier");
+            }
+            if (mIsHiddenSSID && mSsidPatternMatcher.getType() != PatternMatcher.PATTERN_LITERAL) {
+                throw new IllegalStateException("setSsid should also be invoked when "
+                        + "setIsHiddenSsid is invoked for network specifier");
+            }
+            validateSecurityParams();
+
+            return new WifiNetworkSpecifier(
+                    mSsidPatternMatcher,
+                    mBssidPatternMatcher,
+                    buildWifiConfiguration(),
+                    Process.myUid(),
+                    ActivityThread.currentApplication().getApplicationContext().getOpPackageName());
+        }
+    }
+
+    /**
+     * SSID pattern match specified by the app.
+     * @hide
+     */
+    public final PatternMatcher ssidPatternMatcher;
+
+    /**
+     * BSSID pattern match specified by the app.
+     * Pair of <BaseAddress, Mask>.
+     * @hide
+     */
+    public final Pair<MacAddress, MacAddress> bssidPatternMatcher;
+
+    /**
+     * Security credentials for the network.
+     * <p>
+     * Note: {@link WifiConfiguration#SSID} & {@link WifiConfiguration#BSSID} fields from
+     * WifiConfiguration are not used. Instead we use the {@link #ssidPatternMatcher} &
+     * {@link #bssidPatternMatcher} fields embedded directly
+     * within {@link WifiNetworkSpecifier}.
+     * @hide
+     */
+    public final WifiConfiguration wifiConfiguration;
+
+    /**
+     * The UID of the process initializing this network specifier. Validated by receiver using
+     * checkUidIfNecessary() and is used by satisfiedBy() to determine whether the specifier
+     * matches the offered network.
+     * @hide
+     */
+    public final int requestorUid;
+
+    /**
+     * The package name of the app initializing this network specifier.
+     * @hide
+     */
+    public final String requestorPackageName;
+
+    /** @hide */
+    public WifiNetworkSpecifier() throws IllegalAccessException {
+        throw new IllegalAccessException("Use the builder to create an instance");
+    }
+
+    /** @hide */
+    public WifiNetworkSpecifier(@NonNull PatternMatcher ssidPatternMatcher,
+                                @NonNull Pair<MacAddress, MacAddress> bssidPatternMatcher,
+                                @NonNull WifiConfiguration wifiConfiguration,
+                                int requestorUid, @NonNull String requestorPackageName) {
+        checkNotNull(ssidPatternMatcher);
+        checkNotNull(bssidPatternMatcher);
+        checkNotNull(wifiConfiguration);
+        checkNotNull(requestorPackageName);
+
+        this.ssidPatternMatcher = ssidPatternMatcher;
+        this.bssidPatternMatcher = bssidPatternMatcher;
+        this.wifiConfiguration = wifiConfiguration;
+        this.requestorUid = requestorUid;
+        this.requestorPackageName = requestorPackageName;
+    }
+
+    public static final @NonNull Creator<WifiNetworkSpecifier> CREATOR =
+            new Creator<WifiNetworkSpecifier>() {
+                @Override
+                public WifiNetworkSpecifier createFromParcel(Parcel in) {
+                    PatternMatcher ssidPatternMatcher = in.readParcelable(/* classLoader */null);
+                    MacAddress baseAddress = in.readParcelable(null);
+                    MacAddress mask = in.readParcelable(null);
+                    Pair<MacAddress, MacAddress> bssidPatternMatcher =
+                            Pair.create(baseAddress, mask);
+                    WifiConfiguration wifiConfiguration = in.readParcelable(null);
+                    int requestorUid = in.readInt();
+                    String requestorPackageName = in.readString();
+                    return new WifiNetworkSpecifier(ssidPatternMatcher, bssidPatternMatcher,
+                            wifiConfiguration, requestorUid, requestorPackageName);
+                }
+
+                @Override
+                public WifiNetworkSpecifier[] newArray(int size) {
+                    return new WifiNetworkSpecifier[size];
+                }
+            };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeParcelable(ssidPatternMatcher, flags);
+        dest.writeParcelable(bssidPatternMatcher.first, flags);
+        dest.writeParcelable(bssidPatternMatcher.second, flags);
+        dest.writeParcelable(wifiConfiguration, flags);
+        dest.writeInt(requestorUid);
+        dest.writeString(requestorPackageName);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(
+                ssidPatternMatcher.getPath(),
+                ssidPatternMatcher.getType(),
+                bssidPatternMatcher,
+                wifiConfiguration.allowedKeyManagement,
+                requestorUid, requestorPackageName);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (!(obj instanceof WifiNetworkSpecifier)) {
+            return false;
+        }
+        WifiNetworkSpecifier lhs = (WifiNetworkSpecifier) obj;
+        return Objects.equals(this.ssidPatternMatcher.getPath(),
+                    lhs.ssidPatternMatcher.getPath())
+                && Objects.equals(this.ssidPatternMatcher.getType(),
+                    lhs.ssidPatternMatcher.getType())
+                && Objects.equals(this.bssidPatternMatcher,
+                    lhs.bssidPatternMatcher)
+                && Objects.equals(this.wifiConfiguration.allowedKeyManagement,
+                    lhs.wifiConfiguration.allowedKeyManagement)
+                && requestorUid == lhs.requestorUid
+                && TextUtils.equals(requestorPackageName, lhs.requestorPackageName);
+    }
+
+    @Override
+    public String toString() {
+        return new StringBuilder()
+                .append("WifiNetworkSpecifier [")
+                .append(", SSID Match pattern=").append(ssidPatternMatcher)
+                .append(", BSSID Match pattern=").append(bssidPatternMatcher)
+                .append(", SSID=").append(wifiConfiguration.SSID)
+                .append(", BSSID=").append(wifiConfiguration.BSSID)
+                .append(", requestorUid=").append(requestorUid)
+                .append(", requestorPackageName=").append(requestorPackageName)
+                .append("]")
+                .toString();
+    }
+
+    /** @hide */
+    @Override
+    public boolean satisfiedBy(NetworkSpecifier other) {
+        if (this == other) {
+            return true;
+        }
+        // Any generic requests should be satisifed by a specific wifi network.
+        if (other == null || other instanceof MatchAllNetworkSpecifier) {
+            return true;
+        }
+        if (other instanceof WifiNetworkAgentSpecifier) {
+            return ((WifiNetworkAgentSpecifier) other).satisfiesNetworkSpecifier(this);
+        }
+        // Specific requests are checked for equality although testing for equality of 2 patterns do
+        // not make much sense!
+        return equals(other);
+    }
+
+    /** @hide */
+    @Override
+    public void assertValidFromUid(int requestorUid) {
+        if (this.requestorUid != requestorUid) {
+            throw new SecurityException("mismatched UIDs");
+        }
+    }
+}
diff --git a/android/net/wifi/WifiNetworkSuggestion.java b/android/net/wifi/WifiNetworkSuggestion.java
new file mode 100644
index 0000000..4262017
--- /dev/null
+++ b/android/net/wifi/WifiNetworkSuggestion.java
@@ -0,0 +1,569 @@
+/*
+ * Copyright (C) 2018 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.wifi;
+
+import static com.android.internal.util.Preconditions.checkNotNull;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.ActivityThread;
+import android.net.MacAddress;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.Process;
+import android.text.TextUtils;
+
+import java.nio.charset.CharsetEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * The Network Suggestion object is used to provide a Wi-Fi network for consideration when
+ * auto-connecting to networks. Apps cannot directly create this object, they must use
+ * {@link WifiNetworkSuggestion.Builder#build()} to obtain an instance of this object.
+ *<p>
+ * Apps can provide a list of such networks to the platform using
+ * {@link WifiManager#addNetworkSuggestions(List)}.
+ */
+public final class WifiNetworkSuggestion implements Parcelable {
+
+    /**
+     * Builder used to create {@link WifiNetworkSuggestion} objects.
+     */
+    public static final class Builder {
+        private static final int UNASSIGNED_PRIORITY = -1;
+
+        /**
+         * SSID of the network.
+         */
+        private String mSsid;
+        /**
+         * Optional BSSID within the network.
+         */
+        private MacAddress mBssid;
+        /**
+         * Whether this is an OWE network or not.
+         */
+        private boolean mIsEnhancedOpen;
+        /**
+         * Pre-shared key for use with WPA-PSK networks.
+         */
+        private @Nullable String mWpa2PskPassphrase;
+        /**
+         * Pre-shared key for use with WPA3-SAE networks.
+         */
+        private @Nullable String mWpa3SaePassphrase;
+        /**
+         * The enterprise configuration details specifying the EAP method,
+         * certificates and other settings associated with the WPA-EAP networks.
+         */
+        private @Nullable WifiEnterpriseConfig mWpa2EnterpriseConfig;
+        /**
+         * The enterprise configuration details specifying the EAP method,
+         * certificates and other settings associated with the SuiteB networks.
+         */
+        private @Nullable WifiEnterpriseConfig mWpa3EnterpriseConfig;
+        /**
+         * This is a network that does not broadcast its SSID, so an
+         * SSID-specific probe request must be used for scans.
+         */
+        private boolean mIsHiddenSSID;
+        /**
+         * Whether app needs to log in to captive portal to obtain Internet access.
+         */
+        private boolean mIsAppInteractionRequired;
+        /**
+         * Whether user needs to log in to captive portal to obtain Internet access.
+         */
+        private boolean mIsUserInteractionRequired;
+        /**
+         * Whether this network is metered or not.
+         */
+        private boolean mIsMetered;
+        /**
+         * Priority of this network among other network suggestions provided by the app.
+         * The lower the number, the higher the priority (i.e value of 0 = highest priority).
+         */
+        private int mPriority;
+
+        public Builder() {
+            mSsid = null;
+            mBssid =  null;
+            mIsEnhancedOpen = false;
+            mWpa2PskPassphrase = null;
+            mWpa3SaePassphrase = null;
+            mWpa2EnterpriseConfig = null;
+            mWpa3EnterpriseConfig = null;
+            mIsHiddenSSID = false;
+            mIsAppInteractionRequired = false;
+            mIsUserInteractionRequired = false;
+            mIsMetered = false;
+            mPriority = UNASSIGNED_PRIORITY;
+        }
+
+        /**
+         * Set the unicode SSID for the network.
+         * <p>
+         * <li>Overrides any previous value set using {@link #setSsid(String)}.</li>
+         *
+         * @param ssid The SSID of the network. It must be valid Unicode.
+         * @return Instance of {@link Builder} to enable chaining of the builder method.
+         * @throws IllegalArgumentException if the SSID is not valid unicode.
+         */
+        public @NonNull Builder setSsid(@NonNull String ssid) {
+            checkNotNull(ssid);
+            final CharsetEncoder unicodeEncoder = StandardCharsets.UTF_8.newEncoder();
+            if (!unicodeEncoder.canEncode(ssid)) {
+                throw new IllegalArgumentException("SSID is not a valid unicode string");
+            }
+            mSsid = new String(ssid);
+            return this;
+        }
+
+        /**
+         * Set the BSSID to use for filtering networks from scan results. Will only match network
+         * whose BSSID is identical to the specified value.
+         * <p>
+         * <li Sets a specific BSSID for the network suggestion. If set, only the specified BSSID
+         * with the specified SSID will be considered for connection.
+         * <li>If set, only the specified BSSID with the specified SSID will be considered for
+         * connection.</li>
+         * <li>If not set, all BSSIDs with the specified SSID will be considered for connection.
+         * </li>
+         * <li>Overrides any previous value set using {@link #setBssid(MacAddress)}.</li>
+         *
+         * @param bssid BSSID of the network.
+         * @return Instance of {@link Builder} to enable chaining of the builder method.
+         */
+        public @NonNull Builder setBssid(@NonNull MacAddress bssid) {
+            checkNotNull(bssid);
+            mBssid = MacAddress.fromBytes(bssid.toByteArray());
+            return this;
+        }
+
+        /**
+         * Specifies whether this represents an Enhanced Open (OWE) network.
+         *
+         * @param isEnhancedOpen {@code true} to indicate that the network used enhanced open,
+         *                       {@code false} otherwise.
+         * @return Instance of {@link Builder} to enable chaining of the builder method.
+         */
+        public @NonNull Builder setIsEnhancedOpen(boolean isEnhancedOpen) {
+            mIsEnhancedOpen = isEnhancedOpen;
+            return this;
+        }
+
+        /**
+         * Set the ASCII WPA2 passphrase for this network. Needed for authenticating to
+         * WPA2-PSK networks.
+         *
+         * @param passphrase passphrase of the network.
+         * @return Instance of {@link Builder} to enable chaining of the builder method.
+         * @throws IllegalArgumentException if the passphrase is not ASCII encodable.
+         */
+        public @NonNull Builder setWpa2Passphrase(@NonNull String passphrase) {
+            checkNotNull(passphrase);
+            final CharsetEncoder asciiEncoder = StandardCharsets.US_ASCII.newEncoder();
+            if (!asciiEncoder.canEncode(passphrase)) {
+                throw new IllegalArgumentException("passphrase not ASCII encodable");
+            }
+            mWpa2PskPassphrase = passphrase;
+            return this;
+        }
+
+        /**
+         * Set the ASCII WPA3 passphrase for this network. Needed for authenticating to WPA3-SAE
+         * networks.
+         *
+         * @param passphrase passphrase of the network.
+         * @return Instance of {@link Builder} to enable chaining of the builder method.
+         * @throws IllegalArgumentException if the passphrase is not ASCII encodable.
+         */
+        public @NonNull Builder setWpa3Passphrase(@NonNull String passphrase) {
+            checkNotNull(passphrase);
+            final CharsetEncoder asciiEncoder = StandardCharsets.US_ASCII.newEncoder();
+            if (!asciiEncoder.canEncode(passphrase)) {
+                throw new IllegalArgumentException("passphrase not ASCII encodable");
+            }
+            mWpa3SaePassphrase = passphrase;
+            return this;
+        }
+
+        /**
+         * Set the associated enterprise configuration for this network. Needed for authenticating
+         * to WPA2-EAP networks. See {@link WifiEnterpriseConfig} for description.
+         *
+         * @param enterpriseConfig Instance of {@link WifiEnterpriseConfig}.
+         * @return Instance of {@link Builder} to enable chaining of the builder method.
+         */
+        public @NonNull Builder setWpa2EnterpriseConfig(
+                @NonNull WifiEnterpriseConfig enterpriseConfig) {
+            checkNotNull(enterpriseConfig);
+            mWpa2EnterpriseConfig = new WifiEnterpriseConfig(enterpriseConfig);
+            return this;
+        }
+
+        /**
+         * Set the associated enterprise configuration for this network. Needed for authenticating
+         * to WPA3-SuiteB networks. See {@link WifiEnterpriseConfig} for description.
+         *
+         * @param enterpriseConfig Instance of {@link WifiEnterpriseConfig}.
+         * @return Instance of {@link Builder} to enable chaining of the builder method.
+         */
+        public @NonNull Builder setWpa3EnterpriseConfig(
+                @NonNull WifiEnterpriseConfig enterpriseConfig) {
+            checkNotNull(enterpriseConfig);
+            mWpa3EnterpriseConfig = new WifiEnterpriseConfig(enterpriseConfig);
+            return this;
+        }
+
+        /**
+         * Specifies whether this represents a hidden network.
+         * <p>
+         * <li>If not set, defaults to false (i.e not a hidden network).</li>
+         *
+         * @param isHiddenSsid {@code true} to indicate that the network is hidden, {@code false}
+         *                     otherwise.
+         * @return Instance of {@link Builder} to enable chaining of the builder method.
+         */
+        public @NonNull Builder setIsHiddenSsid(boolean isHiddenSsid) {
+            mIsHiddenSSID = isHiddenSsid;
+            return this;
+        }
+
+        /**
+         * Specifies whether the app needs to log in to a captive portal to obtain Internet access.
+         * <p>
+         * This will dictate if the directed broadcast
+         * {@link WifiManager#ACTION_WIFI_NETWORK_SUGGESTION_POST_CONNECTION} will be sent to the
+         * app after successfully connecting to the network.
+         * Use this for captive portal type networks where the app needs to authenticate the user
+         * before the device can access the network.
+         * <p>
+         * <li>If not set, defaults to false (i.e no app interaction required).</li>
+         *
+         * @param isAppInteractionRequired {@code true} to indicate that app interaction is
+         *                                 required, {@code false} otherwise.
+         * @return Instance of {@link Builder} to enable chaining of the builder method.
+         */
+        public @NonNull Builder setIsAppInteractionRequired(boolean isAppInteractionRequired) {
+            mIsAppInteractionRequired = isAppInteractionRequired;
+            return this;
+        }
+
+        /**
+         * Specifies whether the user needs to log in to a captive portal to obtain Internet access.
+         * <p>
+         * <li>If not set, defaults to false (i.e no user interaction required).</li>
+         *
+         * @param isUserInteractionRequired {@code true} to indicate that user interaction is
+         *                                  required, {@code false} otherwise.
+         * @return Instance of {@link Builder} to enable chaining of the builder method.
+         */
+        public @NonNull Builder setIsUserInteractionRequired(boolean isUserInteractionRequired) {
+            mIsUserInteractionRequired = isUserInteractionRequired;
+            return this;
+        }
+
+        /**
+         * Specify the priority of this network among other network suggestions provided by the same
+         * app (priorities have no impact on suggestions by different apps). The higher the number,
+         * the higher the priority (i.e value of 0 = lowest priority).
+         * <p>
+         * <li>If not set, defaults a lower priority than any assigned priority.</li>
+         *
+         * @param priority Integer number representing the priority among suggestions by the app.
+         * @return Instance of {@link Builder} to enable chaining of the builder method.
+         * @throws IllegalArgumentException if the priority value is negative.
+         */
+        public @NonNull Builder setPriority(@IntRange(from = 0) int priority) {
+            if (priority < 0) {
+                throw new IllegalArgumentException("Invalid priority value " + priority);
+            }
+            mPriority = priority;
+            return this;
+        }
+
+        /**
+         * Specifies whether this network is metered.
+         * <p>
+         * <li>If not set, defaults to false (i.e not metered).</li>
+         *
+         * @param isMetered {@code true} to indicate that the network is metered, {@code false}
+         *                  otherwise.
+         * @return Instance of {@link Builder} to enable chaining of the builder method.
+         */
+        public @NonNull Builder setIsMetered(boolean isMetered) {
+            mIsMetered = isMetered;
+            return this;
+        }
+
+        private void setSecurityParamsInWifiConfiguration(
+                @NonNull WifiConfiguration configuration) {
+            if (!TextUtils.isEmpty(mWpa2PskPassphrase)) { // WPA-PSK network.
+                configuration.setSecurityParams(WifiConfiguration.SECURITY_TYPE_PSK);
+                // WifiConfiguration.preSharedKey needs quotes around ASCII password.
+                configuration.preSharedKey = "\"" + mWpa2PskPassphrase + "\"";
+            } else if (!TextUtils.isEmpty(mWpa3SaePassphrase)) { // WPA3-SAE network.
+                configuration.setSecurityParams(WifiConfiguration.SECURITY_TYPE_SAE);
+                // WifiConfiguration.preSharedKey needs quotes around ASCII password.
+                configuration.preSharedKey = "\"" + mWpa3SaePassphrase + "\"";
+            } else if (mWpa2EnterpriseConfig != null) { // WPA-EAP network
+                configuration.setSecurityParams(WifiConfiguration.SECURITY_TYPE_EAP);
+                configuration.enterpriseConfig = mWpa2EnterpriseConfig;
+            } else if (mWpa3EnterpriseConfig != null) { // WPA3-SuiteB network
+                configuration.setSecurityParams(WifiConfiguration.SECURITY_TYPE_EAP_SUITE_B);
+                configuration.enterpriseConfig = mWpa3EnterpriseConfig;
+            } else if (mIsEnhancedOpen) { // OWE network
+                configuration.setSecurityParams(WifiConfiguration.SECURITY_TYPE_OWE);
+            } else { // Open network
+                configuration.setSecurityParams(WifiConfiguration.SECURITY_TYPE_OPEN);
+            }
+        }
+
+        /**
+         * Helper method to build WifiConfiguration object from the builder.
+         * @return Instance of {@link WifiConfiguration}.
+         */
+        private WifiConfiguration buildWifiConfiguration() {
+            final WifiConfiguration wifiConfiguration = new WifiConfiguration();
+            // WifiConfiguration.SSID needs quotes around unicode SSID.
+            wifiConfiguration.SSID = "\"" + mSsid + "\"";
+            if (mBssid != null) {
+                wifiConfiguration.BSSID = mBssid.toString();
+            }
+
+            setSecurityParamsInWifiConfiguration(wifiConfiguration);
+
+            wifiConfiguration.hiddenSSID = mIsHiddenSSID;
+            wifiConfiguration.priority = mPriority;
+            wifiConfiguration.meteredOverride =
+                    mIsMetered ? WifiConfiguration.METERED_OVERRIDE_METERED
+                            : WifiConfiguration.METERED_OVERRIDE_NONE;
+            return wifiConfiguration;
+        }
+
+        private void validateSecurityParams() {
+            int numSecurityTypes = 0;
+            numSecurityTypes += mIsEnhancedOpen ? 1 : 0;
+            numSecurityTypes += !TextUtils.isEmpty(mWpa2PskPassphrase) ? 1 : 0;
+            numSecurityTypes += !TextUtils.isEmpty(mWpa3SaePassphrase) ? 1 : 0;
+            numSecurityTypes += mWpa2EnterpriseConfig != null ? 1 : 0;
+            numSecurityTypes += mWpa3EnterpriseConfig != null ? 1 : 0;
+            if (numSecurityTypes > 1) {
+                throw new IllegalStateException("only one of setIsEnhancedOpen, setWpa2Passphrase,"
+                        + "setWpa3Passphrase, setWpa2EnterpriseConfig or setWpa3EnterpriseConfig"
+                        + " can be invoked for network specifier");
+            }
+        }
+
+        /**
+         * Create a network suggestion object for use in
+         * {@link WifiManager#addNetworkSuggestions(List)}.
+         *
+         *<p class="note">
+         * <b>Note:</b> Apps can set a combination of SSID using {@link #setSsid(String)} and BSSID
+         * using {@link #setBssid(MacAddress)} to provide more fine grained network suggestions to
+         * the platform.
+         * </p>
+         *
+         * For example:
+         * To provide credentials for one open, one WPA2 and one WPA3 network with their
+         * corresponding SSID's:
+         *
+         * <pre>{@code
+         * final WifiNetworkSuggestion suggestion1 =
+         *      new Builder()
+         *      .setSsid("test111111")
+         *      .build()
+         * final WifiNetworkSuggestion suggestion2 =
+         *      new Builder()
+         *      .setSsid("test222222")
+         *      .setWpa2Passphrase("test123456")
+         *      .build()
+         * final WifiNetworkSuggestion suggestion3 =
+         *      new Builder()
+         *      .setSsid("test333333")
+         *      .setWpa3Passphrase("test6789")
+         *      .build()
+         * final List<WifiNetworkSuggestion> suggestionsList =
+         *      new ArrayList<WifiNetworkSuggestion> { {
+         *          add(suggestion1);
+         *          add(suggestion2);
+         *          add(suggestion3);
+         *      } };
+         * final WifiManager wifiManager =
+         *      context.getSystemService(Context.WIFI_SERVICE);
+         * wifiManager.addNetworkSuggestions(suggestionsList);
+         * // ...
+         * }</pre>
+         *
+         * @return Instance of {@link WifiNetworkSuggestion}
+         * @throws IllegalStateException on invalid params set
+         * @see WifiNetworkSuggestion
+         */
+        public @NonNull WifiNetworkSuggestion build() {
+            if (mSsid == null) {
+                throw new IllegalStateException("setSsid should be invoked for suggestion");
+            }
+            if (TextUtils.isEmpty(mSsid)) {
+                throw new IllegalStateException("invalid ssid for suggestion");
+            }
+            if (mBssid != null
+                    && (mBssid.equals(MacAddress.BROADCAST_ADDRESS)
+                    || mBssid.equals(MacAddress.ALL_ZEROS_ADDRESS))) {
+                throw new IllegalStateException("invalid bssid for suggestion");
+            }
+            validateSecurityParams();
+
+            return new WifiNetworkSuggestion(
+                    buildWifiConfiguration(),
+                    mIsAppInteractionRequired,
+                    mIsUserInteractionRequired,
+                    Process.myUid(),
+                    ActivityThread.currentApplication().getApplicationContext().getOpPackageName());
+        }
+    }
+
+    /**
+     * Network configuration for the provided network.
+     * @hide
+     */
+    public final WifiConfiguration wifiConfiguration;
+
+    /**
+     * Whether app needs to log in to captive portal to obtain Internet access.
+     * @hide
+     */
+    public final boolean isAppInteractionRequired;
+
+    /**
+     * Whether user needs to log in to captive portal to obtain Internet access.
+     * @hide
+     */
+    public final boolean isUserInteractionRequired;
+
+    /**
+     * The UID of the process initializing this network suggestion.
+     * @hide
+     */
+    public final int suggestorUid;
+
+    /**
+     * The package name of the process initializing this network suggestion.
+     * @hide
+     */
+    public final String suggestorPackageName;
+
+    /** @hide */
+    public WifiNetworkSuggestion() {
+        this.wifiConfiguration = null;
+        this.isAppInteractionRequired = false;
+        this.isUserInteractionRequired = false;
+        this.suggestorUid = -1;
+        this.suggestorPackageName = null;
+    }
+
+    /** @hide */
+    public WifiNetworkSuggestion(@NonNull WifiConfiguration wifiConfiguration,
+                                 boolean isAppInteractionRequired,
+                                 boolean isUserInteractionRequired,
+                                 int suggestorUid, @NonNull String suggestorPackageName) {
+        checkNotNull(wifiConfiguration);
+        checkNotNull(suggestorPackageName);
+
+        this.wifiConfiguration = wifiConfiguration;
+        this.isAppInteractionRequired = isAppInteractionRequired;
+        this.isUserInteractionRequired = isUserInteractionRequired;
+        this.suggestorUid = suggestorUid;
+        this.suggestorPackageName = suggestorPackageName;
+    }
+
+    public static final @NonNull Creator<WifiNetworkSuggestion> CREATOR =
+            new Creator<WifiNetworkSuggestion>() {
+                @Override
+                public WifiNetworkSuggestion createFromParcel(Parcel in) {
+                    return new WifiNetworkSuggestion(
+                            in.readParcelable(null), // wifiConfiguration
+                            in.readBoolean(), // isAppInteractionRequired
+                            in.readBoolean(), // isUserInteractionRequired
+                            in.readInt(), // suggestorUid
+                            in.readString() // suggestorPackageName
+                    );
+                }
+
+                @Override
+                public WifiNetworkSuggestion[] newArray(int size) {
+                    return new WifiNetworkSuggestion[size];
+                }
+            };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeParcelable(wifiConfiguration, flags);
+        dest.writeBoolean(isAppInteractionRequired);
+        dest.writeBoolean(isUserInteractionRequired);
+        dest.writeInt(suggestorUid);
+        dest.writeString(suggestorPackageName);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(wifiConfiguration.SSID, wifiConfiguration.BSSID,
+                wifiConfiguration.allowedKeyManagement, suggestorUid, suggestorPackageName);
+    }
+
+    /**
+     * Equals for network suggestions.
+     */
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (!(obj instanceof WifiNetworkSuggestion)) {
+            return false;
+        }
+        WifiNetworkSuggestion lhs = (WifiNetworkSuggestion) obj;
+        return Objects.equals(this.wifiConfiguration.SSID, lhs.wifiConfiguration.SSID)
+                && Objects.equals(this.wifiConfiguration.BSSID, lhs.wifiConfiguration.BSSID)
+                && Objects.equals(this.wifiConfiguration.allowedKeyManagement,
+                                  lhs.wifiConfiguration.allowedKeyManagement)
+                && suggestorUid == lhs.suggestorUid
+                && TextUtils.equals(suggestorPackageName, lhs.suggestorPackageName);
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder("WifiNetworkSuggestion [")
+                .append(", SSID=").append(wifiConfiguration.SSID)
+                .append(", BSSID=").append(wifiConfiguration.BSSID)
+                .append(", isAppInteractionRequired=").append(isAppInteractionRequired)
+                .append(", isUserInteractionRequired=").append(isUserInteractionRequired)
+                .append(", suggestorUid=").append(suggestorUid)
+                .append(", suggestorPackageName=").append(suggestorPackageName)
+                .append("]");
+        return sb.toString();
+    }
+}
diff --git a/android/net/wifi/WifiScanner.java b/android/net/wifi/WifiScanner.java
new file mode 100644
index 0000000..66dc992
--- /dev/null
+++ b/android/net/wifi/WifiScanner.java
@@ -0,0 +1,1450 @@
+/*
+ * Copyright (C) 2008 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.wifi;
+
+import android.Manifest;
+import android.annotation.RequiresPermission;
+import android.annotation.SuppressLint;
+import android.annotation.SystemApi;
+import android.annotation.SystemService;
+import android.content.Context;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.RemoteException;
+import android.os.WorkSource;
+import android.util.Log;
+import android.util.SparseArray;
+
+import com.android.internal.util.AsyncChannel;
+import com.android.internal.util.Preconditions;
+import com.android.internal.util.Protocol;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * This class provides a way to scan the Wifi universe around the device
+ * @hide
+ */
+@SystemApi
+@SystemService(Context.WIFI_SCANNING_SERVICE)
+public class WifiScanner {
+
+    /** no band specified; use channel list instead */
+    public static final int WIFI_BAND_UNSPECIFIED = 0;      /* not specified */
+
+    /** 2.4 GHz band */
+    public static final int WIFI_BAND_24_GHZ = 1;           /* 2.4 GHz band */
+    /** 5 GHz band excluding DFS channels */
+    public static final int WIFI_BAND_5_GHZ = 2;            /* 5 GHz band without DFS channels */
+    /** DFS channels from 5 GHz band only */
+    public static final int WIFI_BAND_5_GHZ_DFS_ONLY  = 4;  /* 5 GHz band with DFS channels */
+    /** 5 GHz band including DFS channels */
+    public static final int WIFI_BAND_5_GHZ_WITH_DFS  = 6;  /* 5 GHz band with DFS channels */
+    /** Both 2.4 GHz band and 5 GHz band; no DFS channels */
+    public static final int WIFI_BAND_BOTH = 3;             /* both bands without DFS channels */
+    /** Both 2.4 GHz band and 5 GHz band; with DFS channels */
+    public static final int WIFI_BAND_BOTH_WITH_DFS = 7;    /* both bands with DFS channels */
+
+    /** Minimum supported scanning period */
+    public static final int MIN_SCAN_PERIOD_MS = 1000;      /* minimum supported period */
+    /** Maximum supported scanning period */
+    public static final int MAX_SCAN_PERIOD_MS = 1024000;   /* maximum supported period */
+
+    /** No Error */
+    public static final int REASON_SUCCEEDED = 0;
+    /** Unknown error */
+    public static final int REASON_UNSPECIFIED = -1;
+    /** Invalid listener */
+    public static final int REASON_INVALID_LISTENER = -2;
+    /** Invalid request */
+    public static final int REASON_INVALID_REQUEST = -3;
+    /** Invalid request */
+    public static final int REASON_NOT_AUTHORIZED = -4;
+    /** An outstanding request with the same listener hasn't finished yet. */
+    public static final int REASON_DUPLICATE_REQEUST = -5;
+
+    /** @hide */
+    public static final String GET_AVAILABLE_CHANNELS_EXTRA = "Channels";
+
+    /**
+     * Generic action callback invocation interface
+     *  @hide
+     */
+    @SystemApi
+    public static interface ActionListener {
+        public void onSuccess();
+        public void onFailure(int reason, String description);
+    }
+
+    /**
+     * gives you all the possible channels; channel is specified as an
+     * integer with frequency in MHz i.e. channel 1 is 2412
+     * @hide
+     */
+    public List<Integer> getAvailableChannels(int band) {
+        try {
+            Bundle bundle =  mService.getAvailableChannels(band);
+            return bundle.getIntegerArrayList(GET_AVAILABLE_CHANNELS_EXTRA);
+        } catch (RemoteException e) {
+            return null;
+        }
+    }
+
+    /**
+     * provides channel specification for scanning
+     */
+    public static class ChannelSpec {
+        /**
+         * channel frequency in MHz; for example channel 1 is specified as 2412
+         */
+        public int frequency;
+        /**
+         * if true, scan this channel in passive fashion.
+         * This flag is ignored on DFS channel specification.
+         * @hide
+         */
+        public boolean passive;                                    /* ignored on DFS channels */
+        /**
+         * how long to dwell on this channel
+         * @hide
+         */
+        public int dwellTimeMS;                                    /* not supported for now */
+
+        /**
+         * default constructor for channel spec
+         */
+        public ChannelSpec(int frequency) {
+            this.frequency = frequency;
+            passive = false;
+            dwellTimeMS = 0;
+        }
+    }
+
+    /**
+     * reports {@link ScanListener#onResults} when underlying buffers are full
+     * this is simply the lack of the {@link #REPORT_EVENT_AFTER_EACH_SCAN} flag
+     * @deprecated It is not supported anymore.
+     */
+    @Deprecated
+    public static final int REPORT_EVENT_AFTER_BUFFER_FULL = 0;
+    /**
+     * reports {@link ScanListener#onResults} after each scan
+     */
+    public static final int REPORT_EVENT_AFTER_EACH_SCAN = (1 << 0);
+    /**
+     * reports {@link ScanListener#onFullResult} whenever each beacon is discovered
+     */
+    public static final int REPORT_EVENT_FULL_SCAN_RESULT = (1 << 1);
+    /**
+     * Do not place scans in the chip's scan history buffer
+     */
+    public static final int REPORT_EVENT_NO_BATCH = (1 << 2);
+
+    /**
+     * This is used to indicate the purpose of the scan to the wifi chip in
+     * {@link ScanSettings#type}.
+     * On devices with multiple hardware radio chains (and hence different modes of scan),
+     * this type serves as an indication to the hardware on what mode of scan to perform.
+     * Only apps holding android.Manifest.permission.NETWORK_STACK permission can set this value.
+     *
+     * Note: This serves as an intent and not as a stipulation, the wifi chip
+     * might honor or ignore the indication based on the current radio conditions. Always
+     * use the {@link ScanResult#radioChainInfos} to figure out the radio chain configuration used
+     * to receive the corresponding scan result.
+     */
+    /** {@hide} */
+    public static final int TYPE_LOW_LATENCY = 0;
+    /** {@hide} */
+    public static final int TYPE_LOW_POWER = 1;
+    /** {@hide} */
+    public static final int TYPE_HIGH_ACCURACY = 2;
+
+    /** {@hide} */
+    public static final String SCAN_PARAMS_SCAN_SETTINGS_KEY = "ScanSettings";
+    /** {@hide} */
+    public static final String SCAN_PARAMS_WORK_SOURCE_KEY = "WorkSource";
+    /** {@hide} */
+    public static final String REQUEST_PACKAGE_NAME_KEY = "PackageName";
+
+    /**
+     * scan configuration parameters to be sent to {@link #startBackgroundScan}
+     */
+    public static class ScanSettings implements Parcelable {
+        /**
+         * Hidden network to be scanned for.
+         * {@hide}
+         */
+        public static class HiddenNetwork {
+            /** SSID of the network */
+            public String ssid;
+
+            /**
+             * Default constructor for HiddenNetwork.
+             */
+            public HiddenNetwork(String ssid) {
+                this.ssid = ssid;
+            }
+        }
+
+        /** one of the WIFI_BAND values */
+        public int band;
+        /** list of channels; used when band is set to WIFI_BAND_UNSPECIFIED */
+        public ChannelSpec[] channels;
+        /**
+         * list of hidden networks to scan for. Explicit probe requests are sent out for such
+         * networks during scan. Only valid for single scan requests.
+         * {@hide}
+         */
+        @RequiresPermission(android.Manifest.permission.NETWORK_STACK)
+        public HiddenNetwork[] hiddenNetworks;
+        /** period of background scan; in millisecond, 0 => single shot scan */
+        public int periodInMs;
+        /** must have a valid REPORT_EVENT value */
+        public int reportEvents;
+        /** defines number of bssids to cache from each scan */
+        public int numBssidsPerScan;
+        /**
+         * defines number of scans to cache; use it with REPORT_EVENT_AFTER_BUFFER_FULL
+         * to wake up at fixed interval
+         */
+        public int maxScansToCache;
+        /**
+         * if maxPeriodInMs is non zero or different than period, then this bucket is
+         * a truncated binary exponential backoff bucket and the scan period will grow
+         * exponentially as per formula: actual_period(N) = period * (2 ^ (N/stepCount))
+         * to maxPeriodInMs
+         */
+        public int maxPeriodInMs;
+        /**
+         * for truncated binary exponential back off bucket, number of scans to perform
+         * for a given period
+         */
+        public int stepCount;
+        /**
+         * Flag to indicate if the scan settings are targeted for PNO scan.
+         * {@hide}
+         */
+        public boolean isPnoScan;
+        /**
+         * Indicate the type of scan to be performed by the wifi chip.
+         * Default value: {@link #TYPE_LOW_LATENCY}.
+         * {@hide}
+         */
+        @RequiresPermission(android.Manifest.permission.NETWORK_STACK)
+        public int type = TYPE_LOW_LATENCY;
+        /**
+         * This scan request may ignore location settings while receiving scans. This should only
+         * be used in emergency situations.
+         * {@hide}
+         */
+        @SystemApi
+        public boolean ignoreLocationSettings;
+        /**
+         * This scan request will be hidden from app-ops noting for location information. This
+         * should only be used by FLP/NLP module on the device which is using the scan results to
+         * compute results for behalf on their clients. FLP/NLP module using this flag should ensure
+         * that they note in app-ops the eventual delivery of location information computed using
+         * these results to their client .
+         * {@hide}
+         */
+        @SystemApi
+        public boolean hideFromAppOps;
+
+        /** Implement the Parcelable interface {@hide} */
+        public int describeContents() {
+            return 0;
+        }
+
+        /** Implement the Parcelable interface {@hide} */
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeInt(band);
+            dest.writeInt(periodInMs);
+            dest.writeInt(reportEvents);
+            dest.writeInt(numBssidsPerScan);
+            dest.writeInt(maxScansToCache);
+            dest.writeInt(maxPeriodInMs);
+            dest.writeInt(stepCount);
+            dest.writeInt(isPnoScan ? 1 : 0);
+            dest.writeInt(type);
+            dest.writeInt(ignoreLocationSettings ? 1 : 0);
+            dest.writeInt(hideFromAppOps ? 1 : 0);
+            if (channels != null) {
+                dest.writeInt(channels.length);
+                for (int i = 0; i < channels.length; i++) {
+                    dest.writeInt(channels[i].frequency);
+                    dest.writeInt(channels[i].dwellTimeMS);
+                    dest.writeInt(channels[i].passive ? 1 : 0);
+                }
+            } else {
+                dest.writeInt(0);
+            }
+            if (hiddenNetworks != null) {
+                dest.writeInt(hiddenNetworks.length);
+                for (int i = 0; i < hiddenNetworks.length; i++) {
+                    dest.writeString(hiddenNetworks[i].ssid);
+                }
+            } else {
+                dest.writeInt(0);
+            }
+        }
+
+        /** Implement the Parcelable interface {@hide} */
+        public static final @android.annotation.NonNull Creator<ScanSettings> CREATOR =
+                new Creator<ScanSettings>() {
+                    public ScanSettings createFromParcel(Parcel in) {
+                        ScanSettings settings = new ScanSettings();
+                        settings.band = in.readInt();
+                        settings.periodInMs = in.readInt();
+                        settings.reportEvents = in.readInt();
+                        settings.numBssidsPerScan = in.readInt();
+                        settings.maxScansToCache = in.readInt();
+                        settings.maxPeriodInMs = in.readInt();
+                        settings.stepCount = in.readInt();
+                        settings.isPnoScan = in.readInt() == 1;
+                        settings.type = in.readInt();
+                        settings.ignoreLocationSettings = in.readInt() == 1;
+                        settings.hideFromAppOps = in.readInt() == 1;
+                        int num_channels = in.readInt();
+                        settings.channels = new ChannelSpec[num_channels];
+                        for (int i = 0; i < num_channels; i++) {
+                            int frequency = in.readInt();
+                            ChannelSpec spec = new ChannelSpec(frequency);
+                            spec.dwellTimeMS = in.readInt();
+                            spec.passive = in.readInt() == 1;
+                            settings.channels[i] = spec;
+                        }
+                        int numNetworks = in.readInt();
+                        settings.hiddenNetworks = new HiddenNetwork[numNetworks];
+                        for (int i = 0; i < numNetworks; i++) {
+                            String ssid = in.readString();
+                            settings.hiddenNetworks[i] = new HiddenNetwork(ssid);;
+                        }
+                        return settings;
+                    }
+
+                    public ScanSettings[] newArray(int size) {
+                        return new ScanSettings[size];
+                    }
+                };
+
+    }
+
+    /**
+     * all the information garnered from a single scan
+     */
+    public static class ScanData implements Parcelable {
+        /** scan identifier */
+        private int mId;
+        /** additional information about scan
+         * 0 => no special issues encountered in the scan
+         * non-zero => scan was truncated, so results may not be complete
+         */
+        private int mFlags;
+        /**
+         * Indicates the buckets that were scanned to generate these results.
+         * This is not relevant to WifiScanner API users and is used internally.
+         * {@hide}
+         */
+        private int mBucketsScanned;
+        /**
+         * Bands scanned. One of the WIFI_BAND values.
+         * Will be {@link #WIFI_BAND_UNSPECIFIED} if the list of channels do not fully cover
+         * any of the bands.
+         * {@hide}
+         */
+        private int mBandScanned;
+        /** all scan results discovered in this scan, sorted by timestamp in ascending order */
+        private ScanResult mResults[];
+
+        ScanData() {}
+
+        public ScanData(int id, int flags, ScanResult[] results) {
+            mId = id;
+            mFlags = flags;
+            mResults = results;
+        }
+
+        /** {@hide} */
+        public ScanData(int id, int flags, int bucketsScanned, int bandScanned,
+                        ScanResult[] results) {
+            mId = id;
+            mFlags = flags;
+            mBucketsScanned = bucketsScanned;
+            mBandScanned = bandScanned;
+            mResults = results;
+        }
+
+        public ScanData(ScanData s) {
+            mId = s.mId;
+            mFlags = s.mFlags;
+            mBucketsScanned = s.mBucketsScanned;
+            mBandScanned = s.mBandScanned;
+            mResults = new ScanResult[s.mResults.length];
+            for (int i = 0; i < s.mResults.length; i++) {
+                ScanResult result = s.mResults[i];
+                ScanResult newResult = new ScanResult(result);
+                mResults[i] = newResult;
+            }
+        }
+
+        public int getId() {
+            return mId;
+        }
+
+        public int getFlags() {
+            return mFlags;
+        }
+
+        /** {@hide} */
+        public int getBucketsScanned() {
+            return mBucketsScanned;
+        }
+
+        /** {@hide} */
+        public int getBandScanned() {
+            return mBandScanned;
+        }
+
+        public ScanResult[] getResults() {
+            return mResults;
+        }
+
+        /** Implement the Parcelable interface {@hide} */
+        public int describeContents() {
+            return 0;
+        }
+
+        /** Implement the Parcelable interface {@hide} */
+        public void writeToParcel(Parcel dest, int flags) {
+            if (mResults != null) {
+                dest.writeInt(mId);
+                dest.writeInt(mFlags);
+                dest.writeInt(mBucketsScanned);
+                dest.writeInt(mBandScanned);
+                dest.writeInt(mResults.length);
+                for (int i = 0; i < mResults.length; i++) {
+                    ScanResult result = mResults[i];
+                    result.writeToParcel(dest, flags);
+                }
+            } else {
+                dest.writeInt(0);
+            }
+        }
+
+        /** Implement the Parcelable interface {@hide} */
+        public static final @android.annotation.NonNull Creator<ScanData> CREATOR =
+                new Creator<ScanData>() {
+                    public ScanData createFromParcel(Parcel in) {
+                        int id = in.readInt();
+                        int flags = in.readInt();
+                        int bucketsScanned = in.readInt();
+                        int bandScanned = in.readInt();
+                        int n = in.readInt();
+                        ScanResult results[] = new ScanResult[n];
+                        for (int i = 0; i < n; i++) {
+                            results[i] = ScanResult.CREATOR.createFromParcel(in);
+                        }
+                        return new ScanData(id, flags, bucketsScanned, bandScanned, results);
+                    }
+
+                    public ScanData[] newArray(int size) {
+                        return new ScanData[size];
+                    }
+                };
+    }
+
+    public static class ParcelableScanData implements Parcelable {
+
+        public ScanData mResults[];
+
+        public ParcelableScanData(ScanData[] results) {
+            mResults = results;
+        }
+
+        public ScanData[] getResults() {
+            return mResults;
+        }
+
+        /** Implement the Parcelable interface {@hide} */
+        public int describeContents() {
+            return 0;
+        }
+
+        /** Implement the Parcelable interface {@hide} */
+        public void writeToParcel(Parcel dest, int flags) {
+            if (mResults != null) {
+                dest.writeInt(mResults.length);
+                for (int i = 0; i < mResults.length; i++) {
+                    ScanData result = mResults[i];
+                    result.writeToParcel(dest, flags);
+                }
+            } else {
+                dest.writeInt(0);
+            }
+        }
+
+        /** Implement the Parcelable interface {@hide} */
+        public static final @android.annotation.NonNull Creator<ParcelableScanData> CREATOR =
+                new Creator<ParcelableScanData>() {
+                    public ParcelableScanData createFromParcel(Parcel in) {
+                        int n = in.readInt();
+                        ScanData results[] = new ScanData[n];
+                        for (int i = 0; i < n; i++) {
+                            results[i] = ScanData.CREATOR.createFromParcel(in);
+                        }
+                        return new ParcelableScanData(results);
+                    }
+
+                    public ParcelableScanData[] newArray(int size) {
+                        return new ParcelableScanData[size];
+                    }
+                };
+    }
+
+    public static class ParcelableScanResults implements Parcelable {
+
+        public ScanResult mResults[];
+
+        public ParcelableScanResults(ScanResult[] results) {
+            mResults = results;
+        }
+
+        public ScanResult[] getResults() {
+            return mResults;
+        }
+
+        /** Implement the Parcelable interface {@hide} */
+        public int describeContents() {
+            return 0;
+        }
+
+        /** Implement the Parcelable interface {@hide} */
+        public void writeToParcel(Parcel dest, int flags) {
+            if (mResults != null) {
+                dest.writeInt(mResults.length);
+                for (int i = 0; i < mResults.length; i++) {
+                    ScanResult result = mResults[i];
+                    result.writeToParcel(dest, flags);
+                }
+            } else {
+                dest.writeInt(0);
+            }
+        }
+
+        /** Implement the Parcelable interface {@hide} */
+        public static final @android.annotation.NonNull Creator<ParcelableScanResults> CREATOR =
+                new Creator<ParcelableScanResults>() {
+                    public ParcelableScanResults createFromParcel(Parcel in) {
+                        int n = in.readInt();
+                        ScanResult results[] = new ScanResult[n];
+                        for (int i = 0; i < n; i++) {
+                            results[i] = ScanResult.CREATOR.createFromParcel(in);
+                        }
+                        return new ParcelableScanResults(results);
+                    }
+
+                    public ParcelableScanResults[] newArray(int size) {
+                        return new ParcelableScanResults[size];
+                    }
+                };
+    }
+
+    /** {@hide} */
+    public static final String PNO_PARAMS_PNO_SETTINGS_KEY = "PnoSettings";
+    /** {@hide} */
+    public static final String PNO_PARAMS_SCAN_SETTINGS_KEY = "ScanSettings";
+    /**
+     * PNO scan configuration parameters to be sent to {@link #startPnoScan}.
+     * Note: This structure needs to be in sync with |wifi_epno_params| struct in gscan HAL API.
+     * {@hide}
+     */
+    public static class PnoSettings implements Parcelable {
+        /**
+         * Pno network to be added to the PNO scan filtering.
+         * {@hide}
+         */
+        public static class PnoNetwork {
+            /*
+             * Pno flags bitmask to be set in {@link #PnoNetwork.flags}
+             */
+            /** Whether directed scan needs to be performed (for hidden SSIDs) */
+            public static final byte FLAG_DIRECTED_SCAN = (1 << 0);
+            /** Whether PNO event shall be triggered if the network is found on A band */
+            public static final byte FLAG_A_BAND = (1 << 1);
+            /** Whether PNO event shall be triggered if the network is found on G band */
+            public static final byte FLAG_G_BAND = (1 << 2);
+            /**
+             * Whether strict matching is required
+             * If required then the firmware must store the network's SSID and not just a hash
+             */
+            public static final byte FLAG_STRICT_MATCH = (1 << 3);
+            /**
+             * If this SSID should be considered the same network as the currently connected
+             * one for scoring.
+             */
+            public static final byte FLAG_SAME_NETWORK = (1 << 4);
+
+            /*
+             * Code for matching the beacon AUTH IE - additional codes. Bitmask to be set in
+             * {@link #PnoNetwork.authBitField}
+             */
+            /** Open Network */
+            public static final byte AUTH_CODE_OPEN = (1 << 0);
+            /** WPA_PSK or WPA2PSK */
+            public static final byte AUTH_CODE_PSK = (1 << 1);
+            /** any EAPOL */
+            public static final byte AUTH_CODE_EAPOL = (1 << 2);
+
+            /** SSID of the network */
+            public String ssid;
+            /** Bitmask of the FLAG_XXX */
+            public byte flags = 0;
+            /** Bitmask of the ATUH_XXX */
+            public byte authBitField = 0;
+            /** frequencies on which the particular network needs to be scanned for */
+            public int[] frequencies = {};
+
+            /**
+             * default constructor for PnoNetwork
+             */
+            public PnoNetwork(String ssid) {
+                this.ssid = ssid;
+            }
+        }
+
+        /** Connected vs Disconnected PNO flag {@hide} */
+        public boolean isConnected;
+        /** Minimum 5GHz RSSI for a BSSID to be considered */
+        public int min5GHzRssi;
+        /** Minimum 2.4GHz RSSI for a BSSID to be considered */
+        public int min24GHzRssi;
+        /** Maximum score that a network can have before bonuses */
+        public int initialScoreMax;
+        /**
+         *  Only report when there is a network's score this much higher
+         *  than the current connection.
+         */
+        public int currentConnectionBonus;
+        /** score bonus for all networks with the same network flag */
+        public int sameNetworkBonus;
+        /** score bonus for networks that are not open */
+        public int secureBonus;
+        /** 5GHz RSSI score bonus (applied to all 5GHz networks) */
+        public int band5GHzBonus;
+        /** Pno Network filter list */
+        public PnoNetwork[] networkList;
+
+        /** Implement the Parcelable interface {@hide} */
+        public int describeContents() {
+            return 0;
+        }
+
+        /** Implement the Parcelable interface {@hide} */
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeInt(isConnected ? 1 : 0);
+            dest.writeInt(min5GHzRssi);
+            dest.writeInt(min24GHzRssi);
+            dest.writeInt(initialScoreMax);
+            dest.writeInt(currentConnectionBonus);
+            dest.writeInt(sameNetworkBonus);
+            dest.writeInt(secureBonus);
+            dest.writeInt(band5GHzBonus);
+            if (networkList != null) {
+                dest.writeInt(networkList.length);
+                for (int i = 0; i < networkList.length; i++) {
+                    dest.writeString(networkList[i].ssid);
+                    dest.writeByte(networkList[i].flags);
+                    dest.writeByte(networkList[i].authBitField);
+                    dest.writeIntArray(networkList[i].frequencies);
+                }
+            } else {
+                dest.writeInt(0);
+            }
+        }
+
+        /** Implement the Parcelable interface {@hide} */
+        public static final @android.annotation.NonNull Creator<PnoSettings> CREATOR =
+                new Creator<PnoSettings>() {
+                    public PnoSettings createFromParcel(Parcel in) {
+                        PnoSettings settings = new PnoSettings();
+                        settings.isConnected = in.readInt() == 1;
+                        settings.min5GHzRssi = in.readInt();
+                        settings.min24GHzRssi = in.readInt();
+                        settings.initialScoreMax = in.readInt();
+                        settings.currentConnectionBonus = in.readInt();
+                        settings.sameNetworkBonus = in.readInt();
+                        settings.secureBonus = in.readInt();
+                        settings.band5GHzBonus = in.readInt();
+                        int numNetworks = in.readInt();
+                        settings.networkList = new PnoNetwork[numNetworks];
+                        for (int i = 0; i < numNetworks; i++) {
+                            String ssid = in.readString();
+                            PnoNetwork network = new PnoNetwork(ssid);
+                            network.flags = in.readByte();
+                            network.authBitField = in.readByte();
+                            network.frequencies = in.createIntArray();
+                            settings.networkList[i] = network;
+                        }
+                        return settings;
+                    }
+
+                    public PnoSettings[] newArray(int size) {
+                        return new PnoSettings[size];
+                    }
+                };
+
+    }
+
+    /**
+     * interface to get scan events on; specify this on {@link #startBackgroundScan} or
+     * {@link #startScan}
+     */
+    public interface ScanListener extends ActionListener {
+        /**
+         * Framework co-ordinates scans across multiple apps; so it may not give exactly the
+         * same period requested. If period of a scan is changed; it is reported by this event.
+         */
+        public void onPeriodChanged(int periodInMs);
+        /**
+         * reports results retrieved from background scan and single shot scans
+         */
+        public void onResults(ScanData[] results);
+        /**
+         * reports full scan result for each access point found in scan
+         */
+        public void onFullResult(ScanResult fullScanResult);
+    }
+
+    /**
+     * interface to get PNO scan events on; specify this on {@link #startDisconnectedPnoScan} and
+     * {@link #startConnectedPnoScan}.
+     * {@hide}
+     */
+    public interface PnoScanListener extends ScanListener {
+        /**
+         * Invoked when one of the PNO networks are found in scan results.
+         */
+        void onPnoNetworkFound(ScanResult[] results);
+    }
+
+    /**
+     * Enable/Disable wifi scanning.
+     *
+     * {@hide}
+     */
+    @RequiresPermission(Manifest.permission.NETWORK_STACK)
+    public void setScanningEnabled(boolean enable) {
+        validateChannel();
+        mAsyncChannel.sendMessage(enable ? CMD_ENABLE : CMD_DISABLE);
+    }
+
+    /**
+     * Register a listener that will receive results from all single scans
+     * Either the onSuccess/onFailure will be called once when the listener is registered. After
+     * (assuming onSuccess was called) all subsequent single scan results will be delivered to the
+     * listener. It is possible that onFullResult will not be called for all results of the first
+     * scan if the listener was registered during the scan.
+     *
+     * @param listener specifies the object to report events to. This object is also treated as a
+     *                 key for this request, and must also be specified to cancel the request.
+     *                 Multiple requests should also not share this object.
+     * {@hide}
+     */
+    @RequiresPermission(Manifest.permission.NETWORK_STACK)
+    public void registerScanListener(ScanListener listener) {
+        Preconditions.checkNotNull(listener, "listener cannot be null");
+        int key = addListener(listener);
+        if (key == INVALID_KEY) return;
+        validateChannel();
+        mAsyncChannel.sendMessage(CMD_REGISTER_SCAN_LISTENER, 0, key);
+    }
+
+    /**
+     * Deregister a listener for ongoing single scans
+     * @param listener specifies which scan to cancel; must be same object as passed in {@link
+     *  #registerScanListener}
+     * {@hide}
+     */
+    public void deregisterScanListener(ScanListener listener) {
+        Preconditions.checkNotNull(listener, "listener cannot be null");
+        int key = removeListener(listener);
+        if (key == INVALID_KEY) return;
+        validateChannel();
+        mAsyncChannel.sendMessage(CMD_DEREGISTER_SCAN_LISTENER, 0, key);
+    }
+
+    /** start wifi scan in background
+     * @param settings specifies various parameters for the scan; for more information look at
+     * {@link ScanSettings}
+     * @param listener specifies the object to report events to. This object is also treated as a
+     *                 key for this scan, and must also be specified to cancel the scan. Multiple
+     *                 scans should also not share this object.
+     */
+    @RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE)
+    public void startBackgroundScan(ScanSettings settings, ScanListener listener) {
+        startBackgroundScan(settings, listener, null);
+    }
+
+    /** start wifi scan in background
+     * @param settings specifies various parameters for the scan; for more information look at
+     * {@link ScanSettings}
+     * @param workSource WorkSource to blame for power usage
+     * @param listener specifies the object to report events to. This object is also treated as a
+     *                 key for this scan, and must also be specified to cancel the scan. Multiple
+     *                 scans should also not share this object.
+     */
+    @RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE)
+    public void startBackgroundScan(ScanSettings settings, ScanListener listener,
+            WorkSource workSource) {
+        Preconditions.checkNotNull(listener, "listener cannot be null");
+        int key = addListener(listener);
+        if (key == INVALID_KEY) return;
+        validateChannel();
+        Bundle scanParams = new Bundle();
+        scanParams.putParcelable(SCAN_PARAMS_SCAN_SETTINGS_KEY, settings);
+        scanParams.putParcelable(SCAN_PARAMS_WORK_SOURCE_KEY, workSource);
+        scanParams.putString(REQUEST_PACKAGE_NAME_KEY, mContext.getOpPackageName());
+        mAsyncChannel.sendMessage(CMD_START_BACKGROUND_SCAN, 0, key, scanParams);
+    }
+
+    /**
+     * stop an ongoing wifi scan
+     * @param listener specifies which scan to cancel; must be same object as passed in {@link
+     *  #startBackgroundScan}
+     */
+    @RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE)
+    public void stopBackgroundScan(ScanListener listener) {
+        Preconditions.checkNotNull(listener, "listener cannot be null");
+        int key = removeListener(listener);
+        if (key == INVALID_KEY) return;
+        validateChannel();
+        Bundle scanParams = new Bundle();
+        scanParams.putString(REQUEST_PACKAGE_NAME_KEY, mContext.getOpPackageName());
+        mAsyncChannel.sendMessage(CMD_STOP_BACKGROUND_SCAN, 0, key, scanParams);
+    }
+
+    /**
+     * reports currently available scan results on appropriate listeners
+     * @return true if all scan results were reported correctly
+     */
+    @RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE)
+    public boolean getScanResults() {
+        validateChannel();
+        Bundle scanParams = new Bundle();
+        scanParams.putString(REQUEST_PACKAGE_NAME_KEY, mContext.getOpPackageName());
+        Message reply =
+                mAsyncChannel.sendMessageSynchronously(CMD_GET_SCAN_RESULTS, 0, 0, scanParams);
+        return reply.what == CMD_OP_SUCCEEDED;
+    }
+
+    /**
+     * starts a single scan and reports results asynchronously
+     * @param settings specifies various parameters for the scan; for more information look at
+     * {@link ScanSettings}
+     * @param listener specifies the object to report events to. This object is also treated as a
+     *                 key for this scan, and must also be specified to cancel the scan. Multiple
+     *                 scans should also not share this object.
+     */
+    @RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE)
+    public void startScan(ScanSettings settings, ScanListener listener) {
+        startScan(settings, listener, null);
+    }
+
+    /**
+     * starts a single scan and reports results asynchronously
+     * @param settings specifies various parameters for the scan; for more information look at
+     * {@link ScanSettings}
+     * @param workSource WorkSource to blame for power usage
+     * @param listener specifies the object to report events to. This object is also treated as a
+     *                 key for this scan, and must also be specified to cancel the scan. Multiple
+     *                 scans should also not share this object.
+     */
+    @RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE)
+    public void startScan(ScanSettings settings, ScanListener listener, WorkSource workSource) {
+        Preconditions.checkNotNull(listener, "listener cannot be null");
+        int key = addListener(listener);
+        if (key == INVALID_KEY) return;
+        validateChannel();
+        Bundle scanParams = new Bundle();
+        scanParams.putParcelable(SCAN_PARAMS_SCAN_SETTINGS_KEY, settings);
+        scanParams.putParcelable(SCAN_PARAMS_WORK_SOURCE_KEY, workSource);
+        scanParams.putString(REQUEST_PACKAGE_NAME_KEY, mContext.getOpPackageName());
+        mAsyncChannel.sendMessage(CMD_START_SINGLE_SCAN, 0, key, scanParams);
+    }
+
+    /**
+     * stops an ongoing single shot scan; only useful after {@link #startScan} if onResults()
+     * hasn't been called on the listener, ignored otherwise
+     * @param listener
+     */
+    @RequiresPermission(android.Manifest.permission.LOCATION_HARDWARE)
+    public void stopScan(ScanListener listener) {
+        Preconditions.checkNotNull(listener, "listener cannot be null");
+        int key = removeListener(listener);
+        if (key == INVALID_KEY) return;
+        validateChannel();
+        Bundle scanParams = new Bundle();
+        scanParams.putString(REQUEST_PACKAGE_NAME_KEY, mContext.getOpPackageName());
+        mAsyncChannel.sendMessage(CMD_STOP_SINGLE_SCAN, 0, key, scanParams);
+    }
+
+    /**
+     * Retrieve the most recent scan results from a single scan request.
+     * {@hide}
+     */
+    public List<ScanResult> getSingleScanResults() {
+        validateChannel();
+        Bundle scanParams = new Bundle();
+        scanParams.putString(REQUEST_PACKAGE_NAME_KEY, mContext.getOpPackageName());
+        Message reply = mAsyncChannel.sendMessageSynchronously(CMD_GET_SINGLE_SCAN_RESULTS, 0, 0,
+                scanParams);
+        if (reply.what == WifiScanner.CMD_OP_SUCCEEDED) {
+            return Arrays.asList(((ParcelableScanResults) reply.obj).getResults());
+        }
+        OperationResult result = (OperationResult) reply.obj;
+        Log.e(TAG, "Error retrieving SingleScan results reason: " + result.reason
+                + " description: " + result.description);
+        return new ArrayList<ScanResult>();
+    }
+
+    private void startPnoScan(ScanSettings scanSettings, PnoSettings pnoSettings, int key) {
+        // Bundle up both the settings and send it across.
+        Bundle pnoParams = new Bundle();
+        // Set the PNO scan flag.
+        scanSettings.isPnoScan = true;
+        pnoParams.putParcelable(PNO_PARAMS_SCAN_SETTINGS_KEY, scanSettings);
+        pnoParams.putParcelable(PNO_PARAMS_PNO_SETTINGS_KEY, pnoSettings);
+        mAsyncChannel.sendMessage(CMD_START_PNO_SCAN, 0, key, pnoParams);
+    }
+    /**
+     * Start wifi connected PNO scan
+     * @param scanSettings specifies various parameters for the scan; for more information look at
+     * {@link ScanSettings}
+     * @param pnoSettings specifies various parameters for PNO; for more information look at
+     * {@link PnoSettings}
+     * @param listener specifies the object to report events to. This object is also treated as a
+     *                 key for this scan, and must also be specified to cancel the scan. Multiple
+     *                 scans should also not share this object.
+     * {@hide}
+     */
+    public void startConnectedPnoScan(ScanSettings scanSettings, PnoSettings pnoSettings,
+            PnoScanListener listener) {
+        Preconditions.checkNotNull(listener, "listener cannot be null");
+        Preconditions.checkNotNull(pnoSettings, "pnoSettings cannot be null");
+        int key = addListener(listener);
+        if (key == INVALID_KEY) return;
+        validateChannel();
+        pnoSettings.isConnected = true;
+        startPnoScan(scanSettings, pnoSettings, key);
+    }
+    /**
+     * Start wifi disconnected PNO scan
+     * @param scanSettings specifies various parameters for the scan; for more information look at
+     * {@link ScanSettings}
+     * @param pnoSettings specifies various parameters for PNO; for more information look at
+     * {@link PnoSettings}
+     * @param listener specifies the object to report events to. This object is also treated as a
+     *                 key for this scan, and must also be specified to cancel the scan. Multiple
+     *                 scans should also not share this object.
+     * {@hide}
+     */
+    @RequiresPermission(android.Manifest.permission.NETWORK_STACK)
+    public void startDisconnectedPnoScan(ScanSettings scanSettings, PnoSettings pnoSettings,
+            PnoScanListener listener) {
+        Preconditions.checkNotNull(listener, "listener cannot be null");
+        Preconditions.checkNotNull(pnoSettings, "pnoSettings cannot be null");
+        int key = addListener(listener);
+        if (key == INVALID_KEY) return;
+        validateChannel();
+        pnoSettings.isConnected = false;
+        startPnoScan(scanSettings, pnoSettings, key);
+    }
+    /**
+     * Stop an ongoing wifi PNO scan
+     * @param listener specifies which scan to cancel; must be same object as passed in {@link
+     *  #startPnoScan}
+     * {@hide}
+     */
+    @RequiresPermission(android.Manifest.permission.NETWORK_STACK)
+    public void stopPnoScan(ScanListener listener) {
+        Preconditions.checkNotNull(listener, "listener cannot be null");
+        int key = removeListener(listener);
+        if (key == INVALID_KEY) return;
+        validateChannel();
+        mAsyncChannel.sendMessage(CMD_STOP_PNO_SCAN, 0, key);
+    }
+
+    /** specifies information about an access point of interest */
+    @Deprecated
+    public static class BssidInfo {
+        /** bssid of the access point; in XX:XX:XX:XX:XX:XX format */
+        public String bssid;
+        /** low signal strength threshold; more information at {@link ScanResult#level} */
+        public int low;                                            /* minimum RSSI */
+        /** high signal threshold; more information at {@link ScanResult#level} */
+        public int high;                                           /* maximum RSSI */
+        /** channel frequency (in KHz) where you may find this BSSID */
+        public int frequencyHint;
+    }
+
+    /** @hide */
+    @SystemApi
+    @Deprecated
+    public static class WifiChangeSettings implements Parcelable {
+        public int rssiSampleSize;                          /* sample size for RSSI averaging */
+        public int lostApSampleSize;                        /* samples to confirm AP's loss */
+        public int unchangedSampleSize;                     /* samples to confirm no change */
+        public int minApsBreachingThreshold;                /* change threshold to trigger event */
+        public int periodInMs;                              /* scan period in millisecond */
+        public BssidInfo[] bssidInfos;
+
+        /** Implement the Parcelable interface {@hide} */
+        public int describeContents() {
+            return 0;
+        }
+
+        /** Implement the Parcelable interface {@hide} */
+        public void writeToParcel(Parcel dest, int flags) {
+        }
+
+        /** Implement the Parcelable interface {@hide} */
+        public static final @android.annotation.NonNull Creator<WifiChangeSettings> CREATOR =
+                new Creator<WifiChangeSettings>() {
+                    public WifiChangeSettings createFromParcel(Parcel in) {
+                        return new WifiChangeSettings();
+                    }
+
+                    public WifiChangeSettings[] newArray(int size) {
+                        return new WifiChangeSettings[size];
+                    }
+                };
+
+    }
+
+    /** configure WifiChange detection
+     * @param rssiSampleSize number of samples used for RSSI averaging
+     * @param lostApSampleSize number of samples to confirm an access point's loss
+     * @param unchangedSampleSize number of samples to confirm there are no changes
+     * @param minApsBreachingThreshold minimum number of access points that need to be
+     *                                 out of range to detect WifiChange
+     * @param periodInMs indicates period of scan to find changes
+     * @param bssidInfos access points to watch
+     */
+    @Deprecated
+    @SuppressLint("Doclava125")
+    public void configureWifiChange(
+            int rssiSampleSize,                             /* sample size for RSSI averaging */
+            int lostApSampleSize,                           /* samples to confirm AP's loss */
+            int unchangedSampleSize,                        /* samples to confirm no change */
+            int minApsBreachingThreshold,                   /* change threshold to trigger event */
+            int periodInMs,                                 /* period of scan */
+            BssidInfo[] bssidInfos                          /* signal thresholds to cross */
+            )
+    {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * interface to get wifi change events on; use this on {@link #startTrackingWifiChange}
+     */
+    @Deprecated
+    public interface WifiChangeListener extends ActionListener {
+        /** indicates that changes were detected in wifi environment
+         * @param results indicate the access points that exhibited change
+         */
+        public void onChanging(ScanResult[] results);           /* changes are found */
+        /** indicates that no wifi changes are being detected for a while
+         * @param results indicate the access points that are bing monitored for change
+         */
+        public void onQuiescence(ScanResult[] results);         /* changes settled down */
+    }
+
+    /**
+     * track changes in wifi environment
+     * @param listener object to report events on; this object must be unique and must also be
+     *                 provided on {@link #stopTrackingWifiChange}
+     */
+    @Deprecated
+    @SuppressLint("Doclava125")
+    public void startTrackingWifiChange(WifiChangeListener listener) {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * stop tracking changes in wifi environment
+     * @param listener object that was provided to report events on {@link
+     * #stopTrackingWifiChange}
+     */
+    @Deprecated
+    @SuppressLint("Doclava125")
+    public void stopTrackingWifiChange(WifiChangeListener listener) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** @hide */
+    @SystemApi
+    @Deprecated
+    @SuppressLint("Doclava125")
+    public void configureWifiChange(WifiChangeSettings settings) {
+        throw new UnsupportedOperationException();
+    }
+
+    /** interface to receive hotlist events on; use this on {@link #setHotlist} */
+    @Deprecated
+    public static interface BssidListener extends ActionListener {
+        /** indicates that access points were found by on going scans
+         * @param results list of scan results, one for each access point visible currently
+         */
+        public void onFound(ScanResult[] results);
+        /** indicates that access points were missed by on going scans
+         * @param results list of scan results, for each access point that is not visible anymore
+         */
+        public void onLost(ScanResult[] results);
+    }
+
+    /** @hide */
+    @SystemApi
+    @Deprecated
+    public static class HotlistSettings implements Parcelable {
+        public BssidInfo[] bssidInfos;
+        public int apLostThreshold;
+
+        /** Implement the Parcelable interface {@hide} */
+        public int describeContents() {
+            return 0;
+        }
+
+        /** Implement the Parcelable interface {@hide} */
+        public void writeToParcel(Parcel dest, int flags) {
+        }
+
+        /** Implement the Parcelable interface {@hide} */
+        public static final @android.annotation.NonNull Creator<HotlistSettings> CREATOR =
+                new Creator<HotlistSettings>() {
+                    public HotlistSettings createFromParcel(Parcel in) {
+                        HotlistSettings settings = new HotlistSettings();
+                        return settings;
+                    }
+
+                    public HotlistSettings[] newArray(int size) {
+                        return new HotlistSettings[size];
+                    }
+                };
+    }
+
+    /**
+     * set interesting access points to find
+     * @param bssidInfos access points of interest
+     * @param apLostThreshold number of scans needed to indicate that AP is lost
+     * @param listener object provided to report events on; this object must be unique and must
+     *                 also be provided on {@link #stopTrackingBssids}
+     */
+    @Deprecated
+    @SuppressLint("Doclava125")
+    public void startTrackingBssids(BssidInfo[] bssidInfos,
+                                    int apLostThreshold, BssidListener listener) {
+        throw new UnsupportedOperationException();
+    }
+
+    /**
+     * remove tracking of interesting access points
+     * @param listener same object provided in {@link #startTrackingBssids}
+     */
+    @Deprecated
+    @SuppressLint("Doclava125")
+    public void stopTrackingBssids(BssidListener listener) {
+        throw new UnsupportedOperationException();
+    }
+
+
+    /* private members and methods */
+
+    private static final String TAG = "WifiScanner";
+    private static final boolean DBG = false;
+
+    /* commands for Wifi Service */
+    private static final int BASE = Protocol.BASE_WIFI_SCANNER;
+
+    /** @hide */
+    public static final int CMD_START_BACKGROUND_SCAN       = BASE + 2;
+    /** @hide */
+    public static final int CMD_STOP_BACKGROUND_SCAN        = BASE + 3;
+    /** @hide */
+    public static final int CMD_GET_SCAN_RESULTS            = BASE + 4;
+    /** @hide */
+    public static final int CMD_SCAN_RESULT                 = BASE + 5;
+    /** @hide */
+    public static final int CMD_OP_SUCCEEDED                = BASE + 17;
+    /** @hide */
+    public static final int CMD_OP_FAILED                   = BASE + 18;
+    /** @hide */
+    public static final int CMD_FULL_SCAN_RESULT            = BASE + 20;
+    /** @hide */
+    public static final int CMD_START_SINGLE_SCAN           = BASE + 21;
+    /** @hide */
+    public static final int CMD_STOP_SINGLE_SCAN            = BASE + 22;
+    /** @hide */
+    public static final int CMD_SINGLE_SCAN_COMPLETED       = BASE + 23;
+    /** @hide */
+    public static final int CMD_START_PNO_SCAN              = BASE + 24;
+    /** @hide */
+    public static final int CMD_STOP_PNO_SCAN               = BASE + 25;
+    /** @hide */
+    public static final int CMD_PNO_NETWORK_FOUND           = BASE + 26;
+    /** @hide */
+    public static final int CMD_REGISTER_SCAN_LISTENER      = BASE + 27;
+    /** @hide */
+    public static final int CMD_DEREGISTER_SCAN_LISTENER    = BASE + 28;
+    /** @hide */
+    public static final int CMD_GET_SINGLE_SCAN_RESULTS     = BASE + 29;
+    /** @hide */
+    public static final int CMD_ENABLE                      = BASE + 30;
+    /** @hide */
+    public static final int CMD_DISABLE                     = BASE + 31;
+
+    private Context mContext;
+    private IWifiScanner mService;
+
+    private static final int INVALID_KEY = 0;
+    private int mListenerKey = 1;
+
+    private final SparseArray mListenerMap = new SparseArray();
+    private final Object mListenerMapLock = new Object();
+
+    private AsyncChannel mAsyncChannel;
+    private final Handler mInternalHandler;
+
+    /**
+     * Create a new WifiScanner instance.
+     * Applications will almost always want to use
+     * {@link android.content.Context#getSystemService Context.getSystemService()} to retrieve
+     * the standard {@link android.content.Context#WIFI_SERVICE Context.WIFI_SERVICE}.
+     * @param context the application context
+     * @param service the Binder interface
+     * @param looper the Looper used to deliver callbacks
+     * @hide
+     */
+    public WifiScanner(Context context, IWifiScanner service, Looper looper) {
+        mContext = context;
+        mService = service;
+
+        Messenger messenger = null;
+        try {
+            messenger = mService.getMessenger();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+
+        if (messenger == null) {
+            throw new IllegalStateException("getMessenger() returned null!  This is invalid.");
+        }
+
+        mAsyncChannel = new AsyncChannel();
+
+        mInternalHandler = new ServiceHandler(looper);
+        mAsyncChannel.connectSync(mContext, mInternalHandler, messenger);
+        // We cannot use fullyConnectSync because it sends the FULL_CONNECTION message
+        // synchronously, which causes WifiScanningService to receive the wrong replyTo value.
+        mAsyncChannel.sendMessage(AsyncChannel.CMD_CHANNEL_FULL_CONNECTION);
+    }
+
+    private void validateChannel() {
+        if (mAsyncChannel == null) throw new IllegalStateException(
+                "No permission to access and change wifi or a bad initialization");
+    }
+
+    // Add a listener into listener map. If the listener already exists, return INVALID_KEY and
+    // send an error message to internal handler; Otherwise add the listener to the listener map and
+    // return the key of the listener.
+    private int addListener(ActionListener listener) {
+        synchronized (mListenerMapLock) {
+            boolean keyExists = (getListenerKey(listener) != INVALID_KEY);
+            // Note we need to put the listener into listener map even if it's a duplicate as the
+            // internal handler will need the key to find the listener. In case of duplicates,
+            // removing duplicate key logic will be handled in internal handler.
+            int key = putListener(listener);
+            if (keyExists) {
+                if (DBG) Log.d(TAG, "listener key already exists");
+                OperationResult operationResult = new OperationResult(REASON_DUPLICATE_REQEUST,
+                        "Outstanding request with same key not stopped yet");
+                Message message = Message.obtain(mInternalHandler, CMD_OP_FAILED, 0, key,
+                        operationResult);
+                message.sendToTarget();
+                return INVALID_KEY;
+            } else {
+                return key;
+            }
+        }
+    }
+
+    private int putListener(Object listener) {
+        if (listener == null) return INVALID_KEY;
+        int key;
+        synchronized (mListenerMapLock) {
+            do {
+                key = mListenerKey++;
+            } while (key == INVALID_KEY);
+            mListenerMap.put(key, listener);
+        }
+        return key;
+    }
+
+    private Object getListener(int key) {
+        if (key == INVALID_KEY) return null;
+        synchronized (mListenerMapLock) {
+            Object listener = mListenerMap.get(key);
+            return listener;
+        }
+    }
+
+    private int getListenerKey(Object listener) {
+        if (listener == null) return INVALID_KEY;
+        synchronized (mListenerMapLock) {
+            int index = mListenerMap.indexOfValue(listener);
+            if (index == -1) {
+                return INVALID_KEY;
+            } else {
+                return mListenerMap.keyAt(index);
+            }
+        }
+    }
+
+    private Object removeListener(int key) {
+        if (key == INVALID_KEY) return null;
+        synchronized (mListenerMapLock) {
+            Object listener = mListenerMap.get(key);
+            mListenerMap.remove(key);
+            return listener;
+        }
+    }
+
+    private int removeListener(Object listener) {
+        int key = getListenerKey(listener);
+        if (key == INVALID_KEY) {
+            Log.e(TAG, "listener cannot be found");
+            return key;
+        }
+        synchronized (mListenerMapLock) {
+            mListenerMap.remove(key);
+            return key;
+        }
+    }
+
+    /** @hide */
+    public static class OperationResult implements Parcelable {
+        public int reason;
+        public String description;
+
+        public OperationResult(int reason, String description) {
+            this.reason = reason;
+            this.description = description;
+        }
+
+        /** Implement the Parcelable interface {@hide} */
+        public int describeContents() {
+            return 0;
+        }
+
+        /** Implement the Parcelable interface {@hide} */
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeInt(reason);
+            dest.writeString(description);
+        }
+
+        /** Implement the Parcelable interface {@hide} */
+        public static final @android.annotation.NonNull Creator<OperationResult> CREATOR =
+                new Creator<OperationResult>() {
+                    public OperationResult createFromParcel(Parcel in) {
+                        int reason = in.readInt();
+                        String description = in.readString();
+                        return new OperationResult(reason, description);
+                    }
+
+                    public OperationResult[] newArray(int size) {
+                        return new OperationResult[size];
+                    }
+                };
+    }
+
+    private class ServiceHandler extends Handler {
+        ServiceHandler(Looper looper) {
+            super(looper);
+        }
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case AsyncChannel.CMD_CHANNEL_FULLY_CONNECTED:
+                    return;
+                case AsyncChannel.CMD_CHANNEL_DISCONNECTED:
+                    Log.e(TAG, "Channel connection lost");
+                    // This will cause all further async API calls on the WifiManager
+                    // to fail and throw an exception
+                    mAsyncChannel = null;
+                    getLooper().quit();
+                    return;
+            }
+
+            Object listener = getListener(msg.arg2);
+
+            if (listener == null) {
+                if (DBG) Log.d(TAG, "invalid listener key = " + msg.arg2);
+                return;
+            } else {
+                if (DBG) Log.d(TAG, "listener key = " + msg.arg2);
+            }
+
+            switch (msg.what) {
+                    /* ActionListeners grouped together */
+                case CMD_OP_SUCCEEDED :
+                    ((ActionListener) listener).onSuccess();
+                    break;
+                case CMD_OP_FAILED : {
+                        OperationResult result = (OperationResult)msg.obj;
+                        ((ActionListener) listener).onFailure(result.reason, result.description);
+                        removeListener(msg.arg2);
+                    }
+                    break;
+                case CMD_SCAN_RESULT :
+                    ((ScanListener) listener).onResults(
+                            ((ParcelableScanData) msg.obj).getResults());
+                    return;
+                case CMD_FULL_SCAN_RESULT :
+                    ScanResult result = (ScanResult) msg.obj;
+                    ((ScanListener) listener).onFullResult(result);
+                    return;
+                case CMD_SINGLE_SCAN_COMPLETED:
+                    if (DBG) Log.d(TAG, "removing listener for single scan");
+                    removeListener(msg.arg2);
+                    break;
+                case CMD_PNO_NETWORK_FOUND:
+                    ((PnoScanListener) listener).onPnoNetworkFound(
+                            ((ParcelableScanResults) msg.obj).getResults());
+                    return;
+                default:
+                    if (DBG) Log.d(TAG, "Ignoring message " + msg.what);
+                    return;
+            }
+        }
+    }
+}
diff --git a/android/net/wifi/WifiSsid.java b/android/net/wifi/WifiSsid.java
new file mode 100644
index 0000000..70ca088
--- /dev/null
+++ b/android/net/wifi/WifiSsid.java
@@ -0,0 +1,272 @@
+/*
+ * Copyright (C) 2012 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.wifi;
+
+import android.annotation.UnsupportedAppUsage;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.io.ByteArrayOutputStream;
+import java.nio.ByteBuffer;
+import java.nio.CharBuffer;
+import java.nio.charset.Charset;
+import java.nio.charset.CharsetDecoder;
+import java.nio.charset.CoderResult;
+import java.nio.charset.CodingErrorAction;
+import java.util.Arrays;
+import java.util.Locale;
+
+/**
+ * Stores SSID octets and handles conversion.
+ *
+ * For Ascii encoded string, any octet < 32 or > 127 is encoded as
+ * a "\x" followed by the hex representation of the octet.
+ * Exception chars are ", \, \e, \n, \r, \t which are escaped by a \
+ * See src/utils/common.c for the implementation in the supplicant.
+ *
+ * @hide
+ */
+public class WifiSsid implements Parcelable {
+    private static final String TAG = "WifiSsid";
+
+    @UnsupportedAppUsage
+    public final ByteArrayOutputStream octets = new ByteArrayOutputStream(32);
+
+    private static final int HEX_RADIX = 16;
+    @UnsupportedAppUsage
+    public static final String NONE = "<unknown ssid>";
+
+    private WifiSsid() {
+    }
+
+    public static WifiSsid createFromByteArray(byte ssid[]) {
+        WifiSsid wifiSsid = new WifiSsid();
+        if (ssid != null) {
+            wifiSsid.octets.write(ssid, 0/* the start offset */, ssid.length);;
+        }
+        return wifiSsid;
+    }
+
+    @UnsupportedAppUsage
+    public static WifiSsid createFromAsciiEncoded(String asciiEncoded) {
+        WifiSsid a = new WifiSsid();
+        a.convertToBytes(asciiEncoded);
+        return a;
+    }
+
+    public static WifiSsid createFromHex(String hexStr) {
+        WifiSsid a = new WifiSsid();
+        if (hexStr == null) return a;
+
+        if (hexStr.startsWith("0x") || hexStr.startsWith("0X")) {
+            hexStr = hexStr.substring(2);
+        }
+
+        for (int i = 0; i < hexStr.length()-1; i += 2) {
+            int val;
+            try {
+                val = Integer.parseInt(hexStr.substring(i, i + 2), HEX_RADIX);
+            } catch(NumberFormatException e) {
+                val = 0;
+            }
+            a.octets.write(val);
+        }
+        return a;
+    }
+
+    /* This function is equivalent to printf_decode() at src/utils/common.c in
+     * the supplicant */
+    private void convertToBytes(String asciiEncoded) {
+        int i = 0;
+        int val = 0;
+        while (i< asciiEncoded.length()) {
+            char c = asciiEncoded.charAt(i);
+            switch (c) {
+                case '\\':
+                    i++;
+                    switch(asciiEncoded.charAt(i)) {
+                        case '\\':
+                            octets.write('\\');
+                            i++;
+                            break;
+                        case '"':
+                            octets.write('"');
+                            i++;
+                            break;
+                        case 'n':
+                            octets.write('\n');
+                            i++;
+                            break;
+                        case 'r':
+                            octets.write('\r');
+                            i++;
+                            break;
+                        case 't':
+                            octets.write('\t');
+                            i++;
+                            break;
+                        case 'e':
+                            octets.write(27); //escape char
+                            i++;
+                            break;
+                        case 'x':
+                            i++;
+                            try {
+                                val = Integer.parseInt(asciiEncoded.substring(i, i + 2), HEX_RADIX);
+                            } catch (NumberFormatException e) {
+                                val = -1;
+                            } catch (StringIndexOutOfBoundsException e) {
+                                val = -1;
+                            }
+                            if (val < 0) {
+                                val = Character.digit(asciiEncoded.charAt(i), HEX_RADIX);
+                                if (val < 0) break;
+                                octets.write(val);
+                                i++;
+                            } else {
+                                octets.write(val);
+                                i += 2;
+                            }
+                            break;
+                        case '0':
+                        case '1':
+                        case '2':
+                        case '3':
+                        case '4':
+                        case '5':
+                        case '6':
+                        case '7':
+                            val = asciiEncoded.charAt(i) - '0';
+                            i++;
+                            if (asciiEncoded.charAt(i) >= '0' && asciiEncoded.charAt(i) <= '7') {
+                                val = val * 8 + asciiEncoded.charAt(i) - '0';
+                                i++;
+                            }
+                            if (asciiEncoded.charAt(i) >= '0' && asciiEncoded.charAt(i) <= '7') {
+                                val = val * 8 + asciiEncoded.charAt(i) - '0';
+                                i++;
+                            }
+                            octets.write(val);
+                            break;
+                        default:
+                            break;
+                    }
+                    break;
+                default:
+                    octets.write(c);
+                    i++;
+                    break;
+            }
+        }
+    }
+
+    @Override
+    public String toString() {
+        byte[] ssidBytes = octets.toByteArray();
+        // Supplicant returns \x00\x00\x00\x00\x00\x00\x00\x00 hex string
+        // for a hidden access point. Make sure we maintain the previous
+        // behavior of returning empty string for this case.
+        if (octets.size() <= 0 || isArrayAllZeroes(ssidBytes)) return "";
+        // TODO: Handle conversion to other charsets upon failure
+        Charset charset = Charset.forName("UTF-8");
+        CharsetDecoder decoder = charset.newDecoder()
+                .onMalformedInput(CodingErrorAction.REPLACE)
+                .onUnmappableCharacter(CodingErrorAction.REPLACE);
+        CharBuffer out = CharBuffer.allocate(32);
+
+        CoderResult result = decoder.decode(ByteBuffer.wrap(ssidBytes), out, true);
+        out.flip();
+        if (result.isError()) {
+            return NONE;
+        }
+        return out.toString();
+    }
+
+    @Override
+    public boolean equals(Object thatObject) {
+        if (this == thatObject) {
+            return true;
+        }
+        if (!(thatObject instanceof WifiSsid)) {
+            return false;
+        }
+        WifiSsid that = (WifiSsid) thatObject;
+        return Arrays.equals(octets.toByteArray(), that.octets.toByteArray());
+    }
+
+    @Override
+    public int hashCode() {
+        return Arrays.hashCode(octets.toByteArray());
+    }
+
+    private boolean isArrayAllZeroes(byte[] ssidBytes) {
+        for (int i = 0; i< ssidBytes.length; i++) {
+            if (ssidBytes[i] != 0) return false;
+        }
+        return true;
+    }
+
+    /** @hide */
+    public boolean isHidden() {
+        return isArrayAllZeroes(octets.toByteArray());
+    }
+
+    /** @hide */
+    @UnsupportedAppUsage
+    public byte[] getOctets() {
+        return octets.toByteArray();
+    }
+
+    /** @hide */
+    public String getHexString() {
+        String out = "0x";
+        byte[] ssidbytes = getOctets();
+        for (int i = 0; i < octets.size(); i++) {
+            out += String.format(Locale.US, "%02x", ssidbytes[i]);
+        }
+        return (octets.size() > 0) ? out : null;
+    }
+
+    /** Implement the Parcelable interface {@hide} */
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Implement the Parcelable interface {@hide} */
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(octets.size());
+        dest.writeByteArray(octets.toByteArray());
+    }
+
+    /** Implement the Parcelable interface {@hide} */
+    @UnsupportedAppUsage
+    public static final @android.annotation.NonNull Creator<WifiSsid> CREATOR =
+        new Creator<WifiSsid>() {
+            public WifiSsid createFromParcel(Parcel in) {
+                WifiSsid ssid = new WifiSsid();
+                int length = in.readInt();
+                byte b[] = new byte[length];
+                in.readByteArray(b);
+                ssid.octets.write(b, 0, length);
+                return ssid;
+            }
+
+            public WifiSsid[] newArray(int size) {
+                return new WifiSsid[size];
+            }
+        };
+}
diff --git a/android/net/wifi/WifiUsabilityStatsEntry.java b/android/net/wifi/WifiUsabilityStatsEntry.java
new file mode 100644
index 0000000..e595164
--- /dev/null
+++ b/android/net/wifi/WifiUsabilityStatsEntry.java
@@ -0,0 +1,351 @@
+/*
+ * Copyright (C) 2019 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.wifi;
+
+import android.annotation.IntDef;
+import android.annotation.SystemApi;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.telephony.TelephonyManager.NetworkType;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * This class makes a subset of
+ * com.android.server.wifi.nano.WifiMetricsProto.WifiUsabilityStatsEntry parcelable.
+ *
+ * @hide
+ */
+@SystemApi
+public final class WifiUsabilityStatsEntry implements Parcelable {
+    /** {@hide} */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(prefix = {"PROBE_STATUS_"}, value = {
+            PROBE_STATUS_UNKNOWN,
+            PROBE_STATUS_NO_PROBE,
+            PROBE_STATUS_SUCCESS,
+            PROBE_STATUS_FAILURE})
+    public @interface ProbeStatus {}
+
+    /** Link probe status is unknown */
+    public static final int PROBE_STATUS_UNKNOWN = 0;
+    /** Link probe is not triggered */
+    public static final int PROBE_STATUS_NO_PROBE = 1;
+    /** Link probe is triggered and the result is success */
+    public static final int PROBE_STATUS_SUCCESS = 2;
+    /** Link probe is triggered and the result is failure */
+    public static final int PROBE_STATUS_FAILURE = 3;
+
+    /** Absolute milliseconds from device boot when these stats were sampled */
+    private final long mTimeStampMillis;
+    /** The RSSI (in dBm) at the sample time */
+    private final int mRssi;
+    /** Link speed at the sample time in Mbps */
+    private final int mLinkSpeedMbps;
+    /** The total number of tx success counted from the last radio chip reset */
+    private final long mTotalTxSuccess;
+    /** The total number of MPDU data packet retries counted from the last radio chip reset */
+    private final long mTotalTxRetries;
+    /** The total number of tx bad counted from the last radio chip reset */
+    private final long mTotalTxBad;
+    /** The total number of rx success counted from the last radio chip reset */
+    private final long mTotalRxSuccess;
+    /** The total time the wifi radio is on in ms counted from the last radio chip reset */
+    private final long mTotalRadioOnTimeMillis;
+    /** The total time the wifi radio is doing tx in ms counted from the last radio chip reset */
+    private final long mTotalRadioTxTimeMillis;
+    /** The total time the wifi radio is doing rx in ms counted from the last radio chip reset */
+    private final long mTotalRadioRxTimeMillis;
+    /** The total time spent on all types of scans in ms counted from the last radio chip reset */
+    private final long mTotalScanTimeMillis;
+    /** The total time spent on nan scans in ms counted from the last radio chip reset */
+    private final long mTotalNanScanTimeMillis;
+    /** The total time spent on background scans in ms counted from the last radio chip reset */
+    private final long mTotalBackgroundScanTimeMillis;
+    /** The total time spent on roam scans in ms counted from the last radio chip reset */
+    private final long mTotalRoamScanTimeMillis;
+    /** The total time spent on pno scans in ms counted from the last radio chip reset */
+    private final long mTotalPnoScanTimeMillis;
+    /** The total time spent on hotspot2.0 scans and GAS exchange in ms counted from the last radio
+     * chip reset */
+    private final long mTotalHotspot2ScanTimeMillis;
+    /** The total time CCA is on busy status on the current frequency in ms counted from the last
+     * radio chip reset */
+    private final long mTotalCcaBusyFreqTimeMillis;
+    /** The total radio on time on the current frequency from the last radio chip reset */
+    private final long mTotalRadioOnFreqTimeMillis;
+    /** The total number of beacons received from the last radio chip reset */
+    private final long mTotalBeaconRx;
+    /** The status of link probe since last stats update */
+    @ProbeStatus private final int mProbeStatusSinceLastUpdate;
+    /** The elapsed time of the most recent link probe since last stats update */
+    private final int mProbeElapsedTimeSinceLastUpdateMillis;
+    /** The MCS rate of the most recent link probe since last stats update */
+    private final int mProbeMcsRateSinceLastUpdate;
+    /** Rx link speed at the sample time in Mbps */
+    private final int mRxLinkSpeedMbps;
+    private final @NetworkType int mCellularDataNetworkType;
+    private final int mCellularSignalStrengthDbm;
+    private final int mCellularSignalStrengthDb;
+    private final boolean mIsSameRegisteredCell;
+
+    /** Constructor function {@hide} */
+    public WifiUsabilityStatsEntry(long timeStampMillis, int rssi, int linkSpeedMbps,
+            long totalTxSuccess, long totalTxRetries, long totalTxBad, long totalRxSuccess,
+            long totalRadioOnTimeMillis, long totalRadioTxTimeMillis, long totalRadioRxTimeMillis,
+            long totalScanTimeMillis, long totalNanScanTimeMillis,
+            long totalBackgroundScanTimeMillis,
+            long totalRoamScanTimeMillis, long totalPnoScanTimeMillis,
+            long totalHotspot2ScanTimeMillis,
+            long totalCcaBusyFreqTimeMillis, long totalRadioOnFreqTimeMillis, long totalBeaconRx,
+            @ProbeStatus int probeStatusSinceLastUpdate, int probeElapsedTimeSinceLastUpdateMillis,
+            int probeMcsRateSinceLastUpdate, int rxLinkSpeedMbps,
+            @NetworkType int cellularDataNetworkType,
+            int cellularSignalStrengthDbm, int cellularSignalStrengthDb,
+            boolean isSameRegisteredCell) {
+        mTimeStampMillis = timeStampMillis;
+        mRssi = rssi;
+        mLinkSpeedMbps = linkSpeedMbps;
+        mTotalTxSuccess = totalTxSuccess;
+        mTotalTxRetries = totalTxRetries;
+        mTotalTxBad = totalTxBad;
+        mTotalRxSuccess = totalRxSuccess;
+        mTotalRadioOnTimeMillis = totalRadioOnTimeMillis;
+        mTotalRadioTxTimeMillis = totalRadioTxTimeMillis;
+        mTotalRadioRxTimeMillis = totalRadioRxTimeMillis;
+        mTotalScanTimeMillis = totalScanTimeMillis;
+        mTotalNanScanTimeMillis = totalNanScanTimeMillis;
+        mTotalBackgroundScanTimeMillis = totalBackgroundScanTimeMillis;
+        mTotalRoamScanTimeMillis = totalRoamScanTimeMillis;
+        mTotalPnoScanTimeMillis = totalPnoScanTimeMillis;
+        mTotalHotspot2ScanTimeMillis = totalHotspot2ScanTimeMillis;
+        mTotalCcaBusyFreqTimeMillis = totalCcaBusyFreqTimeMillis;
+        mTotalRadioOnFreqTimeMillis = totalRadioOnFreqTimeMillis;
+        mTotalBeaconRx = totalBeaconRx;
+        mProbeStatusSinceLastUpdate = probeStatusSinceLastUpdate;
+        mProbeElapsedTimeSinceLastUpdateMillis = probeElapsedTimeSinceLastUpdateMillis;
+        mProbeMcsRateSinceLastUpdate = probeMcsRateSinceLastUpdate;
+        mRxLinkSpeedMbps = rxLinkSpeedMbps;
+        mCellularDataNetworkType = cellularDataNetworkType;
+        mCellularSignalStrengthDbm = cellularSignalStrengthDbm;
+        mCellularSignalStrengthDb = cellularSignalStrengthDb;
+        mIsSameRegisteredCell = isSameRegisteredCell;
+    }
+
+    /** Implement the Parcelable interface */
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Implement the Parcelable interface */
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeLong(mTimeStampMillis);
+        dest.writeInt(mRssi);
+        dest.writeInt(mLinkSpeedMbps);
+        dest.writeLong(mTotalTxSuccess);
+        dest.writeLong(mTotalTxRetries);
+        dest.writeLong(mTotalTxBad);
+        dest.writeLong(mTotalRxSuccess);
+        dest.writeLong(mTotalRadioOnTimeMillis);
+        dest.writeLong(mTotalRadioTxTimeMillis);
+        dest.writeLong(mTotalRadioRxTimeMillis);
+        dest.writeLong(mTotalScanTimeMillis);
+        dest.writeLong(mTotalNanScanTimeMillis);
+        dest.writeLong(mTotalBackgroundScanTimeMillis);
+        dest.writeLong(mTotalRoamScanTimeMillis);
+        dest.writeLong(mTotalPnoScanTimeMillis);
+        dest.writeLong(mTotalHotspot2ScanTimeMillis);
+        dest.writeLong(mTotalCcaBusyFreqTimeMillis);
+        dest.writeLong(mTotalRadioOnFreqTimeMillis);
+        dest.writeLong(mTotalBeaconRx);
+        dest.writeInt(mProbeStatusSinceLastUpdate);
+        dest.writeInt(mProbeElapsedTimeSinceLastUpdateMillis);
+        dest.writeInt(mProbeMcsRateSinceLastUpdate);
+        dest.writeInt(mRxLinkSpeedMbps);
+        dest.writeInt(mCellularDataNetworkType);
+        dest.writeInt(mCellularSignalStrengthDbm);
+        dest.writeInt(mCellularSignalStrengthDb);
+        dest.writeBoolean(mIsSameRegisteredCell);
+    }
+
+    /** Implement the Parcelable interface */
+    public static final @android.annotation.NonNull Creator<WifiUsabilityStatsEntry> CREATOR =
+            new Creator<WifiUsabilityStatsEntry>() {
+        public WifiUsabilityStatsEntry createFromParcel(Parcel in) {
+            return new WifiUsabilityStatsEntry(
+                    in.readLong(), in.readInt(),
+                    in.readInt(), in.readLong(), in.readLong(),
+                    in.readLong(), in.readLong(), in.readLong(),
+                    in.readLong(), in.readLong(), in.readLong(),
+                    in.readLong(), in.readLong(), in.readLong(),
+                    in.readLong(), in.readLong(), in.readLong(),
+                    in.readLong(), in.readLong(), in.readInt(),
+                    in.readInt(), in.readInt(), in.readInt(),
+                    in.readInt(), in.readInt(), in.readInt(),
+                    in.readBoolean()
+            );
+        }
+
+        public WifiUsabilityStatsEntry[] newArray(int size) {
+            return new WifiUsabilityStatsEntry[size];
+        }
+    };
+
+    /** Absolute milliseconds from device boot when these stats were sampled */
+    public long getTimeStampMillis() {
+        return mTimeStampMillis;
+    }
+
+    /** The RSSI (in dBm) at the sample time */
+    public int getRssi() {
+        return mRssi;
+    }
+
+    /** Link speed at the sample time in Mbps */
+    public int getLinkSpeedMbps() {
+        return mLinkSpeedMbps;
+    }
+
+    /** The total number of tx success counted from the last radio chip reset */
+    public long getTotalTxSuccess() {
+        return mTotalTxSuccess;
+    }
+
+    /** The total number of MPDU data packet retries counted from the last radio chip reset */
+    public long getTotalTxRetries() {
+        return mTotalTxRetries;
+    }
+
+    /** The total number of tx bad counted from the last radio chip reset */
+    public long getTotalTxBad() {
+        return mTotalTxBad;
+    }
+
+    /** The total number of rx success counted from the last radio chip reset */
+    public long getTotalRxSuccess() {
+        return mTotalRxSuccess;
+    }
+
+    /** The total time the wifi radio is on in ms counted from the last radio chip reset */
+    public long getTotalRadioOnTimeMillis() {
+        return mTotalRadioOnTimeMillis;
+    }
+
+    /** The total time the wifi radio is doing tx in ms counted from the last radio chip reset */
+    public long getTotalRadioTxTimeMillis() {
+        return mTotalRadioTxTimeMillis;
+    }
+
+    /** The total time the wifi radio is doing rx in ms counted from the last radio chip reset */
+    public long getTotalRadioRxTimeMillis() {
+        return mTotalRadioRxTimeMillis;
+    }
+
+    /** The total time spent on all types of scans in ms counted from the last radio chip reset */
+    public long getTotalScanTimeMillis() {
+        return mTotalScanTimeMillis;
+    }
+
+    /** The total time spent on nan scans in ms counted from the last radio chip reset */
+    public long getTotalNanScanTimeMillis() {
+        return mTotalNanScanTimeMillis;
+    }
+
+    /** The total time spent on background scans in ms counted from the last radio chip reset */
+    public long getTotalBackgroundScanTimeMillis() {
+        return mTotalBackgroundScanTimeMillis;
+    }
+
+    /** The total time spent on roam scans in ms counted from the last radio chip reset */
+    public long getTotalRoamScanTimeMillis() {
+        return mTotalRoamScanTimeMillis;
+    }
+
+    /** The total time spent on pno scans in ms counted from the last radio chip reset */
+    public long getTotalPnoScanTimeMillis() {
+        return mTotalPnoScanTimeMillis;
+    }
+
+    /** The total time spent on hotspot2.0 scans and GAS exchange in ms counted from the last radio
+     * chip reset */
+    public long getTotalHotspot2ScanTimeMillis() {
+        return mTotalHotspot2ScanTimeMillis;
+    }
+
+    /** The total time CCA is on busy status on the current frequency in ms counted from the last
+     * radio chip reset */
+    public long getTotalCcaBusyFreqTimeMillis() {
+        return mTotalCcaBusyFreqTimeMillis;
+    }
+
+    /** The total radio on time on the current frequency from the last radio chip reset */
+    public long getTotalRadioOnFreqTimeMillis() {
+        return mTotalRadioOnFreqTimeMillis;
+    }
+
+    /** The total number of beacons received from the last radio chip reset */
+    public long getTotalBeaconRx() {
+        return mTotalBeaconRx;
+    }
+
+    /** The status of link probe since last stats update */
+    @ProbeStatus public int getProbeStatusSinceLastUpdate() {
+        return mProbeStatusSinceLastUpdate;
+    }
+
+    /** The elapsed time of the most recent link probe since last stats update */
+    public int getProbeElapsedTimeSinceLastUpdateMillis() {
+        return mProbeElapsedTimeSinceLastUpdateMillis;
+    }
+
+    /** The MCS rate of the most recent link probe since last stats update */
+    public int getProbeMcsRateSinceLastUpdate() {
+        return mProbeMcsRateSinceLastUpdate;
+    }
+
+    /** Rx link speed at the sample time in Mbps */
+    public int getRxLinkSpeedMbps() {
+        return mRxLinkSpeedMbps;
+    }
+
+    /** Cellular data network type currently in use on the device for data transmission */
+    @NetworkType public int getCellularDataNetworkType() {
+        return mCellularDataNetworkType;
+    }
+
+    /**
+     * Cellular signal strength in dBm, NR: CsiRsrp, LTE: Rsrp, WCDMA/TDSCDMA: Rscp,
+     * CDMA: Rssi, EVDO: Rssi, GSM: Rssi
+     */
+    public int getCellularSignalStrengthDbm() {
+        return mCellularSignalStrengthDbm;
+    }
+
+    /**
+     * Cellular signal strength in dB, NR: CsiSinr, LTE: Rsrq, WCDMA: EcNo, TDSCDMA: invalid,
+     * CDMA: Ecio, EVDO: SNR, GSM: invalid
+     */
+    public int getCellularSignalStrengthDb() {
+        return mCellularSignalStrengthDb;
+    }
+
+    /** Whether the primary registered cell of current entry is same as that of previous entry */
+    public boolean isSameRegisteredCell() {
+        return mIsSameRegisteredCell;
+    }
+}
diff --git a/android/net/wifi/WpsInfo.java b/android/net/wifi/WpsInfo.java
new file mode 100644
index 0000000..00cb243
--- /dev/null
+++ b/android/net/wifi/WpsInfo.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2011 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.wifi;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * A class representing Wi-Fi Protected Setup
+ *
+ * {@see WifiP2pConfig}
+ */
+public class WpsInfo implements Parcelable {
+
+    /** Push button configuration */
+    public static final int PBC     = 0;
+    /** Display pin method configuration - pin is generated and displayed on device */
+    public static final int DISPLAY = 1;
+    /** Keypad pin method configuration - pin is entered on device */
+    public static final int KEYPAD  = 2;
+    /** Label pin method configuration - pin is labelled on device */
+    public static final int LABEL   = 3;
+    /** Invalid configuration */
+    public static final int INVALID = 4;
+
+    /** Wi-Fi Protected Setup. www.wi-fi.org/wifi-protected-setup has details */
+    public int setup;
+
+    /** Passed with pin method KEYPAD */
+    public String BSSID;
+
+    /** Passed with pin method configuration */
+    public String pin;
+
+    public WpsInfo() {
+        setup = INVALID;
+        BSSID = null;
+        pin = null;
+    }
+
+    public String toString() {
+        StringBuffer sbuf = new StringBuffer();
+        sbuf.append(" setup: ").append(setup);
+        sbuf.append('\n');
+        sbuf.append(" BSSID: ").append(BSSID);
+        sbuf.append('\n');
+        sbuf.append(" pin: ").append(pin);
+        sbuf.append('\n');
+        return sbuf.toString();
+    }
+
+    /** Implement the Parcelable interface */
+    public int describeContents() {
+        return 0;
+    }
+
+    /* Copy constructor */
+    public WpsInfo(WpsInfo source) {
+        if (source != null) {
+            setup = source.setup;
+            BSSID = source.BSSID;
+            pin = source.pin;
+        }
+    }
+
+    /** Implement the Parcelable interface */
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(setup);
+        dest.writeString(BSSID);
+        dest.writeString(pin);
+    }
+
+    /** Implement the Parcelable interface */
+    public static final @android.annotation.NonNull Creator<WpsInfo> CREATOR =
+        new Creator<WpsInfo>() {
+            public WpsInfo createFromParcel(Parcel in) {
+                WpsInfo config = new WpsInfo();
+                config.setup = in.readInt();
+                config.BSSID = in.readString();
+                config.pin = in.readString();
+                return config;
+            }
+
+            public WpsInfo[] newArray(int size) {
+                return new WpsInfo[size];
+            }
+        };
+}
diff --git a/android/net/wifi/WpsResult.java b/android/net/wifi/WpsResult.java
new file mode 100644
index 0000000..f2ffb6f
--- /dev/null
+++ b/android/net/wifi/WpsResult.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2010 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.wifi;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * A class representing the result of a WPS request
+ * @hide
+ */
+public class WpsResult implements Parcelable {
+
+    public enum Status {
+        SUCCESS,
+        FAILURE,
+        IN_PROGRESS,
+    }
+
+    public Status status;
+
+    public String pin;
+
+    public WpsResult() {
+        status = Status.FAILURE;
+        pin = null;
+    }
+
+    public WpsResult(Status s) {
+        status = s;
+        pin = null;
+    }
+
+    public String toString() {
+        StringBuffer sbuf = new StringBuffer();
+        sbuf.append(" status: ").append(status.toString());
+        sbuf.append('\n');
+        sbuf.append(" pin: ").append(pin);
+        sbuf.append("\n");
+        return sbuf.toString();
+    }
+
+    /** Implement the Parcelable interface {@hide} */
+    public int describeContents() {
+        return 0;
+    }
+
+    /** copy constructor {@hide} */
+    public WpsResult(WpsResult source) {
+        if (source != null) {
+            status = source.status;
+            pin = source.pin;
+        }
+    }
+
+    /** Implement the Parcelable interface {@hide} */
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeString(status.name());
+        dest.writeString(pin);
+    }
+
+    /** Implement the Parcelable interface {@hide} */
+    public static final @android.annotation.NonNull Creator<WpsResult> CREATOR =
+        new Creator<WpsResult>() {
+            public WpsResult createFromParcel(Parcel in) {
+                WpsResult result = new WpsResult();
+                result.status = Status.valueOf(in.readString());
+                result.pin = in.readString();
+                return result;
+            }
+
+            public WpsResult[] newArray(int size) {
+                return new WpsResult[size];
+            }
+        };
+}
diff --git a/android/net/wifi/aware/AttachCallback.java b/android/net/wifi/aware/AttachCallback.java
new file mode 100644
index 0000000..c368b46
--- /dev/null
+++ b/android/net/wifi/aware/AttachCallback.java
@@ -0,0 +1,45 @@
+/*
+ * 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.wifi.aware;
+
+/**
+ * Base class for Aware attach callbacks. Should be extended by applications and set when calling
+ * {@link WifiAwareManager#attach(AttachCallback, android.os.Handler)}. These are callbacks
+ * applying to the Aware connection as a whole - not to specific publish or subscribe sessions -
+ * for that see {@link DiscoverySessionCallback}.
+ */
+public class AttachCallback {
+    /**
+     * Called when Aware attach operation
+     * {@link WifiAwareManager#attach(AttachCallback, android.os.Handler)}
+     * is completed and that we can now start discovery sessions or connections.
+     *
+     * @param session The Aware object on which we can execute further Aware operations - e.g.
+     *                discovery, connections.
+     */
+    public void onAttached(WifiAwareSession session) {
+        /* empty */
+    }
+
+    /**
+     * Called when Aware attach operation
+     * {@link WifiAwareManager#attach(AttachCallback, android.os.Handler)} failed.
+     */
+    public void onAttachFailed() {
+        /* empty */
+    }
+}
diff --git a/android/net/wifi/aware/Characteristics.java b/android/net/wifi/aware/Characteristics.java
new file mode 100644
index 0000000..e2cf4dc
--- /dev/null
+++ b/android/net/wifi/aware/Characteristics.java
@@ -0,0 +1,103 @@
+/*
+ * 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.wifi.aware;
+
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * The characteristics of the Wi-Fi Aware implementation.
+ */
+public final class Characteristics implements Parcelable {
+    /** @hide */
+    public static final String KEY_MAX_SERVICE_NAME_LENGTH = "key_max_service_name_length";
+    /** @hide */
+    public static final String KEY_MAX_SERVICE_SPECIFIC_INFO_LENGTH =
+            "key_max_service_specific_info_length";
+    /** @hide */
+    public static final String KEY_MAX_MATCH_FILTER_LENGTH = "key_max_match_filter_length";
+
+    private Bundle mCharacteristics = new Bundle();
+
+    /** @hide : should not be created by apps */
+    public Characteristics(Bundle characteristics) {
+        mCharacteristics = characteristics;
+    }
+
+    /**
+     * Returns the maximum string length that can be used to specify a Aware service name. Restricts
+     * the parameters of the {@link PublishConfig.Builder#setServiceName(String)} and
+     * {@link SubscribeConfig.Builder#setServiceName(String)}.
+     *
+     * @return A positive integer, maximum string length of Aware service name.
+     */
+    public int getMaxServiceNameLength() {
+        return mCharacteristics.getInt(KEY_MAX_SERVICE_NAME_LENGTH);
+    }
+
+    /**
+     * Returns the maximum length of byte array that can be used to specify a Aware service specific
+     * information field: the arbitrary load used in discovery or the message length of Aware
+     * message exchange. Restricts the parameters of the
+     * {@link PublishConfig.Builder#setServiceSpecificInfo(byte[])},
+     * {@link SubscribeConfig.Builder#setServiceSpecificInfo(byte[])}, and
+     * {@link DiscoverySession#sendMessage(PeerHandle, int, byte[])}
+     * variants.
+     *
+     * @return A positive integer, maximum length of byte array for Aware messaging.
+     */
+    public int getMaxServiceSpecificInfoLength() {
+        return mCharacteristics.getInt(KEY_MAX_SERVICE_SPECIFIC_INFO_LENGTH);
+    }
+
+    /**
+     * Returns the maximum length of byte array that can be used to specify a Aware match filter.
+     * Restricts the parameters of the
+     * {@link PublishConfig.Builder#setMatchFilter(java.util.List)} and
+     * {@link SubscribeConfig.Builder#setMatchFilter(java.util.List)}.
+     *
+     * @return A positive integer, maximum legngth of byte array for Aware discovery match filter.
+     */
+    public int getMaxMatchFilterLength() {
+        return mCharacteristics.getInt(KEY_MAX_MATCH_FILTER_LENGTH);
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeBundle(mCharacteristics);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    public static final @android.annotation.NonNull Creator<Characteristics> CREATOR =
+            new Creator<Characteristics>() {
+                @Override
+                public Characteristics createFromParcel(Parcel in) {
+                    Characteristics c = new Characteristics(in.readBundle());
+                    return c;
+                }
+
+                @Override
+                public Characteristics[] newArray(int size) {
+                    return new Characteristics[size];
+                }
+            };
+}
diff --git a/android/net/wifi/aware/ConfigRequest.java b/android/net/wifi/aware/ConfigRequest.java
new file mode 100644
index 0000000..b07d8ed
--- /dev/null
+++ b/android/net/wifi/aware/ConfigRequest.java
@@ -0,0 +1,349 @@
+/*
+ * 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.wifi.aware;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Arrays;
+
+/**
+ * Defines a request object to configure a Wi-Fi Aware network. Built using
+ * {@link ConfigRequest.Builder}. Configuration is requested using
+ * {@link WifiAwareManager#attach(AttachCallback, android.os.Handler)}.
+ * Note that the actual achieved configuration may be different from the
+ * requested configuration - since different applications may request different
+ * configurations.
+ *
+ * @hide
+ */
+public final class ConfigRequest implements Parcelable {
+    /**
+     * Lower range of possible cluster ID.
+     */
+    public static final int CLUSTER_ID_MIN = 0;
+
+    /**
+     * Upper range of possible cluster ID.
+     */
+    public static final int CLUSTER_ID_MAX = 0xFFFF;
+
+    /**
+     * Indices for configuration variables which are specified per band.
+     */
+    public static final int NAN_BAND_24GHZ = 0;
+    public static final int NAN_BAND_5GHZ = 1;
+
+    /**
+     * Magic values for Discovery Window (DW) interval configuration
+     */
+    public static final int DW_INTERVAL_NOT_INIT = -1;
+    public static final int DW_DISABLE = 0; // only valid for 5GHz
+
+    /**
+     * Indicates whether 5G band support is requested.
+     */
+    public final boolean mSupport5gBand;
+
+    /**
+     * Specifies the desired master preference.
+     */
+    public final int mMasterPreference;
+
+    /**
+     * Specifies the desired lower range of the cluster ID. Must be lower then
+     * {@link ConfigRequest#mClusterHigh}.
+     */
+    public final int mClusterLow;
+
+    /**
+     * Specifies the desired higher range of the cluster ID. Must be higher then
+     * {@link ConfigRequest#mClusterLow}.
+     */
+    public final int mClusterHigh;
+
+    /**
+     * Specifies the discovery window interval for the device on NAN_BAND_*.
+     */
+    public final int mDiscoveryWindowInterval[];
+
+    private ConfigRequest(boolean support5gBand, int masterPreference, int clusterLow,
+            int clusterHigh, int discoveryWindowInterval[]) {
+        mSupport5gBand = support5gBand;
+        mMasterPreference = masterPreference;
+        mClusterLow = clusterLow;
+        mClusterHigh = clusterHigh;
+        mDiscoveryWindowInterval = discoveryWindowInterval;
+    }
+
+    @Override
+    public String toString() {
+        return "ConfigRequest [mSupport5gBand=" + mSupport5gBand + ", mMasterPreference="
+                + mMasterPreference + ", mClusterLow=" + mClusterLow + ", mClusterHigh="
+                + mClusterHigh + ", mDiscoveryWindowInterval="
+                + Arrays.toString(mDiscoveryWindowInterval) + "]";
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(mSupport5gBand ? 1 : 0);
+        dest.writeInt(mMasterPreference);
+        dest.writeInt(mClusterLow);
+        dest.writeInt(mClusterHigh);
+        dest.writeIntArray(mDiscoveryWindowInterval);
+    }
+
+    public static final @android.annotation.NonNull Creator<ConfigRequest> CREATOR = new Creator<ConfigRequest>() {
+        @Override
+        public ConfigRequest[] newArray(int size) {
+            return new ConfigRequest[size];
+        }
+
+        @Override
+        public ConfigRequest createFromParcel(Parcel in) {
+            boolean support5gBand = in.readInt() != 0;
+            int masterPreference = in.readInt();
+            int clusterLow = in.readInt();
+            int clusterHigh = in.readInt();
+            int discoveryWindowInterval[] = in.createIntArray();
+
+            return new ConfigRequest(support5gBand, masterPreference, clusterLow, clusterHigh,
+                    discoveryWindowInterval);
+        }
+    };
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+
+        if (!(o instanceof ConfigRequest)) {
+            return false;
+        }
+
+        ConfigRequest lhs = (ConfigRequest) o;
+
+        return mSupport5gBand == lhs.mSupport5gBand && mMasterPreference == lhs.mMasterPreference
+                && mClusterLow == lhs.mClusterLow && mClusterHigh == lhs.mClusterHigh
+                && Arrays.equals(mDiscoveryWindowInterval, lhs.mDiscoveryWindowInterval);
+    }
+
+    @Override
+    public int hashCode() {
+        int result = 17;
+
+        result = 31 * result + (mSupport5gBand ? 1 : 0);
+        result = 31 * result + mMasterPreference;
+        result = 31 * result + mClusterLow;
+        result = 31 * result + mClusterHigh;
+        result = 31 * result + Arrays.hashCode(mDiscoveryWindowInterval);
+
+        return result;
+    }
+
+    /**
+     * Verifies that the contents of the ConfigRequest are valid. Otherwise
+     * throws an IllegalArgumentException.
+     */
+    public void validate() throws IllegalArgumentException {
+        if (mMasterPreference < 0) {
+            throw new IllegalArgumentException(
+                    "Master Preference specification must be non-negative");
+        }
+        if (mMasterPreference == 1 || mMasterPreference == 255 || mMasterPreference > 255) {
+            throw new IllegalArgumentException("Master Preference specification must not "
+                    + "exceed 255 or use 1 or 255 (reserved values)");
+        }
+        if (mClusterLow < CLUSTER_ID_MIN) {
+            throw new IllegalArgumentException("Cluster specification must be non-negative");
+        }
+        if (mClusterLow > CLUSTER_ID_MAX) {
+            throw new IllegalArgumentException("Cluster specification must not exceed 0xFFFF");
+        }
+        if (mClusterHigh < CLUSTER_ID_MIN) {
+            throw new IllegalArgumentException("Cluster specification must be non-negative");
+        }
+        if (mClusterHigh > CLUSTER_ID_MAX) {
+            throw new IllegalArgumentException("Cluster specification must not exceed 0xFFFF");
+        }
+        if (mClusterLow > mClusterHigh) {
+            throw new IllegalArgumentException(
+                    "Invalid argument combination - must have Cluster Low <= Cluster High");
+        }
+        if (mDiscoveryWindowInterval.length != 2) {
+            throw new IllegalArgumentException(
+                    "Invalid discovery window interval: must have 2 elements (2.4 & 5");
+        }
+        if (mDiscoveryWindowInterval[NAN_BAND_24GHZ] != DW_INTERVAL_NOT_INIT &&
+                (mDiscoveryWindowInterval[NAN_BAND_24GHZ] < 1 // valid for 2.4GHz: [1-5]
+                || mDiscoveryWindowInterval[NAN_BAND_24GHZ] > 5)) {
+            throw new IllegalArgumentException(
+                    "Invalid discovery window interval for 2.4GHz: valid is UNSET or [1,5]");
+        }
+        if (mDiscoveryWindowInterval[NAN_BAND_5GHZ] != DW_INTERVAL_NOT_INIT &&
+                (mDiscoveryWindowInterval[NAN_BAND_5GHZ] < 0 // valid for 5GHz: [0-5]
+                || mDiscoveryWindowInterval[NAN_BAND_5GHZ] > 5)) {
+            throw new IllegalArgumentException(
+                "Invalid discovery window interval for 5GHz: valid is UNSET or [0,5]");
+        }
+
+    }
+
+    /**
+     * Builder used to build {@link ConfigRequest} objects.
+     */
+    public static final class Builder {
+        private boolean mSupport5gBand = true;
+        private int mMasterPreference = 0;
+        private int mClusterLow = CLUSTER_ID_MIN;
+        private int mClusterHigh = CLUSTER_ID_MAX;
+        private int mDiscoveryWindowInterval[] = {DW_INTERVAL_NOT_INIT, DW_INTERVAL_NOT_INIT};
+
+        /**
+         * Specify whether 5G band support is required in this request. Disabled by default.
+         *
+         * @param support5gBand Support for 5G band is required.
+         *
+         * @return The builder to facilitate chaining
+         *         {@code builder.setXXX(..).setXXX(..)}.
+         */
+        public Builder setSupport5gBand(boolean support5gBand) {
+            mSupport5gBand = support5gBand;
+            return this;
+        }
+
+        /**
+         * Specify the Master Preference requested. The permitted range is 0 (the default) to
+         * 255 with 1 and 255 excluded (reserved).
+         *
+         * @param masterPreference The requested master preference
+         *
+         * @return The builder to facilitate chaining
+         *         {@code builder.setXXX(..).setXXX(..)}.
+         */
+        public Builder setMasterPreference(int masterPreference) {
+            if (masterPreference < 0) {
+                throw new IllegalArgumentException(
+                        "Master Preference specification must be non-negative");
+            }
+            if (masterPreference == 1 || masterPreference == 255 || masterPreference > 255) {
+                throw new IllegalArgumentException("Master Preference specification must not "
+                        + "exceed 255 or use 1 or 255 (reserved values)");
+            }
+
+            mMasterPreference = masterPreference;
+            return this;
+        }
+
+        /**
+         * The Cluster ID is generated randomly for new Aware networks. Specify
+         * the lower range of the cluster ID. The upper range is specified using
+         * the {@link ConfigRequest.Builder#setClusterHigh(int)}. The permitted
+         * range is 0 (the default) to the value specified by
+         * {@link ConfigRequest.Builder#setClusterHigh(int)}. Equality of Low and High is
+         * permitted which restricts the Cluster ID to the specified value.
+         *
+         * @param clusterLow The lower range of the generated cluster ID.
+         *
+         * @return The builder to facilitate chaining
+         *         {@code builder.setClusterLow(..).setClusterHigh(..)}.
+         */
+        public Builder setClusterLow(int clusterLow) {
+            if (clusterLow < CLUSTER_ID_MIN) {
+                throw new IllegalArgumentException("Cluster specification must be non-negative");
+            }
+            if (clusterLow > CLUSTER_ID_MAX) {
+                throw new IllegalArgumentException("Cluster specification must not exceed 0xFFFF");
+            }
+
+            mClusterLow = clusterLow;
+            return this;
+        }
+
+        /**
+         * The Cluster ID is generated randomly for new Aware networks. Specify
+         * the lower upper of the cluster ID. The lower range is specified using
+         * the {@link ConfigRequest.Builder#setClusterLow(int)}. The permitted
+         * range is the value specified by
+         * {@link ConfigRequest.Builder#setClusterLow(int)} to 0xFFFF (the default). Equality of
+         * Low and High is permitted which restricts the Cluster ID to the specified value.
+         *
+         * @param clusterHigh The upper range of the generated cluster ID.
+         *
+         * @return The builder to facilitate chaining
+         *         {@code builder.setClusterLow(..).setClusterHigh(..)}.
+         */
+        public Builder setClusterHigh(int clusterHigh) {
+            if (clusterHigh < CLUSTER_ID_MIN) {
+                throw new IllegalArgumentException("Cluster specification must be non-negative");
+            }
+            if (clusterHigh > CLUSTER_ID_MAX) {
+                throw new IllegalArgumentException("Cluster specification must not exceed 0xFFFF");
+            }
+
+            mClusterHigh = clusterHigh;
+            return this;
+        }
+
+        /**
+         * The discovery window interval specifies the discovery windows in which the device will be
+         * awake. The configuration enables trading off latency vs. power (higher interval means
+         * higher discovery latency but lower power).
+         *
+         * @param band Either {@link #NAN_BAND_24GHZ} or {@link #NAN_BAND_5GHZ}.
+         * @param interval A value of 1, 2, 3, 4, or 5 indicating an interval of 2^(interval-1). For
+         *                 the 5GHz band a value of 0 indicates that the device will not be awake
+         *                 for any discovery windows.
+         *
+         * @return The builder itself to facilitate chaining operations
+         *         {@code builder.setDiscoveryWindowInterval(...).setMasterPreference(...)}.
+         */
+        public Builder setDiscoveryWindowInterval(int band, int interval) {
+            if (band != NAN_BAND_24GHZ && band != NAN_BAND_5GHZ) {
+                throw new IllegalArgumentException("Invalid band value");
+            }
+            if ((band == NAN_BAND_24GHZ && (interval < 1 || interval > 5))
+                    || (band == NAN_BAND_5GHZ && (interval < 0 || interval > 5))) {
+                throw new IllegalArgumentException(
+                        "Invalid interval value: 2.4 GHz [1,5] or 5GHz [0,5]");
+            }
+
+            mDiscoveryWindowInterval[band] = interval;
+            return this;
+        }
+
+        /**
+         * Build {@link ConfigRequest} given the current requests made on the
+         * builder.
+         */
+        public ConfigRequest build() {
+            if (mClusterLow > mClusterHigh) {
+                throw new IllegalArgumentException(
+                        "Invalid argument combination - must have Cluster Low <= Cluster High");
+            }
+
+            return new ConfigRequest(mSupport5gBand, mMasterPreference, mClusterLow, mClusterHigh,
+                    mDiscoveryWindowInterval);
+        }
+    }
+}
diff --git a/android/net/wifi/aware/DiscoverySession.java b/android/net/wifi/aware/DiscoverySession.java
new file mode 100644
index 0000000..d97f6fb
--- /dev/null
+++ b/android/net/wifi/aware/DiscoverySession.java
@@ -0,0 +1,430 @@
+/*
+ * 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.wifi.aware;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.net.NetworkSpecifier;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import dalvik.system.CloseGuard;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * A class representing a single publish or subscribe Aware session. This object
+ * will not be created directly - only its child classes are available:
+ * {@link PublishDiscoverySession} and {@link SubscribeDiscoverySession}. This
+ * class provides functionality common to both publish and subscribe discovery sessions:
+ * <ul>
+ *      <li>Sending messages: {@link #sendMessage(PeerHandle, int, byte[])} method.
+ *      <li>Creating a network-specifier when requesting a Aware connection using
+ *      {@link WifiAwareNetworkSpecifier.Builder}.
+ * </ul>
+ * <p>
+ * The {@link #close()} method must be called to destroy discovery sessions once they are
+ * no longer needed.
+ */
+public class DiscoverySession implements AutoCloseable {
+    private static final String TAG = "DiscoverySession";
+    private static final boolean DBG = false;
+    private static final boolean VDBG = false; // STOPSHIP if true
+
+    private static final int MAX_SEND_RETRY_COUNT = 5;
+
+    /** @hide */
+    protected WeakReference<WifiAwareManager> mMgr;
+    /** @hide */
+    protected final int mClientId;
+    /** @hide */
+    protected final int mSessionId;
+    /** @hide */
+    protected boolean mTerminated = false;
+
+    private final CloseGuard mCloseGuard = CloseGuard.get();
+
+    /**
+     * Return the maximum permitted retry count when sending messages using
+     * {@link #sendMessage(PeerHandle, int, byte[], int)}.
+     *
+     * @return Maximum retry count when sending messages.
+     *
+     * @hide
+     */
+    public static int getMaxSendRetryCount() {
+        return MAX_SEND_RETRY_COUNT;
+    }
+
+    /** @hide */
+    public DiscoverySession(WifiAwareManager manager, int clientId, int sessionId) {
+        if (VDBG) {
+            Log.v(TAG, "New discovery session created: manager=" + manager + ", clientId="
+                    + clientId + ", sessionId=" + sessionId);
+        }
+
+        mMgr = new WeakReference<>(manager);
+        mClientId = clientId;
+        mSessionId = sessionId;
+
+        mCloseGuard.open("close");
+    }
+
+    /**
+     * Destroy the publish or subscribe session - free any resources, and stop
+     * transmitting packets on-air (for an active session) or listening for
+     * matches (for a passive session). The session may not be used for any
+     * additional operations after its destruction.
+     * <p>
+     *     This operation must be done on a session which is no longer needed. Otherwise system
+     *     resources will continue to be utilized until the application exits. The only
+     *     exception is a session for which we received a termination callback,
+     *     {@link DiscoverySessionCallback#onSessionTerminated()}.
+     */
+    @Override
+    public void close() {
+        WifiAwareManager mgr = mMgr.get();
+        if (mgr == null) {
+            Log.w(TAG, "destroy: called post GC on WifiAwareManager");
+            return;
+        }
+        mgr.terminateSession(mClientId, mSessionId);
+        mTerminated = true;
+        mMgr.clear();
+        mCloseGuard.close();
+    }
+
+    /**
+     * Sets the status of the session to terminated - i.e. an indication that
+     * already terminated rather than executing a termination.
+     *
+     * @hide
+     */
+    public void setTerminated() {
+        if (mTerminated) {
+            Log.w(TAG, "terminate: already terminated.");
+            return;
+        }
+
+        mTerminated = true;
+        mMgr.clear();
+        mCloseGuard.close();
+    }
+
+    /** @hide */
+    @Override
+    protected void finalize() throws Throwable {
+        try {
+            if (mCloseGuard != null) {
+                mCloseGuard.warnIfOpen();
+            }
+
+            if (!mTerminated) {
+                close();
+            }
+        } finally {
+            super.finalize();
+        }
+    }
+
+    /**
+     * Access the client ID of the Aware session.
+     *
+     * Note: internal visibility for testing.
+     *
+     * @return The internal client ID.
+     *
+     * @hide
+     */
+    @VisibleForTesting
+    public int getClientId() {
+        return mClientId;
+    }
+
+    /**
+     * Access the discovery session ID of the Aware session.
+     *
+     * Note: internal visibility for testing.
+     *
+     * @return The internal discovery session ID.
+     *
+     * @hide
+     */
+    @VisibleForTesting
+    public int getSessionId() {
+        return mSessionId;
+    }
+
+    /**
+     * Sends a message to the specified destination. Aware messages are transmitted in the context
+     * of a discovery session - executed subsequent to a publish/subscribe
+     * {@link DiscoverySessionCallback#onServiceDiscovered(PeerHandle,
+     * byte[], java.util.List)} event.
+     * <p>
+     *     Aware messages are not guaranteed delivery. Callbacks on
+     *     {@link DiscoverySessionCallback} indicate message was transmitted successfully,
+     *     {@link DiscoverySessionCallback#onMessageSendSucceeded(int)}, or transmission
+     *     failed (possibly after several retries) -
+     *     {@link DiscoverySessionCallback#onMessageSendFailed(int)}.
+     * <p>
+     *     The peer will get a callback indicating a message was received using
+     *     {@link DiscoverySessionCallback#onMessageReceived(PeerHandle,
+     *     byte[])}.
+     *
+     * @param peerHandle The peer's handle for the message. Must be a result of an
+     * {@link DiscoverySessionCallback#onServiceDiscovered(PeerHandle,
+     * byte[], java.util.List)} or
+     * {@link DiscoverySessionCallback#onMessageReceived(PeerHandle,
+     * byte[])} events.
+     * @param messageId An arbitrary integer used by the caller to identify the message. The same
+     *            integer ID will be returned in the callbacks indicating message send success or
+     *            failure. The {@code messageId} is not used internally by the Aware service - it
+     *                  can be arbitrary and non-unique.
+     * @param message The message to be transmitted.
+     * @param retryCount An integer specifying how many additional service-level (as opposed to PHY
+     *            or MAC level) retries should be attempted if there is no ACK from the receiver
+     *            (note: no retransmissions are attempted in other failure cases). A value of 0
+     *            indicates no retries. Max permitted value is {@link #getMaxSendRetryCount()}.
+     *
+     * @hide
+     */
+    public void sendMessage(@NonNull PeerHandle peerHandle, int messageId,
+            @Nullable byte[] message, int retryCount) {
+        if (mTerminated) {
+            Log.w(TAG, "sendMessage: called on terminated session");
+            return;
+        }
+
+        WifiAwareManager mgr = mMgr.get();
+        if (mgr == null) {
+            Log.w(TAG, "sendMessage: called post GC on WifiAwareManager");
+            return;
+        }
+
+        mgr.sendMessage(mClientId, mSessionId, peerHandle, message, messageId, retryCount);
+    }
+
+    /**
+     * Sends a message to the specified destination. Aware messages are transmitted in the context
+     * of a discovery session - executed subsequent to a publish/subscribe
+     * {@link DiscoverySessionCallback#onServiceDiscovered(PeerHandle,
+     * byte[], java.util.List)} event.
+     * <p>
+     *     Aware messages are not guaranteed delivery. Callbacks on
+     *     {@link DiscoverySessionCallback} indicate message was transmitted successfully,
+     *     {@link DiscoverySessionCallback#onMessageSendSucceeded(int)}, or transmission
+     *     failed (possibly after several retries) -
+     *     {@link DiscoverySessionCallback#onMessageSendFailed(int)}.
+     * <p>
+     * The peer will get a callback indicating a message was received using
+     * {@link DiscoverySessionCallback#onMessageReceived(PeerHandle,
+     * byte[])}.
+     *
+     * @param peerHandle The peer's handle for the message. Must be a result of an
+     * {@link DiscoverySessionCallback#onServiceDiscovered(PeerHandle,
+     * byte[], java.util.List)} or
+     * {@link DiscoverySessionCallback#onMessageReceived(PeerHandle,
+     * byte[])} events.
+     * @param messageId An arbitrary integer used by the caller to identify the message. The same
+     *            integer ID will be returned in the callbacks indicating message send success or
+     *            failure. The {@code messageId} is not used internally by the Aware service - it
+     *                  can be arbitrary and non-unique.
+     * @param message The message to be transmitted.
+     */
+    public void sendMessage(@NonNull PeerHandle peerHandle, int messageId,
+            @Nullable byte[] message) {
+        sendMessage(peerHandle, messageId, message, 0);
+    }
+
+    /**
+     * Create a {@link android.net.NetworkRequest.Builder#setNetworkSpecifier(NetworkSpecifier)} for
+     * an unencrypted WiFi Aware connection (link) to the specified peer. The
+     * {@link android.net.NetworkRequest.Builder#addTransportType(int)} should be set to
+     * {@link android.net.NetworkCapabilities#TRANSPORT_WIFI_AWARE}.
+     * <p>
+     * This method should be used when setting up a connection with a peer discovered through Aware
+     * discovery or communication (in such scenarios the MAC address of the peer is shielded by
+     * an opaque peer ID handle). If an Aware connection is needed to a peer discovered using other
+     * OOB (out-of-band) mechanism then use the alternative
+     * {@link WifiAwareSession#createNetworkSpecifierOpen(int, byte[])} method - which uses the
+     * peer's MAC address.
+     * <p>
+     * Note: per the Wi-Fi Aware specification the roles are fixed - a Subscriber is an INITIATOR
+     * and a Publisher is a RESPONDER.
+     * <p>
+     * To set up an encrypted link use the
+     * {@link #createNetworkSpecifierPassphrase(PeerHandle, String)} API.
+     * @deprecated Use the replacement {@link WifiAwareNetworkSpecifier.Builder}.
+     *
+     * @param peerHandle The peer's handle obtained through
+     * {@link DiscoverySessionCallback#onServiceDiscovered(PeerHandle, byte[], java.util.List)}
+     *                   or
+     *                   {@link DiscoverySessionCallback#onMessageReceived(PeerHandle, byte[])}.
+     *                   On a RESPONDER this value is used to gate the acceptance of a connection
+     *                   request from only that peer.
+     *
+     * @return A {@link NetworkSpecifier} to be used to construct
+     * {@link android.net.NetworkRequest.Builder#setNetworkSpecifier(NetworkSpecifier)} to pass to
+     * {@link android.net.ConnectivityManager#requestNetwork(android.net.NetworkRequest,
+     * android.net.ConnectivityManager.NetworkCallback)}
+     * [or other varieties of that API].
+     */
+    @Deprecated
+    public NetworkSpecifier createNetworkSpecifierOpen(@NonNull PeerHandle peerHandle) {
+        if (mTerminated) {
+            Log.w(TAG, "createNetworkSpecifierOpen: called on terminated session");
+            return null;
+        }
+
+        WifiAwareManager mgr = mMgr.get();
+        if (mgr == null) {
+            Log.w(TAG, "createNetworkSpecifierOpen: called post GC on WifiAwareManager");
+            return null;
+        }
+
+        int role = this instanceof SubscribeDiscoverySession
+                ? WifiAwareManager.WIFI_AWARE_DATA_PATH_ROLE_INITIATOR
+                : WifiAwareManager.WIFI_AWARE_DATA_PATH_ROLE_RESPONDER;
+
+        return mgr.createNetworkSpecifier(mClientId, role, mSessionId, peerHandle, null, null);
+    }
+
+    /**
+     * Create a {@link android.net.NetworkRequest.Builder#setNetworkSpecifier(NetworkSpecifier)} for
+     * an encrypted WiFi Aware connection (link) to the specified peer. The
+     * {@link android.net.NetworkRequest.Builder#addTransportType(int)} should be set to
+     * {@link android.net.NetworkCapabilities#TRANSPORT_WIFI_AWARE}.
+     * <p>
+     * This method should be used when setting up a connection with a peer discovered through Aware
+     * discovery or communication (in such scenarios the MAC address of the peer is shielded by
+     * an opaque peer ID handle). If an Aware connection is needed to a peer discovered using other
+     * OOB (out-of-band) mechanism then use the alternative
+     * {@link WifiAwareSession#createNetworkSpecifierPassphrase(int, byte[], String)} method -
+     * which uses the peer's MAC address.
+     * <p>
+     * Note: per the Wi-Fi Aware specification the roles are fixed - a Subscriber is an INITIATOR
+     * and a Publisher is a RESPONDER.
+     * @deprecated Use the replacement {@link WifiAwareNetworkSpecifier.Builder}.
+     *
+     * @param peerHandle The peer's handle obtained through
+     * {@link DiscoverySessionCallback#onServiceDiscovered(PeerHandle,
+     * byte[], java.util.List)} or
+     * {@link DiscoverySessionCallback#onMessageReceived(PeerHandle,
+     * byte[])}. On a RESPONDER this value is used to gate the acceptance of a connection request
+     *                   from only that peer.
+     * @param passphrase The passphrase to be used to encrypt the link. The PMK is generated from
+     *                   the passphrase. Use the
+     *                   {@link #createNetworkSpecifierOpen(PeerHandle)} API to
+     *                   specify an open (unencrypted) link.
+     *
+     * @return A {@link NetworkSpecifier} to be used to construct
+     * {@link android.net.NetworkRequest.Builder#setNetworkSpecifier(NetworkSpecifier)} to pass to
+     * {@link android.net.ConnectivityManager#requestNetwork(android.net.NetworkRequest,
+     * android.net.ConnectivityManager.NetworkCallback)}
+     * [or other varieties of that API].
+     */
+    @Deprecated
+    public NetworkSpecifier createNetworkSpecifierPassphrase(
+            @NonNull PeerHandle peerHandle, @NonNull String passphrase) {
+        if (!WifiAwareUtils.validatePassphrase(passphrase)) {
+            throw new IllegalArgumentException("Passphrase must meet length requirements");
+        }
+
+        if (mTerminated) {
+            Log.w(TAG, "createNetworkSpecifierPassphrase: called on terminated session");
+            return null;
+        }
+
+        WifiAwareManager mgr = mMgr.get();
+        if (mgr == null) {
+            Log.w(TAG, "createNetworkSpecifierPassphrase: called post GC on WifiAwareManager");
+            return null;
+        }
+
+        int role = this instanceof SubscribeDiscoverySession
+                ? WifiAwareManager.WIFI_AWARE_DATA_PATH_ROLE_INITIATOR
+                : WifiAwareManager.WIFI_AWARE_DATA_PATH_ROLE_RESPONDER;
+
+        return mgr.createNetworkSpecifier(mClientId, role, mSessionId, peerHandle, null,
+                passphrase);
+    }
+
+    /**
+     * Create a {@link android.net.NetworkRequest.Builder#setNetworkSpecifier(NetworkSpecifier)} for
+     * an encrypted WiFi Aware connection (link) to the specified peer. The
+     * {@link android.net.NetworkRequest.Builder#addTransportType(int)} should be set to
+     * {@link android.net.NetworkCapabilities#TRANSPORT_WIFI_AWARE}.
+     * <p>
+     * This method should be used when setting up a connection with a peer discovered through Aware
+     * discovery or communication (in such scenarios the MAC address of the peer is shielded by
+     * an opaque peer ID handle). If an Aware connection is needed to a peer discovered using other
+     * OOB (out-of-band) mechanism then use the alternative
+     * {@link WifiAwareSession#createNetworkSpecifierPmk(int, byte[], byte[])} method - which uses
+     * the peer's MAC address.
+     * <p>
+     * Note: per the Wi-Fi Aware specification the roles are fixed - a Subscriber is an INITIATOR
+     * and a Publisher is a RESPONDER.
+     * @deprecated Use the replacement {@link WifiAwareNetworkSpecifier.Builder}.
+     *
+     * @param peerHandle The peer's handle obtained through
+     * {@link DiscoverySessionCallback#onServiceDiscovered(PeerHandle,
+     * byte[], java.util.List)} or
+     * {@link DiscoverySessionCallback#onMessageReceived(PeerHandle,
+     * byte[])}. On a RESPONDER this value is used to gate the acceptance of a connection request
+     *                   from only that peer.
+     * @param pmk A PMK (pairwise master key, see IEEE 802.11i) specifying the key to use for
+     *            encrypting the data-path. Use the
+     *            {@link #createNetworkSpecifierPassphrase(PeerHandle, String)} to specify a
+     *            Passphrase or {@link #createNetworkSpecifierOpen(PeerHandle)} to specify an
+     *            open (unencrypted) link.
+     *
+     * @return A {@link NetworkSpecifier} to be used to construct
+     * {@link android.net.NetworkRequest.Builder#setNetworkSpecifier(NetworkSpecifier)} to pass to
+     * {@link android.net.ConnectivityManager#requestNetwork(android.net.NetworkRequest,
+     * android.net.ConnectivityManager.NetworkCallback)}
+     * [or other varieties of that API].
+     *
+     * @hide
+     */
+    @Deprecated
+    @SystemApi
+    public NetworkSpecifier createNetworkSpecifierPmk(@NonNull PeerHandle peerHandle,
+            @NonNull byte[] pmk) {
+        if (!WifiAwareUtils.validatePmk(pmk)) {
+            throw new IllegalArgumentException("PMK must 32 bytes");
+        }
+
+        if (mTerminated) {
+            Log.w(TAG, "createNetworkSpecifierPmk: called on terminated session");
+            return null;
+        }
+
+        WifiAwareManager mgr = mMgr.get();
+        if (mgr == null) {
+            Log.w(TAG, "createNetworkSpecifierPmk: called post GC on WifiAwareManager");
+            return null;
+        }
+
+        int role = this instanceof SubscribeDiscoverySession
+                ? WifiAwareManager.WIFI_AWARE_DATA_PATH_ROLE_INITIATOR
+                : WifiAwareManager.WIFI_AWARE_DATA_PATH_ROLE_RESPONDER;
+
+        return mgr.createNetworkSpecifier(mClientId, role, mSessionId, peerHandle, pmk, null);
+    }
+}
diff --git a/android/net/wifi/aware/DiscoverySessionCallback.java b/android/net/wifi/aware/DiscoverySessionCallback.java
new file mode 100644
index 0000000..bfb0462
--- /dev/null
+++ b/android/net/wifi/aware/DiscoverySessionCallback.java
@@ -0,0 +1,192 @@
+/*
+ * 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.wifi.aware;
+
+import android.annotation.NonNull;
+
+import java.util.List;
+
+/**
+ * Base class for Aware session events callbacks. Should be extended by
+ * applications wanting notifications. The callbacks are set when a
+ * publish or subscribe session is created using
+ * {@link WifiAwareSession#publish(PublishConfig, DiscoverySessionCallback,
+ * android.os.Handler)} or
+ * {@link WifiAwareSession#subscribe(SubscribeConfig, DiscoverySessionCallback,
+ * android.os.Handler)}.
+ * <p>
+ * A single callback is set at session creation - it cannot be replaced.
+ */
+public class DiscoverySessionCallback {
+    /**
+     * Called when a publish operation is started successfully in response to a
+     * {@link WifiAwareSession#publish(PublishConfig, DiscoverySessionCallback,
+     * android.os.Handler)} operation.
+     *
+     * @param session The {@link PublishDiscoverySession} used to control the
+     *            discovery session.
+     */
+    public void onPublishStarted(@NonNull PublishDiscoverySession session) {
+        /* empty */
+    }
+
+    /**
+     * Called when a subscribe operation is started successfully in response to a
+     * {@link WifiAwareSession#subscribe(SubscribeConfig, DiscoverySessionCallback,
+     * android.os.Handler)} operation.
+     *
+     * @param session The {@link SubscribeDiscoverySession} used to control the
+     *            discovery session.
+     */
+    public void onSubscribeStarted(@NonNull SubscribeDiscoverySession session) {
+        /* empty */
+    }
+
+    /**
+     * Called when a publish or subscribe discovery session configuration update request
+     * succeeds. Called in response to
+     * {@link PublishDiscoverySession#updatePublish(PublishConfig)} or
+     * {@link SubscribeDiscoverySession#updateSubscribe(SubscribeConfig)}.
+     */
+    public void onSessionConfigUpdated() {
+        /* empty */
+    }
+
+    /**
+     * Called when a publish or subscribe discovery session cannot be created:
+     * {@link WifiAwareSession#publish(PublishConfig, DiscoverySessionCallback,
+     * android.os.Handler)} or
+     * {@link WifiAwareSession#subscribe(SubscribeConfig, DiscoverySessionCallback,
+     * android.os.Handler)}, or when a configuration update fails:
+     * {@link PublishDiscoverySession#updatePublish(PublishConfig)} or
+     * {@link SubscribeDiscoverySession#updateSubscribe(SubscribeConfig)}.
+     * <p>
+     *     For discovery session updates failure leaves the session running with its previous
+     *     configuration - the discovery session is not terminated.
+     */
+    public void onSessionConfigFailed() {
+        /* empty */
+    }
+
+    /**
+     * Called when a discovery session (publish or subscribe) terminates. Termination may be due
+     * to user-request (either directly through {@link DiscoverySession#close()} or
+     * application-specified expiration, e.g. {@link PublishConfig.Builder#setTtlSec(int)}
+     * or {@link SubscribeConfig.Builder#setTtlSec(int)}).
+     */
+    public void onSessionTerminated() {
+        /* empty */
+    }
+
+    /**
+     * Called when a discovery (publish or subscribe) operation results in a
+     * service discovery.
+     * <p>
+     * Note that this method and
+     * {@link #onServiceDiscoveredWithinRange(PeerHandle, byte[], List, int)} may be called
+     * multiple times per service discovery.
+     *
+     * @param peerHandle An opaque handle to the peer matching our discovery operation.
+     * @param serviceSpecificInfo The service specific information (arbitrary
+     *            byte array) provided by the peer as part of its discovery
+     *            configuration.
+     * @param matchFilter The filter which resulted in this service discovery. For
+     * {@link PublishConfig#PUBLISH_TYPE_UNSOLICITED},
+     * {@link SubscribeConfig#SUBSCRIBE_TYPE_PASSIVE} discovery sessions this is the publisher's
+     *                    match filter. For {@link PublishConfig#PUBLISH_TYPE_SOLICITED},
+     *                    {@link SubscribeConfig#SUBSCRIBE_TYPE_ACTIVE} discovery sessions this
+     *                    is the subscriber's match filter.
+     */
+    public void onServiceDiscovered(PeerHandle peerHandle,
+            byte[] serviceSpecificInfo, List<byte[]> matchFilter) {
+        /* empty */
+    }
+
+    /**
+     * Called when a discovery (publish or subscribe) operation results in a
+     * service discovery. Called when a Subscribe service was configured with a range requirement
+     * {@link SubscribeConfig.Builder#setMinDistanceMm(int)} and/or
+     * {@link SubscribeConfig.Builder#setMaxDistanceMm(int)} and the Publish service was configured
+     * with {@link PublishConfig.Builder#setRangingEnabled(boolean)}.
+     * <p>
+     * If either Publisher or Subscriber does not enable Ranging, or if Ranging is temporarily
+     * disabled by the underlying device, service discovery proceeds without ranging and the
+     * {@link #onServiceDiscovered(PeerHandle, byte[], List)} is called.
+     * <p>
+     * Note that this method and {@link #onServiceDiscovered(PeerHandle, byte[], List)} may be
+     * called multiple times per service discovery.
+     *
+     * @param peerHandle An opaque handle to the peer matching our discovery operation.
+     * @param serviceSpecificInfo The service specific information (arbitrary
+     *            byte array) provided by the peer as part of its discovery
+     *            configuration.
+     * @param matchFilter The filter which resulted in this service discovery. For
+     * {@link PublishConfig#PUBLISH_TYPE_UNSOLICITED},
+     * {@link SubscribeConfig#SUBSCRIBE_TYPE_PASSIVE} discovery sessions this is the publisher's
+     *                    match filter. For {@link PublishConfig#PUBLISH_TYPE_SOLICITED},
+     *                    {@link SubscribeConfig#SUBSCRIBE_TYPE_ACTIVE} discovery sessions this
+     *                    is the subscriber's match filter.
+     * @param distanceMm The measured distance to the Publisher in mm. Note: the measured distance
+     *                   may be negative for very close devices.
+     */
+    public void onServiceDiscoveredWithinRange(PeerHandle peerHandle,
+        byte[] serviceSpecificInfo, List<byte[]> matchFilter, int distanceMm) {
+        /* empty */
+    }
+
+    /**
+     * Called in response to
+     * {@link DiscoverySession#sendMessage(PeerHandle, int, byte[])}
+     * when a message is transmitted successfully - i.e. when it was received successfully by the
+     * peer (corresponds to an ACK being received).
+     * <p>
+     * Note that either this callback or
+     * {@link DiscoverySessionCallback#onMessageSendFailed(int)} will be
+     * received - never both.
+     *
+     * @param messageId The arbitrary message ID specified when sending the message.
+     */
+    public void onMessageSendSucceeded(@SuppressWarnings("unused") int messageId) {
+        /* empty */
+    }
+
+    /**
+     * Called when message transmission initiated with
+     * {@link DiscoverySession#sendMessage(PeerHandle, int, byte[])} fails. E.g. when no ACK is
+     * received from the peer.
+     * <p>
+     * Note that either this callback or
+     * {@link DiscoverySessionCallback#onMessageSendSucceeded(int)} will be received
+     * - never both.
+     *
+     * @param messageId The arbitrary message ID specified when sending the message.
+     */
+    public void onMessageSendFailed(@SuppressWarnings("unused") int messageId) {
+        /* empty */
+    }
+
+    /**
+     * Called when a message is received from a discovery session peer - in response to the
+     * peer's {@link DiscoverySession#sendMessage(PeerHandle, int, byte[])}.
+     *
+     * @param peerHandle An opaque handle to the peer matching our discovery operation.
+     * @param message A byte array containing the message.
+     */
+    public void onMessageReceived(PeerHandle peerHandle, byte[] message) {
+        /* empty */
+    }
+}
diff --git a/android/net/wifi/aware/IdentityChangedListener.java b/android/net/wifi/aware/IdentityChangedListener.java
new file mode 100644
index 0000000..a8b19b3
--- /dev/null
+++ b/android/net/wifi/aware/IdentityChangedListener.java
@@ -0,0 +1,39 @@
+/*
+ * 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.wifi.aware;
+
+/**
+ * Base class for a listener which is called with the MAC address of the Aware interface whenever
+ * it is changed. Change may be due to device joining a cluster, starting a cluster, or discovery
+ * interface change (addresses are randomized at regular intervals). The implication is that
+ * peers you've been communicating with may no longer recognize you and you need to re-establish
+ * your identity - e.g. by starting a discovery session. This actual MAC address of the
+ * interface may also be useful if the application uses alternative (non-Aware) discovery but needs
+ * to set up a Aware connection. The provided Aware discovery interface MAC address can then be used
+ * in {@link WifiAwareSession#createNetworkSpecifierOpen(int, byte[])} or
+ * {@link WifiAwareSession#createNetworkSpecifierPassphrase(int, byte[], String)}.
+ */
+public class IdentityChangedListener {
+    /**
+     * @param mac The MAC address of the Aware discovery interface. The application must have the
+     * {@link android.Manifest.permission#ACCESS_FINE_LOCATION} to get the actual MAC address,
+     *            otherwise all 0's will be provided.
+     */
+    public void onIdentityChanged(byte[] mac) {
+        /* empty */
+    }
+}
diff --git a/android/net/wifi/aware/ParcelablePeerHandle.java b/android/net/wifi/aware/ParcelablePeerHandle.java
new file mode 100644
index 0000000..ca473db
--- /dev/null
+++ b/android/net/wifi/aware/ParcelablePeerHandle.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2019 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.wifi.aware;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+/**
+ * A Parcelable {@link PeerHandle}. Can be constructed from a {@code PeerHandle} and then passed
+ * to any of the APIs which take a {@code PeerHandle} as inputs.
+ */
+public final class ParcelablePeerHandle extends PeerHandle implements Parcelable {
+    /**
+     * Construct a parcelable version of {@link PeerHandle}.
+     *
+     * @param peerHandle The {@link PeerHandle} to be made parcelable.
+     */
+    public ParcelablePeerHandle(@NonNull PeerHandle peerHandle) {
+        super(peerHandle.peerId);
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(peerId);
+    }
+
+    public static final @android.annotation.NonNull Creator<ParcelablePeerHandle> CREATOR =
+            new Creator<ParcelablePeerHandle>() {
+                @Override
+                public ParcelablePeerHandle[] newArray(int size) {
+                    return new ParcelablePeerHandle[size];
+                }
+
+                @Override
+                public ParcelablePeerHandle createFromParcel(Parcel in) {
+                    int peerHandle = in.readInt();
+                    return new ParcelablePeerHandle(new PeerHandle(peerHandle));
+                }
+            };
+}
diff --git a/android/net/wifi/aware/PeerHandle.java b/android/net/wifi/aware/PeerHandle.java
new file mode 100644
index 0000000..422e177
--- /dev/null
+++ b/android/net/wifi/aware/PeerHandle.java
@@ -0,0 +1,63 @@
+/*
+ * 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.wifi.aware;
+
+/**
+ * Opaque object used to represent a Wi-Fi Aware peer. Obtained from discovery sessions in
+ * {@link DiscoverySessionCallback#onServiceDiscovered(PeerHandle, byte[], java.util.List)} or
+ * received messages in {@link DiscoverySessionCallback#onMessageReceived(PeerHandle, byte[])}, and
+ * used when sending messages e,g, {@link DiscoverySession#sendMessage(PeerHandle, int, byte[])},
+ * or when configuring a network link to a peer, e.g.
+ * {@link DiscoverySession#createNetworkSpecifierOpen(PeerHandle)} or
+ * {@link DiscoverySession#createNetworkSpecifierPassphrase(PeerHandle, String)}.
+ * <p>
+ * Note that while a {@code PeerHandle} can be used to track a particular peer (i.e. you can compare
+ * the values received from subsequent messages) - it is good practice not to rely on it. Instead
+ * use an application level peer identifier encoded in the message,
+ * {@link DiscoverySession#sendMessage(PeerHandle, int, byte[])}, and/or in the Publish
+ * configuration's service-specific information field,
+ * {@link PublishConfig.Builder#setServiceSpecificInfo(byte[])}, or match filter,
+ * {@link PublishConfig.Builder#setMatchFilter(java.util.List)}.
+ * <p>A parcelable handle object is available with {@link ParcelablePeerHandle}.
+ */
+public class PeerHandle {
+    /** @hide */
+    public PeerHandle(int peerId) {
+        this.peerId = peerId;
+    }
+
+    /** @hide */
+    public int peerId;
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+
+        if (!(o instanceof PeerHandle)) {
+            return false;
+        }
+
+        return peerId == ((PeerHandle) o).peerId;
+    }
+
+    @Override
+    public int hashCode() {
+        return peerId;
+    }
+}
diff --git a/android/net/wifi/aware/PublishConfig.java b/android/net/wifi/aware/PublishConfig.java
new file mode 100644
index 0000000..1886b7e
--- /dev/null
+++ b/android/net/wifi/aware/PublishConfig.java
@@ -0,0 +1,394 @@
+/*
+ * 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.wifi.aware;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import libcore.util.HexEncoding;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Defines the configuration of a Aware publish session. Built using
+ * {@link PublishConfig.Builder}. A publish session is created using
+ * {@link WifiAwareSession#publish(PublishConfig, DiscoverySessionCallback,
+ * android.os.Handler)} or updated using
+ * {@link PublishDiscoverySession#updatePublish(PublishConfig)}.
+ */
+public final class PublishConfig implements Parcelable {
+    /** @hide */
+    @IntDef({
+            PUBLISH_TYPE_UNSOLICITED, PUBLISH_TYPE_SOLICITED })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface PublishTypes {
+    }
+
+    /**
+     * Defines an unsolicited publish session - a publish session where the publisher is
+     * advertising itself by broadcasting on-the-air. An unsolicited publish session is paired
+     * with an passive subscribe session {@link SubscribeConfig#SUBSCRIBE_TYPE_PASSIVE}.
+     * Configuration is done using {@link PublishConfig.Builder#setPublishType(int)}.
+     */
+    public static final int PUBLISH_TYPE_UNSOLICITED = 0;
+
+    /**
+     * Defines a solicited publish session - a publish session which is silent, waiting for a
+     * matching active subscribe session - and responding to it in unicast. A
+     * solicited publish session is paired with an active subscribe session
+     * {@link SubscribeConfig#SUBSCRIBE_TYPE_ACTIVE}. Configuration is done using
+     * {@link PublishConfig.Builder#setPublishType(int)}.
+     */
+    public static final int PUBLISH_TYPE_SOLICITED = 1;
+
+    /** @hide */
+    public final byte[] mServiceName;
+
+    /** @hide */
+    public final byte[] mServiceSpecificInfo;
+
+    /** @hide */
+    public final byte[] mMatchFilter;
+
+    /** @hide */
+    public final int mPublishType;
+
+    /** @hide */
+    public final int mTtlSec;
+
+    /** @hide */
+    public final boolean mEnableTerminateNotification;
+
+    /** @hide */
+    public final boolean mEnableRanging;
+
+    /** @hide */
+    public PublishConfig(byte[] serviceName, byte[] serviceSpecificInfo, byte[] matchFilter,
+            int publishType, int ttlSec, boolean enableTerminateNotification,
+            boolean enableRanging) {
+        mServiceName = serviceName;
+        mServiceSpecificInfo = serviceSpecificInfo;
+        mMatchFilter = matchFilter;
+        mPublishType = publishType;
+        mTtlSec = ttlSec;
+        mEnableTerminateNotification = enableTerminateNotification;
+        mEnableRanging = enableRanging;
+    }
+
+    @Override
+    public String toString() {
+        return "PublishConfig [mServiceName='" + (mServiceName == null ? "<null>" : String.valueOf(
+                HexEncoding.encode(mServiceName))) + ", mServiceName.length=" + (
+                mServiceName == null ? 0 : mServiceName.length) + ", mServiceSpecificInfo='" + (
+                (mServiceSpecificInfo == null) ? "<null>" : String.valueOf(
+                        HexEncoding.encode(mServiceSpecificInfo)))
+                + ", mServiceSpecificInfo.length=" + (mServiceSpecificInfo == null ? 0
+                : mServiceSpecificInfo.length) + ", mMatchFilter="
+                + (new TlvBufferUtils.TlvIterable(0, 1, mMatchFilter)).toString()
+                + ", mMatchFilter.length=" + (mMatchFilter == null ? 0 : mMatchFilter.length)
+                + ", mPublishType=" + mPublishType + ", mTtlSec=" + mTtlSec
+                + ", mEnableTerminateNotification=" + mEnableTerminateNotification
+                + ", mEnableRanging=" + mEnableRanging + "]";
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeByteArray(mServiceName);
+        dest.writeByteArray(mServiceSpecificInfo);
+        dest.writeByteArray(mMatchFilter);
+        dest.writeInt(mPublishType);
+        dest.writeInt(mTtlSec);
+        dest.writeInt(mEnableTerminateNotification ? 1 : 0);
+        dest.writeInt(mEnableRanging ? 1 : 0);
+    }
+
+    public static final @android.annotation.NonNull Creator<PublishConfig> CREATOR = new Creator<PublishConfig>() {
+        @Override
+        public PublishConfig[] newArray(int size) {
+            return new PublishConfig[size];
+        }
+
+        @Override
+        public PublishConfig createFromParcel(Parcel in) {
+            byte[] serviceName = in.createByteArray();
+            byte[] ssi = in.createByteArray();
+            byte[] matchFilter = in.createByteArray();
+            int publishType = in.readInt();
+            int ttlSec = in.readInt();
+            boolean enableTerminateNotification = in.readInt() != 0;
+            boolean enableRanging = in.readInt() != 0;
+
+            return new PublishConfig(serviceName, ssi, matchFilter, publishType,
+                    ttlSec, enableTerminateNotification, enableRanging);
+        }
+    };
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+
+        if (!(o instanceof PublishConfig)) {
+            return false;
+        }
+
+        PublishConfig lhs = (PublishConfig) o;
+
+        return Arrays.equals(mServiceName, lhs.mServiceName) && Arrays.equals(mServiceSpecificInfo,
+                lhs.mServiceSpecificInfo) && Arrays.equals(mMatchFilter, lhs.mMatchFilter)
+                && mPublishType == lhs.mPublishType
+                && mTtlSec == lhs.mTtlSec
+                && mEnableTerminateNotification == lhs.mEnableTerminateNotification
+                && mEnableRanging == lhs.mEnableRanging;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(Arrays.hashCode(mServiceName), Arrays.hashCode(mServiceSpecificInfo),
+                Arrays.hashCode(mMatchFilter), mPublishType, mTtlSec, mEnableTerminateNotification,
+                mEnableRanging);
+    }
+
+    /**
+     * Verifies that the contents of the PublishConfig are valid. Otherwise
+     * throws an IllegalArgumentException.
+     *
+     * @hide
+     */
+    public void assertValid(Characteristics characteristics, boolean rttSupported)
+            throws IllegalArgumentException {
+        WifiAwareUtils.validateServiceName(mServiceName);
+
+        if (!TlvBufferUtils.isValid(mMatchFilter, 0, 1)) {
+            throw new IllegalArgumentException(
+                    "Invalid txFilter configuration - LV fields do not match up to length");
+        }
+        if (mPublishType < PUBLISH_TYPE_UNSOLICITED || mPublishType > PUBLISH_TYPE_SOLICITED) {
+            throw new IllegalArgumentException("Invalid publishType - " + mPublishType);
+        }
+        if (mTtlSec < 0) {
+            throw new IllegalArgumentException("Invalid ttlSec - must be non-negative");
+        }
+
+        if (characteristics != null) {
+            int maxServiceNameLength = characteristics.getMaxServiceNameLength();
+            if (maxServiceNameLength != 0 && mServiceName.length > maxServiceNameLength) {
+                throw new IllegalArgumentException(
+                        "Service name longer than supported by device characteristics");
+            }
+            int maxServiceSpecificInfoLength = characteristics.getMaxServiceSpecificInfoLength();
+            if (maxServiceSpecificInfoLength != 0 && mServiceSpecificInfo != null
+                    && mServiceSpecificInfo.length > maxServiceSpecificInfoLength) {
+                throw new IllegalArgumentException(
+                        "Service specific info longer than supported by device characteristics");
+            }
+            int maxMatchFilterLength = characteristics.getMaxMatchFilterLength();
+            if (maxMatchFilterLength != 0 && mMatchFilter != null
+                    && mMatchFilter.length > maxMatchFilterLength) {
+                throw new IllegalArgumentException(
+                        "Match filter longer than supported by device characteristics");
+            }
+        }
+
+        if (!rttSupported && mEnableRanging) {
+            throw new IllegalArgumentException("Ranging is not supported");
+        }
+    }
+
+    /**
+     * Builder used to build {@link PublishConfig} objects.
+     */
+    public static final class Builder {
+        private byte[] mServiceName;
+        private byte[] mServiceSpecificInfo;
+        private byte[] mMatchFilter;
+        private int mPublishType = PUBLISH_TYPE_UNSOLICITED;
+        private int mTtlSec = 0;
+        private boolean mEnableTerminateNotification = true;
+        private boolean mEnableRanging = false;
+
+        /**
+         * Specify the service name of the publish session. The actual on-air
+         * value is a 6 byte hashed representation of this string.
+         * <p>
+         * The Service Name is a UTF-8 encoded string from 1 to 255 bytes in length.
+         * The only acceptable single-byte UTF-8 symbols for a Service Name are alphanumeric
+         * values (A-Z, a-z, 0-9), the hyphen ('-'), and the period ('.'). All valid multi-byte
+         * UTF-8 characters are acceptable in a Service Name.
+         * <p>
+         * Must be called - an empty ServiceName is not valid.
+         *
+         * @param serviceName The service name for the publish session.
+         *
+         * @return The builder to facilitate chaining
+         *         {@code builder.setXXX(..).setXXX(..)}.
+         */
+        public Builder setServiceName(@NonNull String serviceName) {
+            if (serviceName == null) {
+                throw new IllegalArgumentException("Invalid service name - must be non-null");
+            }
+            mServiceName = serviceName.getBytes(StandardCharsets.UTF_8);
+            return this;
+        }
+
+        /**
+         * Specify service specific information for the publish session. This is
+         * a free-form byte array available to the application to send
+         * additional information as part of the discovery operation - it
+         * will not be used to determine whether a publish/subscribe match
+         * occurs.
+         * <p>
+         *     Optional. Empty by default.
+         *
+         * @param serviceSpecificInfo A byte-array for the service-specific
+         *            information field.
+         *
+         * @return The builder to facilitate chaining
+         *         {@code builder.setXXX(..).setXXX(..)}.
+         */
+        public Builder setServiceSpecificInfo(@Nullable byte[] serviceSpecificInfo) {
+            mServiceSpecificInfo = serviceSpecificInfo;
+            return this;
+        }
+
+        /**
+         * The match filter for a publish session. Used to determine whether a service
+         * discovery occurred - in addition to relying on the service name.
+         * <p>
+         *     Optional. Empty by default.
+         *
+         * @param matchFilter A list of match filter entries (each of which is an arbitrary byte
+         *                    array).
+         *
+         * @return The builder to facilitate chaining
+         *         {@code builder.setXXX(..).setXXX(..)}.
+         */
+        public Builder setMatchFilter(@Nullable List<byte[]> matchFilter) {
+            mMatchFilter = new TlvBufferUtils.TlvConstructor(0, 1).allocateAndPut(
+                    matchFilter).getArray();
+            return this;
+        }
+
+        /**
+         * Specify the type of the publish session: solicited (aka active - publish
+         * packets are transmitted over-the-air), or unsolicited (aka passive -
+         * no publish packets are transmitted, a match is made against an active
+         * subscribe session whose packets are transmitted over-the-air).
+         *
+         * @param publishType Publish session type:
+         *            {@link PublishConfig#PUBLISH_TYPE_SOLICITED} or
+         *            {@link PublishConfig#PUBLISH_TYPE_UNSOLICITED} (the default).
+         *
+         * @return The builder to facilitate chaining
+         *         {@code builder.setXXX(..).setXXX(..)}.
+         */
+        public Builder setPublishType(@PublishTypes int publishType) {
+            if (publishType < PUBLISH_TYPE_UNSOLICITED || publishType > PUBLISH_TYPE_SOLICITED) {
+                throw new IllegalArgumentException("Invalid publishType - " + publishType);
+            }
+            mPublishType = publishType;
+            return this;
+        }
+
+        /**
+         * Sets the time interval (in seconds) an unsolicited (
+         * {@link PublishConfig.Builder#setPublishType(int)}) publish session
+         * will be alive - broadcasting a packet. When the TTL is reached
+         * an event will be generated for
+         * {@link DiscoverySessionCallback#onSessionTerminated()} [unless
+         * {@link #setTerminateNotificationEnabled(boolean)} disables the callback].
+         * <p>
+         *     Optional. 0 by default - indicating the session doesn't terminate on its own.
+         *     Session will be terminated when {@link DiscoverySession#close()} is
+         *     called.
+         *
+         * @param ttlSec Lifetime of a publish session in seconds.
+         *
+         * @return The builder to facilitate chaining
+         *         {@code builder.setXXX(..).setXXX(..)}.
+         */
+        public Builder setTtlSec(int ttlSec) {
+            if (ttlSec < 0) {
+                throw new IllegalArgumentException("Invalid ttlSec - must be non-negative");
+            }
+            mTtlSec = ttlSec;
+            return this;
+        }
+
+        /**
+         * Configure whether a publish terminate notification
+         * {@link DiscoverySessionCallback#onSessionTerminated()} is reported
+         * back to the callback.
+         *
+         * @param enable If true the terminate callback will be called when the
+         *            publish is terminated. Otherwise it will not be called.
+         *
+         * @return The builder to facilitate chaining
+         *         {@code builder.setXXX(..).setXXX(..)}.
+         */
+        public Builder setTerminateNotificationEnabled(boolean enable) {
+            mEnableTerminateNotification = enable;
+            return this;
+        }
+
+        /**
+         * Configure whether the publish discovery session supports ranging and allows peers to
+         * measure distance to it. This API is used in conjunction with
+         * {@link SubscribeConfig.Builder#setMinDistanceMm(int)} and
+         * {@link SubscribeConfig.Builder#setMaxDistanceMm(int)} to specify a minimum and/or
+         * maximum distance at which discovery will be triggered.
+         * <p>
+         * Optional. Disabled by default - i.e. any peer attempt to measure distance to this device
+         * will be refused and discovery will proceed without ranging constraints.
+         * <p>
+         * The device must support Wi-Fi RTT for this feature to be used. Feature support is checked
+         * as described in {@link android.net.wifi.rtt}.
+         *
+         * @param enable If true, ranging is supported on request of the peer.
+         *
+         * @return The builder to facilitate chaining
+         *         {@code builder.setXXX(..).setXXX(..)}.
+         */
+        public Builder setRangingEnabled(boolean enable) {
+            mEnableRanging = enable;
+            return this;
+        }
+
+        /**
+         * Build {@link PublishConfig} given the current requests made on the
+         * builder.
+         */
+        public PublishConfig build() {
+            return new PublishConfig(mServiceName, mServiceSpecificInfo, mMatchFilter, mPublishType,
+                    mTtlSec, mEnableTerminateNotification, mEnableRanging);
+        }
+    }
+}
diff --git a/android/net/wifi/aware/PublishDiscoverySession.java b/android/net/wifi/aware/PublishDiscoverySession.java
new file mode 100644
index 0000000..1c99c87
--- /dev/null
+++ b/android/net/wifi/aware/PublishDiscoverySession.java
@@ -0,0 +1,68 @@
+/*
+ * 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.wifi.aware;
+
+import android.annotation.NonNull;
+import android.util.Log;
+
+/**
+ * A class representing a Aware publish session. Created when
+ * {@link WifiAwareSession#publish(PublishConfig, DiscoverySessionCallback,
+ * android.os.Handler)} is called and a discovery session is created and returned in
+ * {@link DiscoverySessionCallback#onPublishStarted(PublishDiscoverySession)}. See
+ * baseline functionality of all discovery sessions in {@link DiscoverySession}. This
+ * object allows updating an existing/running publish discovery session using
+ * {@link #updatePublish(PublishConfig)}.
+ */
+public class PublishDiscoverySession extends DiscoverySession {
+    private static final String TAG = "PublishDiscoverySession";
+
+    /** @hide */
+    public PublishDiscoverySession(WifiAwareManager manager, int clientId, int sessionId) {
+        super(manager, clientId, sessionId);
+    }
+
+    /**
+     * Re-configure the currently active publish session. The
+     * {@link DiscoverySessionCallback} is not replaced - the same listener used
+     * at creation is still used. The results of the configuration are returned using
+     * {@link DiscoverySessionCallback}:
+     * <ul>
+     *     <li>{@link DiscoverySessionCallback#onSessionConfigUpdated()}: configuration
+     *     update succeeded.
+     *     <li>{@link DiscoverySessionCallback#onSessionConfigFailed()}: configuration
+     *     update failed. The publish discovery session is still running using its previous
+     *     configuration (i.e. update failure does not terminate the session).
+     * </ul>
+     *
+     * @param publishConfig The new discovery publish session configuration ({@link PublishConfig}).
+     */
+    public void updatePublish(@NonNull PublishConfig publishConfig) {
+        if (mTerminated) {
+            Log.w(TAG, "updatePublish: called on terminated session");
+            return;
+        } else {
+            WifiAwareManager mgr = mMgr.get();
+            if (mgr == null) {
+                Log.w(TAG, "updatePublish: called post GC on WifiAwareManager");
+                return;
+            }
+
+            mgr.updatePublish(mClientId, mSessionId, publishConfig);
+        }
+    }
+}
diff --git a/android/net/wifi/aware/SubscribeConfig.java b/android/net/wifi/aware/SubscribeConfig.java
new file mode 100644
index 0000000..f0f7581
--- /dev/null
+++ b/android/net/wifi/aware/SubscribeConfig.java
@@ -0,0 +1,498 @@
+/*
+ * 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.wifi.aware;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import libcore.util.HexEncoding;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Defines the configuration of a Aware subscribe session. Built using
+ * {@link SubscribeConfig.Builder}. Subscribe is done using
+ * {@link WifiAwareSession#subscribe(SubscribeConfig, DiscoverySessionCallback,
+ * android.os.Handler)} or
+ * {@link SubscribeDiscoverySession#updateSubscribe(SubscribeConfig)}.
+ */
+public final class SubscribeConfig implements Parcelable {
+    /** @hide */
+    @IntDef({
+            SUBSCRIBE_TYPE_PASSIVE, SUBSCRIBE_TYPE_ACTIVE })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface SubscribeTypes {
+    }
+
+    /**
+     * Defines a passive subscribe session - a subscribe session where
+     * subscribe packets are not transmitted over-the-air and the device listens
+     * and matches to transmitted publish packets. Configuration is done using
+     * {@link SubscribeConfig.Builder#setSubscribeType(int)}.
+     */
+    public static final int SUBSCRIBE_TYPE_PASSIVE = 0;
+
+    /**
+     * Defines an active subscribe session - a subscribe session where
+     * subscribe packets are transmitted over-the-air. Configuration is done
+     * using {@link SubscribeConfig.Builder#setSubscribeType(int)}.
+     */
+    public static final int SUBSCRIBE_TYPE_ACTIVE = 1;
+
+    /** @hide */
+    public final byte[] mServiceName;
+
+    /** @hide */
+    public final byte[] mServiceSpecificInfo;
+
+    /** @hide */
+    public final byte[] mMatchFilter;
+
+    /** @hide */
+    public final int mSubscribeType;
+
+    /** @hide */
+    public final int mTtlSec;
+
+    /** @hide */
+    public final boolean mEnableTerminateNotification;
+
+    /** @hide */
+    public final boolean mMinDistanceMmSet;
+
+    /** @hide */
+    public final int mMinDistanceMm;
+
+    /** @hide */
+    public final boolean mMaxDistanceMmSet;
+
+    /** @hide */
+    public final int mMaxDistanceMm;
+
+    /** @hide */
+    public SubscribeConfig(byte[] serviceName, byte[] serviceSpecificInfo, byte[] matchFilter,
+            int subscribeType, int ttlSec, boolean enableTerminateNotification,
+            boolean minDistanceMmSet, int minDistanceMm, boolean maxDistanceMmSet,
+            int maxDistanceMm) {
+        mServiceName = serviceName;
+        mServiceSpecificInfo = serviceSpecificInfo;
+        mMatchFilter = matchFilter;
+        mSubscribeType = subscribeType;
+        mTtlSec = ttlSec;
+        mEnableTerminateNotification = enableTerminateNotification;
+        mMinDistanceMm = minDistanceMm;
+        mMinDistanceMmSet = minDistanceMmSet;
+        mMaxDistanceMm = maxDistanceMm;
+        mMaxDistanceMmSet = maxDistanceMmSet;
+    }
+
+    @Override
+    public String toString() {
+        return "SubscribeConfig [mServiceName='" + (mServiceName == null ? "<null>"
+                : String.valueOf(HexEncoding.encode(mServiceName))) + ", mServiceName.length=" + (
+                mServiceName == null ? 0 : mServiceName.length) + ", mServiceSpecificInfo='" + (
+                (mServiceSpecificInfo == null) ? "<null>" : String.valueOf(
+                        HexEncoding.encode(mServiceSpecificInfo)))
+                + ", mServiceSpecificInfo.length=" + (mServiceSpecificInfo == null ? 0
+                : mServiceSpecificInfo.length) + ", mMatchFilter="
+                + (new TlvBufferUtils.TlvIterable(0, 1, mMatchFilter)).toString()
+                + ", mMatchFilter.length=" + (mMatchFilter == null ? 0 : mMatchFilter.length)
+                + ", mSubscribeType=" + mSubscribeType + ", mTtlSec=" + mTtlSec
+                + ", mEnableTerminateNotification=" + mEnableTerminateNotification
+                + ", mMinDistanceMm=" + mMinDistanceMm
+                + ", mMinDistanceMmSet=" + mMinDistanceMmSet
+                + ", mMaxDistanceMm=" + mMaxDistanceMm
+                + ", mMaxDistanceMmSet=" + mMaxDistanceMmSet + "]";
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeByteArray(mServiceName);
+        dest.writeByteArray(mServiceSpecificInfo);
+        dest.writeByteArray(mMatchFilter);
+        dest.writeInt(mSubscribeType);
+        dest.writeInt(mTtlSec);
+        dest.writeInt(mEnableTerminateNotification ? 1 : 0);
+        dest.writeInt(mMinDistanceMm);
+        dest.writeInt(mMinDistanceMmSet ? 1 : 0);
+        dest.writeInt(mMaxDistanceMm);
+        dest.writeInt(mMaxDistanceMmSet ? 1 : 0);
+    }
+
+    public static final @android.annotation.NonNull Creator<SubscribeConfig> CREATOR = new Creator<SubscribeConfig>() {
+        @Override
+        public SubscribeConfig[] newArray(int size) {
+            return new SubscribeConfig[size];
+        }
+
+        @Override
+        public SubscribeConfig createFromParcel(Parcel in) {
+            byte[] serviceName = in.createByteArray();
+            byte[] ssi = in.createByteArray();
+            byte[] matchFilter = in.createByteArray();
+            int subscribeType = in.readInt();
+            int ttlSec = in.readInt();
+            boolean enableTerminateNotification = in.readInt() != 0;
+            int minDistanceMm = in.readInt();
+            boolean minDistanceMmSet = in.readInt() != 0;
+            int maxDistanceMm = in.readInt();
+            boolean maxDistanceMmSet = in.readInt() != 0;
+
+            return new SubscribeConfig(serviceName, ssi, matchFilter, subscribeType, ttlSec,
+                    enableTerminateNotification, minDistanceMmSet, minDistanceMm, maxDistanceMmSet,
+                    maxDistanceMm);
+        }
+    };
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+
+        if (!(o instanceof SubscribeConfig)) {
+            return false;
+        }
+
+        SubscribeConfig lhs = (SubscribeConfig) o;
+
+        if (!(Arrays.equals(mServiceName, lhs.mServiceName) && Arrays.equals(
+                mServiceSpecificInfo, lhs.mServiceSpecificInfo) && Arrays.equals(mMatchFilter,
+                lhs.mMatchFilter) && mSubscribeType == lhs.mSubscribeType && mTtlSec == lhs.mTtlSec
+                && mEnableTerminateNotification == lhs.mEnableTerminateNotification
+                && mMinDistanceMmSet == lhs.mMinDistanceMmSet
+                && mMaxDistanceMmSet == lhs.mMaxDistanceMmSet)) {
+            return false;
+        }
+
+        if (mMinDistanceMmSet && mMinDistanceMm != lhs.mMinDistanceMm) {
+            return false;
+        }
+
+        if (mMaxDistanceMmSet && mMaxDistanceMm != lhs.mMaxDistanceMm) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = Objects.hash(Arrays.hashCode(mServiceName),
+                Arrays.hashCode(mServiceSpecificInfo), Arrays.hashCode(mMatchFilter),
+                mSubscribeType, mTtlSec, mEnableTerminateNotification, mMinDistanceMmSet,
+                mMaxDistanceMmSet);
+
+        if (mMinDistanceMmSet) {
+            result = Objects.hash(result, mMinDistanceMm);
+        }
+        if (mMaxDistanceMmSet) {
+            result = Objects.hash(result, mMaxDistanceMm);
+        }
+
+        return result;
+    }
+
+    /**
+     * Verifies that the contents of the SubscribeConfig are valid. Otherwise
+     * throws an IllegalArgumentException.
+     *
+     * @hide
+     */
+    public void assertValid(Characteristics characteristics, boolean rttSupported)
+            throws IllegalArgumentException {
+        WifiAwareUtils.validateServiceName(mServiceName);
+
+        if (!TlvBufferUtils.isValid(mMatchFilter, 0, 1)) {
+            throw new IllegalArgumentException(
+                    "Invalid matchFilter configuration - LV fields do not match up to length");
+        }
+        if (mSubscribeType < SUBSCRIBE_TYPE_PASSIVE || mSubscribeType > SUBSCRIBE_TYPE_ACTIVE) {
+            throw new IllegalArgumentException("Invalid subscribeType - " + mSubscribeType);
+        }
+        if (mTtlSec < 0) {
+            throw new IllegalArgumentException("Invalid ttlSec - must be non-negative");
+        }
+
+        if (characteristics != null) {
+            int maxServiceNameLength = characteristics.getMaxServiceNameLength();
+            if (maxServiceNameLength != 0 && mServiceName.length > maxServiceNameLength) {
+                throw new IllegalArgumentException(
+                        "Service name longer than supported by device characteristics");
+            }
+            int maxServiceSpecificInfoLength = characteristics.getMaxServiceSpecificInfoLength();
+            if (maxServiceSpecificInfoLength != 0 && mServiceSpecificInfo != null
+                    && mServiceSpecificInfo.length > maxServiceSpecificInfoLength) {
+                throw new IllegalArgumentException(
+                        "Service specific info longer than supported by device characteristics");
+            }
+            int maxMatchFilterLength = characteristics.getMaxMatchFilterLength();
+            if (maxMatchFilterLength != 0 && mMatchFilter != null
+                    && mMatchFilter.length > maxMatchFilterLength) {
+                throw new IllegalArgumentException(
+                        "Match filter longer than supported by device characteristics");
+            }
+        }
+
+        if (mMinDistanceMmSet && mMinDistanceMm < 0) {
+            throw new IllegalArgumentException("Minimum distance must be non-negative");
+        }
+        if (mMaxDistanceMmSet && mMaxDistanceMm < 0) {
+            throw new IllegalArgumentException("Maximum distance must be non-negative");
+        }
+        if (mMinDistanceMmSet && mMaxDistanceMmSet && mMaxDistanceMm <= mMinDistanceMm) {
+            throw new IllegalArgumentException(
+                    "Maximum distance must be greater than minimum distance");
+        }
+
+        if (!rttSupported && (mMinDistanceMmSet || mMaxDistanceMmSet)) {
+            throw new IllegalArgumentException("Ranging is not supported");
+        }
+    }
+
+    /**
+     * Builder used to build {@link SubscribeConfig} objects.
+     */
+    public static final class Builder {
+        private byte[] mServiceName;
+        private byte[] mServiceSpecificInfo;
+        private byte[] mMatchFilter;
+        private int mSubscribeType = SUBSCRIBE_TYPE_PASSIVE;
+        private int mTtlSec = 0;
+        private boolean mEnableTerminateNotification = true;
+        private boolean mMinDistanceMmSet = false;
+        private int mMinDistanceMm;
+        private boolean mMaxDistanceMmSet = false;
+        private int mMaxDistanceMm;
+
+        /**
+         * Specify the service name of the subscribe session. The actual on-air
+         * value is a 6 byte hashed representation of this string.
+         * <p>
+         * The Service Name is a UTF-8 encoded string from 1 to 255 bytes in length.
+         * The only acceptable single-byte UTF-8 symbols for a Service Name are alphanumeric
+         * values (A-Z, a-z, 0-9), the hyphen ('-'), and the period ('.'). All valid multi-byte
+         * UTF-8 characters are acceptable in a Service Name.
+         * <p>
+         * Must be called - an empty ServiceName is not valid.
+         *
+         * @param serviceName The service name for the subscribe session.
+         *
+         * @return The builder to facilitate chaining
+         *         {@code builder.setXXX(..).setXXX(..)}.
+         */
+        public Builder setServiceName(@NonNull String serviceName) {
+            if (serviceName == null) {
+                throw new IllegalArgumentException("Invalid service name - must be non-null");
+            }
+            mServiceName = serviceName.getBytes(StandardCharsets.UTF_8);
+            return this;
+        }
+
+        /**
+         * Specify service specific information for the subscribe session. This is
+         * a free-form byte array available to the application to send
+         * additional information as part of the discovery operation - i.e. it
+         * will not be used to determine whether a publish/subscribe match
+         * occurs.
+         * <p>
+         *     Optional. Empty by default.
+         *
+         * @param serviceSpecificInfo A byte-array for the service-specific
+         *            information field.
+         *
+         * @return The builder to facilitate chaining
+         *         {@code builder.setXXX(..).setXXX(..)}.
+         */
+        public Builder setServiceSpecificInfo(@Nullable byte[] serviceSpecificInfo) {
+            mServiceSpecificInfo = serviceSpecificInfo;
+            return this;
+        }
+
+        /**
+         * The match filter for a subscribe session. Used to determine whether a service
+         * discovery occurred - in addition to relying on the service name.
+         * <p>
+         *     Optional. Empty by default.
+         *
+         * @param matchFilter A list of match filter entries (each of which is an arbitrary byte
+         *                    array).
+         *
+         * @return The builder to facilitate chaining
+         *         {@code builder.setXXX(..).setXXX(..)}.
+         */
+        public Builder setMatchFilter(@Nullable List<byte[]> matchFilter) {
+            mMatchFilter = new TlvBufferUtils.TlvConstructor(0, 1).allocateAndPut(
+                    matchFilter).getArray();
+            return this;
+        }
+
+        /**
+         * Sets the type of the subscribe session: active (subscribe packets are
+         * transmitted over-the-air), or passive (no subscribe packets are
+         * transmitted, a match is made against a solicited/active publish
+         * session whose packets are transmitted over-the-air).
+         *
+         * @param subscribeType Subscribe session type:
+         *            {@link SubscribeConfig#SUBSCRIBE_TYPE_ACTIVE} or
+         *            {@link SubscribeConfig#SUBSCRIBE_TYPE_PASSIVE}.
+         *
+         * @return The builder to facilitate chaining
+         *         {@code builder.setXXX(..).setXXX(..)}.
+         */
+        public Builder setSubscribeType(@SubscribeTypes int subscribeType) {
+            if (subscribeType < SUBSCRIBE_TYPE_PASSIVE || subscribeType > SUBSCRIBE_TYPE_ACTIVE) {
+                throw new IllegalArgumentException("Invalid subscribeType - " + subscribeType);
+            }
+            mSubscribeType = subscribeType;
+            return this;
+        }
+
+        /**
+         * Sets the time interval (in seconds) an active (
+         * {@link SubscribeConfig.Builder#setSubscribeType(int)}) subscribe session
+         * will be alive - i.e. broadcasting a packet. When the TTL is reached
+         * an event will be generated for
+         * {@link DiscoverySessionCallback#onSessionTerminated()}.
+         * <p>
+         *     Optional. 0 by default - indicating the session doesn't terminate on its own.
+         *     Session will be terminated when {@link DiscoverySession#close()} is
+         *     called.
+         *
+         * @param ttlSec Lifetime of a subscribe session in seconds.
+         *
+         * @return The builder to facilitate chaining
+         *         {@code builder.setXXX(..).setXXX(..)}.
+         */
+        public Builder setTtlSec(int ttlSec) {
+            if (ttlSec < 0) {
+                throw new IllegalArgumentException("Invalid ttlSec - must be non-negative");
+            }
+            mTtlSec = ttlSec;
+            return this;
+        }
+
+        /**
+         * Configure whether a subscribe terminate notification
+         * {@link DiscoverySessionCallback#onSessionTerminated()} is reported
+         * back to the callback.
+         *
+         * @param enable If true the terminate callback will be called when the
+         *            subscribe is terminated. Otherwise it will not be called.
+         *
+         * @return The builder to facilitate chaining
+         *         {@code builder.setXXX(..).setXXX(..)}.
+         */
+        public Builder setTerminateNotificationEnabled(boolean enable) {
+            mEnableTerminateNotification = enable;
+            return this;
+        }
+
+        /**
+         * Configure the minimum distance to a discovered publisher at which to trigger a discovery
+         * notification. I.e. discovery will be triggered if we've found a matching publisher
+         * (based on the other criteria in this configuration) <b>and</b> the distance to the
+         * publisher is larger than the value specified in this API. Can be used in conjunction with
+         * {@link #setMaxDistanceMm(int)} to specify a geofence, i.e. discovery with min <=
+         * distance <= max.
+         * <p>
+         * For ranging to be used in discovery it must also be enabled on the publisher using
+         * {@link PublishConfig.Builder#setRangingEnabled(boolean)}. However, ranging may
+         * not be available or enabled on the publisher or may be temporarily disabled on either
+         * subscriber or publisher - in such cases discovery will proceed without ranging.
+         * <p>
+         * When ranging is enabled and available on both publisher and subscriber and a service
+         * is discovered based on geofence constraints the
+         * {@link DiscoverySessionCallback#onServiceDiscoveredWithinRange(PeerHandle, byte[], List, int)}
+         * is called, otherwise the
+         * {@link DiscoverySessionCallback#onServiceDiscovered(PeerHandle, byte[], List)}
+         * is called.
+         * <p>
+         * The device must support Wi-Fi RTT for this feature to be used. Feature support is checked
+         * as described in {@link android.net.wifi.rtt}.
+         *
+         * @param minDistanceMm Minimum distance, in mm, to the publisher above which to trigger
+         *                      discovery.
+         *
+         * @return The builder to facilitate chaining
+         *         {@code builder.setXXX(..).setXXX(..)}.
+         */
+        public Builder setMinDistanceMm(int minDistanceMm) {
+            mMinDistanceMm = minDistanceMm;
+            mMinDistanceMmSet = true;
+            return this;
+        }
+
+        /**
+         * Configure the maximum distance to a discovered publisher at which to trigger a discovery
+         * notification. I.e. discovery will be triggered if we've found a matching publisher
+         * (based on the other criteria in this configuration) <b>and</b> the distance to the
+         * publisher is smaller than the value specified in this API. Can be used in conjunction
+         * with {@link #setMinDistanceMm(int)} to specify a geofence, i.e. discovery with min <=
+         * distance <= max.
+         * <p>
+         * For ranging to be used in discovery it must also be enabled on the publisher using
+         * {@link PublishConfig.Builder#setRangingEnabled(boolean)}. However, ranging may
+         * not be available or enabled on the publisher or may be temporarily disabled on either
+         * subscriber or publisher - in such cases discovery will proceed without ranging.
+         * <p>
+         * When ranging is enabled and available on both publisher and subscriber and a service
+         * is discovered based on geofence constraints the
+         * {@link DiscoverySessionCallback#onServiceDiscoveredWithinRange(PeerHandle, byte[], List, int)}
+         * is called, otherwise the
+         * {@link DiscoverySessionCallback#onServiceDiscovered(PeerHandle, byte[], List)}
+         * is called.
+         * <p>
+         * The device must support Wi-Fi RTT for this feature to be used. Feature support is checked
+         * as described in {@link android.net.wifi.rtt}.
+         *
+         * @param maxDistanceMm Maximum distance, in mm, to the publisher below which to trigger
+         *                      discovery.
+         *
+         * @return The builder to facilitate chaining
+         *         {@code builder.setXXX(..).setXXX(..)}.
+         */
+        public Builder setMaxDistanceMm(int maxDistanceMm) {
+            mMaxDistanceMm = maxDistanceMm;
+            mMaxDistanceMmSet = true;
+            return this;
+        }
+
+        /**
+         * Build {@link SubscribeConfig} given the current requests made on the
+         * builder.
+         */
+        public SubscribeConfig build() {
+            return new SubscribeConfig(mServiceName, mServiceSpecificInfo, mMatchFilter,
+                    mSubscribeType, mTtlSec, mEnableTerminateNotification,
+                    mMinDistanceMmSet, mMinDistanceMm, mMaxDistanceMmSet, mMaxDistanceMm);
+        }
+    }
+}
diff --git a/android/net/wifi/aware/SubscribeDiscoverySession.java b/android/net/wifi/aware/SubscribeDiscoverySession.java
new file mode 100644
index 0000000..ca88a90
--- /dev/null
+++ b/android/net/wifi/aware/SubscribeDiscoverySession.java
@@ -0,0 +1,73 @@
+/*
+ * 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.wifi.aware;
+
+import android.annotation.NonNull;
+import android.util.Log;
+
+/**
+ * A class representing a Aware subscribe session. Created when
+ * {@link WifiAwareSession#subscribe(SubscribeConfig,
+ * DiscoverySessionCallback, android.os.Handler)}
+ * is called and a discovery session is created and returned in
+ * {@link DiscoverySessionCallback#onSubscribeStarted(SubscribeDiscoverySession)}.
+ * See baseline functionality of all discovery sessions in {@link DiscoverySession}.
+ * This object allows updating an existing/running subscribe discovery session using
+ * {@link #updateSubscribe(SubscribeConfig)}.
+ */
+public class SubscribeDiscoverySession extends DiscoverySession {
+    private static final String TAG = "SubscribeDiscSession";
+
+    /**
+     * {@hide}
+     */
+    public SubscribeDiscoverySession(WifiAwareManager manager, int clientId,
+            int sessionId) {
+        super(manager, clientId, sessionId);
+    }
+
+    /**
+     * Re-configure the currently active subscribe session. The
+     * {@link DiscoverySessionCallback} is not replaced - the same listener used
+     * at creation is still used. The results of the configuration are returned using
+     * {@link DiscoverySessionCallback}:
+     * <ul>
+     *     <li>{@link DiscoverySessionCallback#onSessionConfigUpdated()}: configuration
+     *     update succeeded.
+     *     <li>{@link DiscoverySessionCallback#onSessionConfigFailed()}: configuration
+     *     update failed. The subscribe discovery session is still running using its previous
+     *     configuration (i.e. update failure does not terminate the session).
+     * </ul>
+     *
+     * @param subscribeConfig The new discovery subscribe session configuration
+     *                        ({@link SubscribeConfig}).
+     */
+    public void updateSubscribe(@NonNull SubscribeConfig subscribeConfig) {
+        if (mTerminated) {
+            Log.w(TAG, "updateSubscribe: called on terminated session");
+            return;
+        } else {
+            WifiAwareManager mgr = mMgr.get();
+            if (mgr == null) {
+                Log.w(TAG, "updateSubscribe: called post GC on WifiAwareManager");
+                return;
+            }
+
+            mgr.updateSubscribe(mClientId, mSessionId, subscribeConfig);
+        }
+    }
+}
diff --git a/android/net/wifi/aware/TlvBufferUtils.java b/android/net/wifi/aware/TlvBufferUtils.java
new file mode 100644
index 0000000..b3b5b29
--- /dev/null
+++ b/android/net/wifi/aware/TlvBufferUtils.java
@@ -0,0 +1,670 @@
+/*
+ * 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.wifi.aware;
+
+import android.annotation.Nullable;
+
+import libcore.io.Memory;
+
+import java.nio.BufferOverflowException;
+import java.nio.ByteOrder;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+
+/**
+ * Utility class to construct and parse byte arrays using the TLV format -
+ * Type/Length/Value format. The utilities accept a configuration of the size of
+ * the Type field and the Length field. A Type field size of 0 is allowed -
+ * allowing usage for LV (no T) array formats.
+ *
+ * @hide
+ */
+public class TlvBufferUtils {
+    private TlvBufferUtils() {
+        // no reason to ever create this class
+    }
+
+    /**
+     * Utility class to construct byte arrays using the TLV format -
+     * Type/Length/Value.
+     * <p>
+     * A constructor is created specifying the size of the Type (T) and Length
+     * (L) fields. A specification of zero size T field is allowed - resulting
+     * in LV type format.
+     * <p>
+     * The byte array is either provided (using
+     * {@link TlvConstructor#wrap(byte[])}) or allocated (using
+     * {@link TlvConstructor#allocate(int)}).
+     * <p>
+     * Values are added to the structure using the {@code TlvConstructor.put*()}
+     * methods.
+     * <p>
+     * The final byte array is obtained using {@link TlvConstructor#getArray()}.
+     */
+    public static class TlvConstructor {
+        private int mTypeSize;
+        private int mLengthSize;
+        private ByteOrder mByteOrder = ByteOrder.BIG_ENDIAN;
+
+        private byte[] mArray;
+        private int mArrayLength;
+        private int mPosition;
+
+        /**
+         * Define a TLV constructor with the specified size of the Type (T) and
+         * Length (L) fields.
+         *
+         * @param typeSize Number of bytes used for the Type (T) field. Values
+         *            of 0, 1, or 2 bytes are allowed. A specification of 0
+         *            bytes implies that the field being constructed has the LV
+         *            format rather than the TLV format.
+         * @param lengthSize Number of bytes used for the Length (L) field.
+         *            Values of 1 or 2 bytes are allowed.
+         */
+        public TlvConstructor(int typeSize, int lengthSize) {
+            if (typeSize < 0 || typeSize > 2 || lengthSize <= 0 || lengthSize > 2) {
+                throw new IllegalArgumentException(
+                        "Invalid sizes - typeSize=" + typeSize + ", lengthSize=" + lengthSize);
+            }
+            mTypeSize = typeSize;
+            mLengthSize = lengthSize;
+            mPosition = 0;
+        }
+
+        /**
+         * Configure the TLV constructor to use a particular byte order. Should be
+         * {@link ByteOrder#BIG_ENDIAN} (the default at construction) or
+         * {@link ByteOrder#LITTLE_ENDIAN}.
+         *
+         * @return The constructor to facilitate chaining
+         *         {@code ctr.putXXX(..).putXXX(..)}.
+         */
+        public TlvConstructor setByteOrder(ByteOrder byteOrder) {
+            mByteOrder = byteOrder;
+            return this;
+        }
+
+        /**
+         * Set the byte array to be used to construct the TLV.
+         *
+         * @param array Byte array to be formatted.
+         * @return The constructor to facilitate chaining
+         *         {@code ctr.putXXX(..).putXXX(..)}.
+         */
+        public TlvConstructor wrap(@Nullable byte[] array) {
+            mArray = array;
+            mArrayLength = (array == null) ? 0 : array.length;
+            mPosition = 0;
+            return this;
+        }
+
+        /**
+         * Allocates a new byte array to be used ot construct a TLV.
+         *
+         * @param capacity The size of the byte array to be allocated.
+         * @return The constructor to facilitate chaining
+         *         {@code ctr.putXXX(..).putXXX(..)}.
+         */
+        public TlvConstructor allocate(int capacity) {
+            mArray = new byte[capacity];
+            mArrayLength = capacity;
+            mPosition = 0;
+            return this;
+        }
+
+        /**
+         * Creates a TLV array (of the previously specified Type and Length sizes) from the input
+         * list. Allocates an array matching the contents (and required Type and Length
+         * fields), copies the contents, and set the Length fields. The Type field is set to 0.
+         *
+         * @param list A list of fields to be added to the TLV buffer.
+         * @return The constructor of the TLV.
+         */
+        public TlvConstructor allocateAndPut(@Nullable List<byte[]> list) {
+            if (list != null) {
+                int size = 0;
+                for (byte[] field : list) {
+                    size += mTypeSize + mLengthSize;
+                    if (field != null) {
+                        size += field.length;
+                    }
+                }
+                allocate(size);
+                for (byte[] field : list) {
+                    putByteArray(0, field);
+                }
+            }
+            return this;
+        }
+
+        /**
+         * Copies a byte into the TLV with the indicated type. For an LV
+         * formatted structure (i.e. typeLength=0 in {@link TlvConstructor
+         * TlvConstructor(int, int)} ) the type field is ignored.
+         *
+         * @param type The value to be placed into the Type field.
+         * @param b The byte to be inserted into the structure.
+         * @return The constructor to facilitate chaining
+         *         {@code ctr.putXXX(..).putXXX(..)}.
+         */
+        public TlvConstructor putByte(int type, byte b) {
+            checkLength(1);
+            addHeader(type, 1);
+            mArray[mPosition++] = b;
+            return this;
+        }
+
+        /**
+         * Copies a raw byte into the TLV buffer - without a type or a length.
+         *
+         * @param b The byte to be inserted into the structure.
+         * @return The constructor to facilitate chaining {@code cts.putXXX(..).putXXX(..)}.
+         */
+        public TlvConstructor putRawByte(byte b) {
+            checkRawLength(1);
+            mArray[mPosition++] = b;
+            return this;
+        }
+
+        /**
+         * Copies a byte array into the TLV with the indicated type. For an LV
+         * formatted structure (i.e. typeLength=0 in {@link TlvConstructor
+         * TlvConstructor(int, int)} ) the type field is ignored.
+         *
+         * @param type The value to be placed into the Type field.
+         * @param array The array to be copied into the TLV structure.
+         * @param offset Start copying from the array at the specified offset.
+         * @param length Copy the specified number (length) of bytes from the
+         *            array.
+         * @return The constructor to facilitate chaining
+         *         {@code ctr.putXXX(..).putXXX(..)}.
+         */
+        public TlvConstructor putByteArray(int type, @Nullable byte[] array, int offset,
+                int length) {
+            checkLength(length);
+            addHeader(type, length);
+            if (length != 0) {
+                System.arraycopy(array, offset, mArray, mPosition, length);
+            }
+            mPosition += length;
+            return this;
+        }
+
+        /**
+         * Copies a byte array into the TLV with the indicated type. For an LV
+         * formatted structure (i.e. typeLength=0 in {@link TlvConstructor
+         * TlvConstructor(int, int)} ) the type field is ignored.
+         *
+         * @param type The value to be placed into the Type field.
+         * @param array The array to be copied (in full) into the TLV structure.
+         * @return The constructor to facilitate chaining
+         *         {@code ctr.putXXX(..).putXXX(..)}.
+         */
+        public TlvConstructor putByteArray(int type, @Nullable byte[] array) {
+            return putByteArray(type, array, 0, (array == null) ? 0 : array.length);
+        }
+
+        /**
+         * Copies a byte array into the TLV - without a type or a length.
+         *
+         * @param array The array to be copied (in full) into the TLV structure.
+         * @return The constructor to facilitate chaining
+         *         {@code ctr.putXXX(..).putXXX(..)}.
+         */
+        public TlvConstructor putRawByteArray(@Nullable byte[] array) {
+            if (array == null) return this;
+
+            checkRawLength(array.length);
+            System.arraycopy(array, 0, mArray, mPosition, array.length);
+            mPosition += array.length;
+            return this;
+        }
+
+        /**
+         * Places a zero length element (i.e. Length field = 0) into the TLV.
+         * For an LV formatted structure (i.e. typeLength=0 in
+         * {@link TlvConstructor TlvConstructor(int, int)} ) the type field is
+         * ignored.
+         *
+         * @param type The value to be placed into the Type field.
+         * @return The constructor to facilitate chaining
+         *         {@code ctr.putXXX(..).putXXX(..)}.
+         */
+        public TlvConstructor putZeroLengthElement(int type) {
+            checkLength(0);
+            addHeader(type, 0);
+            return this;
+        }
+
+        /**
+         * Copies short into the TLV with the indicated type. For an LV
+         * formatted structure (i.e. typeLength=0 in {@link TlvConstructor
+         * TlvConstructor(int, int)} ) the type field is ignored.
+         *
+         * @param type The value to be placed into the Type field.
+         * @param data The short to be inserted into the structure.
+         * @return The constructor to facilitate chaining
+         *         {@code ctr.putXXX(..).putXXX(..)}.
+         */
+        public TlvConstructor putShort(int type, short data) {
+            checkLength(2);
+            addHeader(type, 2);
+            Memory.pokeShort(mArray, mPosition, data, mByteOrder);
+            mPosition += 2;
+            return this;
+        }
+
+        /**
+         * Copies integer into the TLV with the indicated type. For an LV
+         * formatted structure (i.e. typeLength=0 in {@link TlvConstructor
+         * TlvConstructor(int, int)} ) the type field is ignored.
+         *
+         * @param type The value to be placed into the Type field.
+         * @param data The integer to be inserted into the structure.
+         * @return The constructor to facilitate chaining
+         *         {@code ctr.putXXX(..).putXXX(..)}.
+         */
+        public TlvConstructor putInt(int type, int data) {
+            checkLength(4);
+            addHeader(type, 4);
+            Memory.pokeInt(mArray, mPosition, data, mByteOrder);
+            mPosition += 4;
+            return this;
+        }
+
+        /**
+         * Copies a String's byte representation into the TLV with the indicated
+         * type. For an LV formatted structure (i.e. typeLength=0 in
+         * {@link TlvConstructor TlvConstructor(int, int)} ) the type field is
+         * ignored.
+         *
+         * @param type The value to be placed into the Type field.
+         * @param data The string whose bytes are to be inserted into the
+         *            structure.
+         * @return The constructor to facilitate chaining
+         *         {@code ctr.putXXX(..).putXXX(..)}.
+         */
+        public TlvConstructor putString(int type, @Nullable String data) {
+            byte[] bytes = null;
+            int length = 0;
+            if (data != null) {
+                bytes = data.getBytes();
+                length = bytes.length;
+            }
+            return putByteArray(type, bytes, 0, length);
+        }
+
+        /**
+         * Returns the constructed TLV formatted byte-array. This array is a copy of the wrapped
+         * or allocated array - truncated to just the significant bytes - i.e. those written into
+         * the (T)LV.
+         *
+         * @return The byte array containing the TLV formatted structure.
+         */
+        public byte[] getArray() {
+            return Arrays.copyOf(mArray, getActualLength());
+        }
+
+        /**
+         * Returns the size of the TLV formatted portion of the wrapped or
+         * allocated byte array. The array itself is returned with
+         * {@link TlvConstructor#getArray()}.
+         *
+         * @return The size of the TLV formatted portion of the byte array.
+         */
+        private int getActualLength() {
+            return mPosition;
+        }
+
+        private void checkLength(int dataLength) {
+            if (mPosition + mTypeSize + mLengthSize + dataLength > mArrayLength) {
+                throw new BufferOverflowException();
+            }
+        }
+
+        private void checkRawLength(int dataLength) {
+            if (mPosition + dataLength > mArrayLength) {
+                throw new BufferOverflowException();
+            }
+        }
+
+        private void addHeader(int type, int length) {
+            if (mTypeSize == 1) {
+                mArray[mPosition] = (byte) type;
+            } else if (mTypeSize == 2) {
+                Memory.pokeShort(mArray, mPosition, (short) type, mByteOrder);
+            }
+            mPosition += mTypeSize;
+
+            if (mLengthSize == 1) {
+                mArray[mPosition] = (byte) length;
+            } else if (mLengthSize == 2) {
+                Memory.pokeShort(mArray, mPosition, (short) length, mByteOrder);
+            }
+            mPosition += mLengthSize;
+        }
+    }
+
+    /**
+     * Utility class used when iterating over a TLV formatted byte-array. Use
+     * {@link TlvIterable} to iterate over array. A {@link TlvElement}
+     * represents each entry in a TLV formatted byte-array.
+     */
+    public static class TlvElement {
+        /**
+         * The Type (T) field of the current TLV element. Note that for LV
+         * formatted byte-arrays (i.e. TLV whose Type/T size is 0) the value of
+         * this field is undefined.
+         */
+        public int type;
+
+        /**
+         * The Length (L) field of the current TLV element.
+         */
+        public int length;
+
+        /**
+         * Control of the endianess of the TLV element - true for big-endian, false for little-
+         * endian.
+         */
+        public ByteOrder byteOrder = ByteOrder.BIG_ENDIAN;
+
+        /**
+         * The Value (V) field - a raw byte array representing the current TLV
+         * element where the entry starts at {@link TlvElement#offset}.
+         */
+        private byte[] mRefArray;
+
+        /**
+         * The offset to be used into {@link TlvElement#mRefArray} to access the
+         * raw data representing the current TLV element.
+         */
+        public int offset;
+
+        private TlvElement(int type, int length, @Nullable byte[] refArray, int offset) {
+            this.type = type;
+            this.length = length;
+            mRefArray = refArray;
+            this.offset = offset;
+
+            if (offset + length > refArray.length) {
+                throw new BufferOverflowException();
+            }
+        }
+
+        /**
+         * Return the raw byte array of the Value (V) field.
+         *
+         * @return The Value (V) field as a byte array.
+         */
+        public byte[] getRawData() {
+            return Arrays.copyOfRange(mRefArray, offset, offset + length);
+        }
+
+        /**
+         * Utility function to return a byte representation of a TLV element of
+         * length 1. Note: an attempt to call this function on a TLV item whose
+         * {@link TlvElement#length} is != 1 will result in an exception.
+         *
+         * @return byte representation of current TLV element.
+         */
+        public byte getByte() {
+            if (length != 1) {
+                throw new IllegalArgumentException(
+                        "Accesing a byte from a TLV element of length " + length);
+            }
+            return mRefArray[offset];
+        }
+
+        /**
+         * Utility function to return a short representation of a TLV element of
+         * length 2. Note: an attempt to call this function on a TLV item whose
+         * {@link TlvElement#length} is != 2 will result in an exception.
+         *
+         * @return short representation of current TLV element.
+         */
+        public short getShort() {
+            if (length != 2) {
+                throw new IllegalArgumentException(
+                        "Accesing a short from a TLV element of length " + length);
+            }
+            return Memory.peekShort(mRefArray, offset, byteOrder);
+        }
+
+        /**
+         * Utility function to return an integer representation of a TLV element
+         * of length 4. Note: an attempt to call this function on a TLV item
+         * whose {@link TlvElement#length} is != 4 will result in an exception.
+         *
+         * @return integer representation of current TLV element.
+         */
+        public int getInt() {
+            if (length != 4) {
+                throw new IllegalArgumentException(
+                        "Accesing an int from a TLV element of length " + length);
+            }
+            return Memory.peekInt(mRefArray, offset, byteOrder);
+        }
+
+        /**
+         * Utility function to return a String representation of a TLV element.
+         *
+         * @return String repersentation of the current TLV element.
+         */
+        public String getString() {
+            return new String(mRefArray, offset, length);
+        }
+    }
+
+    /**
+     * Utility class to iterate over a TLV formatted byte-array.
+     */
+    public static class TlvIterable implements Iterable<TlvElement> {
+        private int mTypeSize;
+        private int mLengthSize;
+        private ByteOrder mByteOrder = ByteOrder.BIG_ENDIAN;
+        private byte[] mArray;
+        private int mArrayLength;
+
+        /**
+         * Constructs a TlvIterable object - specifying the format of the TLV
+         * (the sizes of the Type and Length fields), and the byte array whose
+         * data is to be parsed.
+         *
+         * @param typeSize Number of bytes used for the Type (T) field. Valid
+         *            values are 0 (i.e. indicating the format is LV rather than
+         *            TLV), 1, and 2 bytes.
+         * @param lengthSize Number of bytes used for the Length (L) field.
+         *            Values values are 1 or 2 bytes.
+         * @param array The TLV formatted byte-array to parse.
+         */
+        public TlvIterable(int typeSize, int lengthSize, @Nullable byte[] array) {
+            if (typeSize < 0 || typeSize > 2 || lengthSize <= 0 || lengthSize > 2) {
+                throw new IllegalArgumentException(
+                        "Invalid sizes - typeSize=" + typeSize + ", lengthSize=" + lengthSize);
+            }
+            mTypeSize = typeSize;
+            mLengthSize = lengthSize;
+            mArray = array;
+            mArrayLength = (array == null) ? 0 : array.length;
+        }
+
+        /**
+         * Configure the TLV iterator to use little-endian byte ordering.
+         */
+        public void setByteOrder(ByteOrder byteOrder) {
+            mByteOrder = byteOrder;
+        }
+
+        /**
+         * Prints out a parsed representation of the TLV-formatted byte array.
+         * Whenever possible bytes, shorts, and integer are printed out (for
+         * fields whose length is 1, 2, or 4 respectively).
+         */
+        @Override
+        public String toString() {
+            StringBuilder builder = new StringBuilder();
+
+            builder.append("[");
+            boolean first = true;
+            for (TlvElement tlv : this) {
+                if (!first) {
+                    builder.append(",");
+                }
+                first = false;
+                builder.append(" (");
+                if (mTypeSize != 0) {
+                    builder.append("T=" + tlv.type + ",");
+                }
+                builder.append("L=" + tlv.length + ") ");
+                if (tlv.length == 0) {
+                    builder.append("<null>");
+                } else if (tlv.length == 1) {
+                    builder.append(tlv.getByte());
+                } else if (tlv.length == 2) {
+                    builder.append(tlv.getShort());
+                } else if (tlv.length == 4) {
+                    builder.append(tlv.getInt());
+                } else {
+                    builder.append("<bytes>");
+                }
+                if (tlv.length != 0) {
+                    builder.append(" (S='" + tlv.getString() + "')");
+                }
+            }
+            builder.append("]");
+
+            return builder.toString();
+        }
+
+        /**
+         * Returns a List with the raw contents (no types) of the iterator.
+         */
+        public List<byte[]> toList() {
+            List<byte[]> list = new ArrayList<>();
+            for (TlvElement tlv : this) {
+                list.add(Arrays.copyOfRange(tlv.mRefArray, tlv.offset, tlv.offset + tlv.length));
+            }
+
+            return list;
+        }
+
+        /**
+         * Returns an iterator to step through a TLV formatted byte-array. The
+         * individual elements returned by the iterator are {@link TlvElement}.
+         */
+        @Override
+        public Iterator<TlvElement> iterator() {
+            return new Iterator<TlvElement>() {
+                private int mOffset = 0;
+
+                @Override
+                public boolean hasNext() {
+                    return mOffset < mArrayLength;
+                }
+
+                @Override
+                public TlvElement next() {
+                    if (!hasNext()) {
+                        throw new NoSuchElementException();
+                    }
+
+                    int type = 0;
+                    if (mTypeSize == 1) {
+                        type = mArray[mOffset];
+                    } else if (mTypeSize == 2) {
+                        type = Memory.peekShort(mArray, mOffset, mByteOrder);
+                    }
+                    mOffset += mTypeSize;
+
+                    int length = 0;
+                    if (mLengthSize == 1) {
+                        length = mArray[mOffset];
+                    } else if (mLengthSize == 2) {
+                        length = Memory.peekShort(mArray, mOffset, mByteOrder);
+                    }
+                    mOffset += mLengthSize;
+
+                    TlvElement tlv = new TlvElement(type, length, mArray, mOffset);
+                    tlv.byteOrder = mByteOrder;
+                    mOffset += length;
+                    return tlv;
+                }
+
+                @Override
+                public void remove() {
+                    throw new UnsupportedOperationException();
+                }
+            };
+        }
+    }
+
+    /**
+     * Validates that a (T)LV array is constructed correctly. I.e. that its specified Length
+     * fields correctly fill the specified length (and do not overshoot). Uses big-endian
+     * byte ordering.
+     *
+     * @param array The (T)LV array to verify.
+     * @param typeSize The size (in bytes) of the type field. Valid values are 0, 1, or 2.
+     * @param lengthSize The size (in bytes) of the length field. Valid values are 1 or 2.
+     * @return A boolean indicating whether the array is valid (true) or invalid (false).
+     */
+    public static boolean isValid(@Nullable byte[] array, int typeSize, int lengthSize) {
+        return isValidEndian(array, typeSize, lengthSize, ByteOrder.BIG_ENDIAN);
+    }
+
+    /**
+     * Validates that a (T)LV array is constructed correctly. I.e. that its specified Length
+     * fields correctly fill the specified length (and do not overshoot).
+     *
+     * @param array The (T)LV array to verify.
+     * @param typeSize The size (in bytes) of the type field. Valid values are 0, 1, or 2.
+     * @param lengthSize The size (in bytes) of the length field. Valid values are 1 or 2.
+     * @param byteOrder The endianness of the byte array: {@link ByteOrder#BIG_ENDIAN} or
+     *                  {@link ByteOrder#LITTLE_ENDIAN}.
+     * @return A boolean indicating whether the array is valid (true) or invalid (false).
+     */
+    public static boolean isValidEndian(@Nullable byte[] array, int typeSize, int lengthSize,
+            ByteOrder byteOrder) {
+        if (typeSize < 0 || typeSize > 2) {
+            throw new IllegalArgumentException(
+                    "Invalid arguments - typeSize must be 0, 1, or 2: typeSize=" + typeSize);
+        }
+        if (lengthSize <= 0 || lengthSize > 2) {
+            throw new IllegalArgumentException(
+                    "Invalid arguments - lengthSize must be 1 or 2: lengthSize=" + lengthSize);
+        }
+        if (array == null) {
+            return true;
+        }
+
+        int nextTlvIndex = 0;
+        while (nextTlvIndex + typeSize + lengthSize <= array.length) {
+            nextTlvIndex += typeSize;
+            if (lengthSize == 1) {
+                nextTlvIndex += lengthSize + array[nextTlvIndex];
+            } else {
+                nextTlvIndex += lengthSize + Memory.peekShort(array, nextTlvIndex, byteOrder);
+            }
+        }
+
+        return nextTlvIndex == array.length;
+    }
+}
diff --git a/android/net/wifi/aware/WifiAwareAgentNetworkSpecifier.java b/android/net/wifi/aware/WifiAwareAgentNetworkSpecifier.java
new file mode 100644
index 0000000..9164d04
--- /dev/null
+++ b/android/net/wifi/aware/WifiAwareAgentNetworkSpecifier.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.aware;
+
+import android.net.NetworkSpecifier;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+
+import libcore.util.HexEncoding;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+import java.util.StringJoiner;
+
+/**
+ * A network specifier object used to represent the capabilities of an network agent. A collection
+ * of multiple WifiAwareNetworkSpecifier objects whose matching critiera (satisfiedBy) is an OR:
+ * a match on any of the network specifiers in the collection is a match.
+ *
+ * This class is not intended for use in network requests.
+ *
+ * @hide
+ */
+public class WifiAwareAgentNetworkSpecifier extends NetworkSpecifier implements Parcelable {
+    private static final String TAG = "WifiAwareAgentNs";
+
+    private static final boolean VDBG = false; // STOPSHIP if true
+
+    private Set<ByteArrayWrapper> mNetworkSpecifiers = new HashSet<>();
+    private MessageDigest mDigester;
+
+    public WifiAwareAgentNetworkSpecifier() {
+        initialize();
+    }
+
+    public WifiAwareAgentNetworkSpecifier(WifiAwareNetworkSpecifier ns) {
+        initialize();
+        mNetworkSpecifiers.add(convert(ns));
+    }
+
+    public WifiAwareAgentNetworkSpecifier(WifiAwareNetworkSpecifier[] nss) {
+        initialize();
+        for (WifiAwareNetworkSpecifier ns : nss) {
+            mNetworkSpecifiers.add(convert(ns));
+        }
+    }
+
+    public boolean isEmpty() {
+        return mNetworkSpecifiers.isEmpty();
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeArray(mNetworkSpecifiers.toArray());
+    }
+
+    public static final @android.annotation.NonNull Creator<WifiAwareAgentNetworkSpecifier> CREATOR =
+            new Creator<WifiAwareAgentNetworkSpecifier>() {
+                @Override
+                public WifiAwareAgentNetworkSpecifier createFromParcel(Parcel in) {
+                    WifiAwareAgentNetworkSpecifier agentNs = new WifiAwareAgentNetworkSpecifier();
+                    Object[] objs = in.readArray(null);
+                    for (Object obj : objs) {
+                        agentNs.mNetworkSpecifiers.add((ByteArrayWrapper) obj);
+                    }
+                    return agentNs;
+                }
+
+                @Override
+                public WifiAwareAgentNetworkSpecifier[] newArray(int size) {
+                    return new WifiAwareAgentNetworkSpecifier[size];
+                }
+            };
+
+    @Override
+    public int hashCode() {
+        return mNetworkSpecifiers.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == this) {
+            return true;
+        }
+        if (!(obj instanceof WifiAwareAgentNetworkSpecifier)) {
+            return false;
+        }
+        return mNetworkSpecifiers.equals(((WifiAwareAgentNetworkSpecifier) obj).mNetworkSpecifiers);
+    }
+
+    @Override
+    public String toString() {
+        StringJoiner sj = new StringJoiner(",");
+        for (ByteArrayWrapper baw: mNetworkSpecifiers) {
+            sj.add(baw.toString());
+        }
+        return sj.toString();
+    }
+
+    @Override
+    public boolean satisfiedBy(NetworkSpecifier other) {
+        if (!(other instanceof WifiAwareAgentNetworkSpecifier)) {
+            return false;
+        }
+        WifiAwareAgentNetworkSpecifier otherNs = (WifiAwareAgentNetworkSpecifier) other;
+
+        // called as old.satifiedBy(new): satisfied if old contained in new
+        for (ByteArrayWrapper baw: mNetworkSpecifiers) {
+            if (!otherNs.mNetworkSpecifiers.contains(baw)) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    public boolean satisfiesAwareNetworkSpecifier(WifiAwareNetworkSpecifier ns) {
+        if (VDBG) Log.v(TAG, "satisfiesAwareNetworkSpecifier: ns=" + ns);
+        ByteArrayWrapper nsBytes = convert(ns);
+        return mNetworkSpecifiers.contains(nsBytes);
+    }
+
+    @Override
+    public void assertValidFromUid(int requestorUid) {
+        throw new SecurityException(
+                "WifiAwareAgentNetworkSpecifier should not be used in network requests");
+    }
+
+    @Override
+    public NetworkSpecifier redact() {
+        return null;
+    }
+
+    private void initialize() {
+        try {
+            mDigester = MessageDigest.getInstance("SHA-256");
+        } catch (NoSuchAlgorithmException e) {
+            Log.e(TAG, "Can not instantiate a SHA-256 digester!? Will match nothing.");
+            return;
+        }
+    }
+
+    private ByteArrayWrapper convert(WifiAwareNetworkSpecifier ns) {
+        if (mDigester == null) {
+            return null;
+        }
+
+        Parcel parcel = Parcel.obtain();
+        ns.writeToParcel(parcel, 0);
+        byte[] bytes = parcel.marshall();
+
+        mDigester.reset();
+        mDigester.update(bytes);
+        return new ByteArrayWrapper(mDigester.digest());
+    }
+
+    private static class ByteArrayWrapper implements Parcelable {
+        private byte[] mData;
+
+        ByteArrayWrapper(byte[] data) {
+            mData = data;
+        }
+
+        @Override
+        public int hashCode() {
+            return Arrays.hashCode(mData);
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (obj == this) {
+                return true;
+            }
+            if (!(obj instanceof ByteArrayWrapper)) {
+                return false;
+            }
+            return Arrays.equals(((ByteArrayWrapper) obj).mData, mData);
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeBlob(mData);
+        }
+
+        public static final @android.annotation.NonNull Creator<ByteArrayWrapper> CREATOR =
+                new Creator<ByteArrayWrapper>() {
+                    @Override
+                    public ByteArrayWrapper createFromParcel(Parcel in) {
+                        return new ByteArrayWrapper(in.readBlob());
+                    }
+
+                    @Override
+                    public ByteArrayWrapper[] newArray(int size) {
+                        return new ByteArrayWrapper[size];
+                    }
+                };
+
+        @Override
+        public String toString() {
+            return new String(HexEncoding.encode(mData));
+        }
+    }
+}
diff --git a/android/net/wifi/aware/WifiAwareManager.java b/android/net/wifi/aware/WifiAwareManager.java
new file mode 100644
index 0000000..41a412b
--- /dev/null
+++ b/android/net/wifi/aware/WifiAwareManager.java
@@ -0,0 +1,814 @@
+/*
+ * 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.wifi.aware;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.SystemService;
+import android.content.Context;
+import android.net.ConnectivityManager;
+import android.net.NetworkRequest;
+import android.net.NetworkSpecifier;
+import android.os.Binder;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Process;
+import android.os.RemoteException;
+import android.util.Log;
+
+import libcore.util.HexEncoding;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.ref.WeakReference;
+import java.nio.BufferOverflowException;
+import java.util.List;
+
+/**
+ * This class provides the primary API for managing Wi-Fi Aware operations:
+ * discovery and peer-to-peer data connections.
+ * <p>
+ * The class provides access to:
+ * <ul>
+ * <li>Initialize a Aware cluster (peer-to-peer synchronization). Refer to
+ * {@link #attach(AttachCallback, Handler)}.
+ * <li>Create discovery sessions (publish or subscribe sessions). Refer to
+ * {@link WifiAwareSession#publish(PublishConfig, DiscoverySessionCallback, Handler)} and
+ * {@link WifiAwareSession#subscribe(SubscribeConfig, DiscoverySessionCallback, Handler)}.
+ * <li>Create a Aware network specifier to be used with
+ * {@link ConnectivityManager#requestNetwork(NetworkRequest, ConnectivityManager.NetworkCallback)}
+ * to set-up a Aware connection with a peer. Refer to {@link WifiAwareNetworkSpecifier.Builder}.
+ * </ul>
+ * <p>
+ *     Aware may not be usable when Wi-Fi is disabled (and other conditions). To validate that
+ *     the functionality is available use the {@link #isAvailable()} function. To track
+ *     changes in Aware usability register for the {@link #ACTION_WIFI_AWARE_STATE_CHANGED}
+ *     broadcast. Note that this broadcast is not sticky - you should register for it and then
+ *     check the above API to avoid a race condition.
+ * <p>
+ *     An application must use {@link #attach(AttachCallback, Handler)} to initialize a
+ *     Aware cluster - before making any other Aware operation. Aware cluster membership is a
+ *     device-wide operation - the API guarantees that the device is in a cluster or joins a
+ *     Aware cluster (or starts one if none can be found). Information about attach success (or
+ *     failure) are returned in callbacks of {@link AttachCallback}. Proceed with Aware
+ *     discovery or connection setup only after receiving confirmation that Aware attach
+ *     succeeded - {@link AttachCallback#onAttached(WifiAwareSession)}. When an
+ *     application is finished using Aware it <b>must</b> use the
+ *     {@link WifiAwareSession#close()} API to indicate to the Aware service that the device
+ *     may detach from the Aware cluster. The device will actually disable Aware once the last
+ *     application detaches.
+ * <p>
+ *     Once a Aware attach is confirmed use the
+ *     {@link WifiAwareSession#publish(PublishConfig, DiscoverySessionCallback, Handler)}
+ *     or
+ *     {@link WifiAwareSession#subscribe(SubscribeConfig, DiscoverySessionCallback,
+ *     Handler)} to create publish or subscribe Aware discovery sessions. Events are called on the
+ *     provided callback object {@link DiscoverySessionCallback}. Specifically, the
+ *     {@link DiscoverySessionCallback#onPublishStarted(PublishDiscoverySession)}
+ *     and
+ *     {@link DiscoverySessionCallback#onSubscribeStarted(
+ *SubscribeDiscoverySession)}
+ *     return {@link PublishDiscoverySession} and
+ *     {@link SubscribeDiscoverySession}
+ *     objects respectively on which additional session operations can be performed, e.g. updating
+ *     the session {@link PublishDiscoverySession#updatePublish(PublishConfig)} and
+ *     {@link SubscribeDiscoverySession#updateSubscribe(SubscribeConfig)}. Sessions can
+ *     also be used to send messages using the
+ *     {@link DiscoverySession#sendMessage(PeerHandle, int, byte[])} APIs. When an
+ *     application is finished with a discovery session it <b>must</b> terminate it using the
+ *     {@link DiscoverySession#close()} API.
+ * <p>
+ *    Creating connections between Aware devices is managed by the standard
+ *    {@link ConnectivityManager#requestNetwork(NetworkRequest,
+ *    ConnectivityManager.NetworkCallback)}.
+ *    The {@link NetworkRequest} object should be constructed with:
+ *    <ul>
+ *        <li>{@link NetworkRequest.Builder#addTransportType(int)} of
+ *        {@link android.net.NetworkCapabilities#TRANSPORT_WIFI_AWARE}.
+ *        <li>{@link NetworkRequest.Builder#setNetworkSpecifier(String)} using
+ *        {@link WifiAwareNetworkSpecifier.Builder}.
+ *    </ul>
+ */
+@SystemService(Context.WIFI_AWARE_SERVICE)
+public class WifiAwareManager {
+    private static final String TAG = "WifiAwareManager";
+    private static final boolean DBG = false;
+    private static final boolean VDBG = false; // STOPSHIP if true
+
+    /**
+     * Broadcast intent action to indicate that the state of Wi-Fi Aware availability has changed.
+     * Use the {@link #isAvailable()} to query the current status.
+     * This broadcast is <b>not</b> sticky, use the {@link #isAvailable()} API after registering
+     * the broadcast to check the current state of Wi-Fi Aware.
+     * <p>Note: The broadcast is only delivered to registered receivers - no manifest registered
+     * components will be launched.
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String ACTION_WIFI_AWARE_STATE_CHANGED =
+            "android.net.wifi.aware.action.WIFI_AWARE_STATE_CHANGED";
+
+    /** @hide */
+    @IntDef({
+            WIFI_AWARE_DATA_PATH_ROLE_INITIATOR, WIFI_AWARE_DATA_PATH_ROLE_RESPONDER})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface DataPathRole {
+    }
+
+    /**
+     * Connection creation role is that of INITIATOR. Used to create a network specifier string
+     * when requesting a Aware network.
+     *
+     * @see WifiAwareSession#createNetworkSpecifierOpen(int, byte[])
+     * @see WifiAwareSession#createNetworkSpecifierPassphrase(int, byte[], String)
+     */
+    public static final int WIFI_AWARE_DATA_PATH_ROLE_INITIATOR = 0;
+
+    /**
+     * Connection creation role is that of RESPONDER. Used to create a network specifier string
+     * when requesting a Aware network.
+     *
+     * @see WifiAwareSession#createNetworkSpecifierOpen(int, byte[])
+     * @see WifiAwareSession#createNetworkSpecifierPassphrase(int, byte[], String)
+     */
+    public static final int WIFI_AWARE_DATA_PATH_ROLE_RESPONDER = 1;
+
+    private final Context mContext;
+    private final IWifiAwareManager mService;
+
+    private final Object mLock = new Object(); // lock access to the following vars
+
+    /** @hide */
+    public WifiAwareManager(Context context, IWifiAwareManager service) {
+        mContext = context;
+        mService = service;
+    }
+
+    /**
+     * Returns the current status of Aware API: whether or not Aware is available. To track
+     * changes in the state of Aware API register for the
+     * {@link #ACTION_WIFI_AWARE_STATE_CHANGED} broadcast.
+     *
+     * @return A boolean indicating whether the app can use the Aware API at this time (true) or
+     * not (false).
+     */
+    public boolean isAvailable() {
+        try {
+            return mService.isUsageEnabled();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns the characteristics of the Wi-Fi Aware interface: a set of parameters which specify
+     * limitations on configurations, e.g. the maximum service name length.
+     *
+     * @return An object specifying configuration limitations of Aware.
+     */
+    public Characteristics getCharacteristics() {
+        try {
+            return mService.getCharacteristics();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Attach to the Wi-Fi Aware service - enabling the application to create discovery sessions or
+     * create connections to peers. The device will attach to an existing cluster if it can find
+     * one or create a new cluster (if it is the first to enable Aware in its vicinity). Results
+     * (e.g. successful attach to a cluster) are provided to the {@code attachCallback} object.
+     * An application <b>must</b> call {@link WifiAwareSession#close()} when done with the
+     * Wi-Fi Aware object.
+     * <p>
+     * Note: a Aware cluster is a shared resource - if the device is already attached to a cluster
+     * then this function will simply indicate success immediately using the same {@code
+     * attachCallback}.
+     *
+     * @param attachCallback A callback for attach events, extended from
+     * {@link AttachCallback}.
+     * @param handler The Handler on whose thread to execute the callbacks of the {@code
+     * attachCallback} object. If a null is provided then the application's main thread will be
+     *                used.
+     */
+    public void attach(@NonNull AttachCallback attachCallback, @Nullable Handler handler) {
+        attach(handler, null, attachCallback, null);
+    }
+
+    /**
+     * Attach to the Wi-Fi Aware service - enabling the application to create discovery sessions or
+     * create connections to peers. The device will attach to an existing cluster if it can find
+     * one or create a new cluster (if it is the first to enable Aware in its vicinity). Results
+     * (e.g. successful attach to a cluster) are provided to the {@code attachCallback} object.
+     * An application <b>must</b> call {@link WifiAwareSession#close()} when done with the
+     * Wi-Fi Aware object.
+     * <p>
+     * Note: a Aware cluster is a shared resource - if the device is already attached to a cluster
+     * then this function will simply indicate success immediately using the same {@code
+     * attachCallback}.
+     * <p>
+     * This version of the API attaches a listener to receive the MAC address of the Aware interface
+     * on startup and whenever it is updated (it is randomized at regular intervals for privacy).
+     * The application must have the {@link android.Manifest.permission#ACCESS_FINE_LOCATION}
+     * permission to execute this attach request. Otherwise, use the
+     * {@link #attach(AttachCallback, Handler)} version. Note that aside from permission
+     * requirements this listener will wake up the host at regular intervals causing higher power
+     * consumption, do not use it unless the information is necessary (e.g. for OOB discovery).
+     *
+     * @param attachCallback A callback for attach events, extended from
+     * {@link AttachCallback}.
+     * @param identityChangedListener A listener for changed identity, extended from
+     * {@link IdentityChangedListener}.
+     * @param handler The Handler on whose thread to execute the callbacks of the {@code
+     * attachCallback} and {@code identityChangedListener} objects. If a null is provided then the
+     *                application's main thread will be used.
+     */
+    public void attach(@NonNull AttachCallback attachCallback,
+            @NonNull IdentityChangedListener identityChangedListener,
+            @Nullable Handler handler) {
+        attach(handler, null, attachCallback, identityChangedListener);
+    }
+
+    /** @hide */
+    public void attach(Handler handler, ConfigRequest configRequest,
+            AttachCallback attachCallback,
+            IdentityChangedListener identityChangedListener) {
+        if (VDBG) {
+            Log.v(TAG, "attach(): handler=" + handler + ", callback=" + attachCallback
+                    + ", configRequest=" + configRequest + ", identityChangedListener="
+                    + identityChangedListener);
+        }
+
+        if (attachCallback == null) {
+            throw new IllegalArgumentException("Null callback provided");
+        }
+
+        synchronized (mLock) {
+            Looper looper = (handler == null) ? Looper.getMainLooper() : handler.getLooper();
+
+            try {
+                Binder binder = new Binder();
+                mService.connect(binder, mContext.getOpPackageName(),
+                        new WifiAwareEventCallbackProxy(this, looper, binder, attachCallback,
+                                identityChangedListener), configRequest,
+                        identityChangedListener != null);
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
+    }
+
+    /** @hide */
+    public void disconnect(int clientId, Binder binder) {
+        if (VDBG) Log.v(TAG, "disconnect()");
+
+        try {
+            mService.disconnect(clientId, binder);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /** @hide */
+    public void publish(int clientId, Looper looper, PublishConfig publishConfig,
+            DiscoverySessionCallback callback) {
+        if (VDBG) Log.v(TAG, "publish(): clientId=" + clientId + ", config=" + publishConfig);
+
+        if (callback == null) {
+            throw new IllegalArgumentException("Null callback provided");
+        }
+
+        try {
+            mService.publish(mContext.getOpPackageName(), clientId, publishConfig,
+                    new WifiAwareDiscoverySessionCallbackProxy(this, looper, true, callback,
+                            clientId));
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /** @hide */
+    public void updatePublish(int clientId, int sessionId, PublishConfig publishConfig) {
+        if (VDBG) {
+            Log.v(TAG, "updatePublish(): clientId=" + clientId + ",sessionId=" + sessionId
+                    + ", config=" + publishConfig);
+        }
+
+        try {
+            mService.updatePublish(clientId, sessionId, publishConfig);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /** @hide */
+    public void subscribe(int clientId, Looper looper, SubscribeConfig subscribeConfig,
+            DiscoverySessionCallback callback) {
+        if (VDBG) {
+            if (VDBG) {
+                Log.v(TAG,
+                        "subscribe(): clientId=" + clientId + ", config=" + subscribeConfig);
+            }
+        }
+
+        if (callback == null) {
+            throw new IllegalArgumentException("Null callback provided");
+        }
+
+        try {
+            mService.subscribe(mContext.getOpPackageName(), clientId, subscribeConfig,
+                    new WifiAwareDiscoverySessionCallbackProxy(this, looper, false, callback,
+                            clientId));
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /** @hide */
+    public void updateSubscribe(int clientId, int sessionId, SubscribeConfig subscribeConfig) {
+        if (VDBG) {
+            Log.v(TAG, "updateSubscribe(): clientId=" + clientId + ",sessionId=" + sessionId
+                    + ", config=" + subscribeConfig);
+        }
+
+        try {
+            mService.updateSubscribe(clientId, sessionId, subscribeConfig);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /** @hide */
+    public void terminateSession(int clientId, int sessionId) {
+        if (VDBG) {
+            Log.d(TAG,
+                    "terminateSession(): clientId=" + clientId + ", sessionId=" + sessionId);
+        }
+
+        try {
+            mService.terminateSession(clientId, sessionId);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /** @hide */
+    public void sendMessage(int clientId, int sessionId, PeerHandle peerHandle, byte[] message,
+            int messageId, int retryCount) {
+        if (peerHandle == null) {
+            throw new IllegalArgumentException(
+                    "sendMessage: invalid peerHandle - must be non-null");
+        }
+
+        if (VDBG) {
+            Log.v(TAG, "sendMessage(): clientId=" + clientId + ", sessionId=" + sessionId
+                    + ", peerHandle=" + peerHandle.peerId + ", messageId="
+                    + messageId + ", retryCount=" + retryCount);
+        }
+
+        try {
+            mService.sendMessage(clientId, sessionId, peerHandle.peerId, message, messageId,
+                    retryCount);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /** @hide */
+    public NetworkSpecifier createNetworkSpecifier(int clientId, int role, int sessionId,
+            @NonNull PeerHandle peerHandle, @Nullable byte[] pmk, @Nullable String passphrase) {
+        if (VDBG) {
+            Log.v(TAG, "createNetworkSpecifier: role=" + role + ", sessionId=" + sessionId
+                    + ", peerHandle=" + ((peerHandle == null) ? peerHandle : peerHandle.peerId)
+                    + ", pmk=" + ((pmk == null) ? "null" : "non-null")
+                    + ", passphrase=" + ((passphrase == null) ? "null" : "non-null"));
+        }
+
+        if (!WifiAwareUtils.isLegacyVersion(mContext, Build.VERSION_CODES.Q)) {
+            throw new UnsupportedOperationException(
+                    "API deprecated - use WifiAwareNetworkSpecifier.Builder");
+        }
+
+        if (role != WIFI_AWARE_DATA_PATH_ROLE_INITIATOR
+                && role != WIFI_AWARE_DATA_PATH_ROLE_RESPONDER) {
+            throw new IllegalArgumentException(
+                    "createNetworkSpecifier: Invalid 'role' argument when creating a network "
+                            + "specifier");
+        }
+        if (role == WIFI_AWARE_DATA_PATH_ROLE_INITIATOR || !WifiAwareUtils.isLegacyVersion(mContext,
+                Build.VERSION_CODES.P)) {
+            if (peerHandle == null) {
+                throw new IllegalArgumentException(
+                        "createNetworkSpecifier: Invalid peer handle - cannot be null");
+            }
+        }
+
+        return new WifiAwareNetworkSpecifier(
+                (peerHandle == null) ? WifiAwareNetworkSpecifier.NETWORK_SPECIFIER_TYPE_IB_ANY_PEER
+                        : WifiAwareNetworkSpecifier.NETWORK_SPECIFIER_TYPE_IB,
+                role,
+                clientId,
+                sessionId,
+                peerHandle != null ? peerHandle.peerId : 0, // 0 is an invalid peer ID
+                null, // peerMac (not used in this method)
+                pmk,
+                passphrase,
+                0, // no port info for deprecated IB APIs
+                -1, // no transport info for deprecated IB APIs
+                Process.myUid());
+    }
+
+    /** @hide */
+    public NetworkSpecifier createNetworkSpecifier(int clientId, @DataPathRole int role,
+            @NonNull byte[] peer, @Nullable byte[] pmk, @Nullable String passphrase) {
+        if (VDBG) {
+            Log.v(TAG, "createNetworkSpecifier: role=" + role
+                    + ", pmk=" + ((pmk == null) ? "null" : "non-null")
+                    + ", passphrase=" + ((passphrase == null) ? "null" : "non-null"));
+        }
+
+        if (role != WIFI_AWARE_DATA_PATH_ROLE_INITIATOR
+                && role != WIFI_AWARE_DATA_PATH_ROLE_RESPONDER) {
+            throw new IllegalArgumentException(
+                    "createNetworkSpecifier: Invalid 'role' argument when creating a network "
+                            + "specifier");
+        }
+        if (role == WIFI_AWARE_DATA_PATH_ROLE_INITIATOR || !WifiAwareUtils.isLegacyVersion(mContext,
+                Build.VERSION_CODES.P)) {
+            if (peer == null) {
+                throw new IllegalArgumentException(
+                        "createNetworkSpecifier: Invalid peer MAC - cannot be null");
+            }
+        }
+        if (peer != null && peer.length != 6) {
+            throw new IllegalArgumentException("createNetworkSpecifier: Invalid peer MAC address");
+        }
+
+        return new WifiAwareNetworkSpecifier(
+                (peer == null) ? WifiAwareNetworkSpecifier.NETWORK_SPECIFIER_TYPE_OOB_ANY_PEER
+                        : WifiAwareNetworkSpecifier.NETWORK_SPECIFIER_TYPE_OOB,
+                role,
+                clientId,
+                0, // 0 is an invalid session ID
+                0, // 0 is an invalid peer ID
+                peer,
+                pmk,
+                passphrase,
+                0, // no port info for OOB APIs
+                -1, // no transport protocol info for OOB APIs
+                Process.myUid());
+    }
+
+    private static class WifiAwareEventCallbackProxy extends IWifiAwareEventCallback.Stub {
+        private static final int CALLBACK_CONNECT_SUCCESS = 0;
+        private static final int CALLBACK_CONNECT_FAIL = 1;
+        private static final int CALLBACK_IDENTITY_CHANGED = 2;
+
+        private final Handler mHandler;
+        private final WeakReference<WifiAwareManager> mAwareManager;
+        private final Binder mBinder;
+        private final Looper mLooper;
+
+        /**
+         * Constructs a {@link AttachCallback} using the specified looper.
+         * All callbacks will delivered on the thread of the specified looper.
+         *
+         * @param looper The looper on which to execute the callbacks.
+         */
+        WifiAwareEventCallbackProxy(WifiAwareManager mgr, Looper looper, Binder binder,
+                final AttachCallback attachCallback,
+                final IdentityChangedListener identityChangedListener) {
+            mAwareManager = new WeakReference<>(mgr);
+            mLooper = looper;
+            mBinder = binder;
+
+            if (VDBG) Log.v(TAG, "WifiAwareEventCallbackProxy ctor: looper=" + looper);
+            mHandler = new Handler(looper) {
+                @Override
+                public void handleMessage(Message msg) {
+                    if (DBG) {
+                        Log.d(TAG, "WifiAwareEventCallbackProxy: What=" + msg.what + ", msg="
+                                + msg);
+                    }
+
+                    WifiAwareManager mgr = mAwareManager.get();
+                    if (mgr == null) {
+                        Log.w(TAG, "WifiAwareEventCallbackProxy: handleMessage post GC");
+                        return;
+                    }
+
+                    switch (msg.what) {
+                        case CALLBACK_CONNECT_SUCCESS:
+                            attachCallback.onAttached(
+                                    new WifiAwareSession(mgr, mBinder, msg.arg1));
+                            break;
+                        case CALLBACK_CONNECT_FAIL:
+                            mAwareManager.clear();
+                            attachCallback.onAttachFailed();
+                            break;
+                        case CALLBACK_IDENTITY_CHANGED:
+                            if (identityChangedListener == null) {
+                                Log.e(TAG, "CALLBACK_IDENTITY_CHANGED: null listener.");
+                            } else {
+                                identityChangedListener.onIdentityChanged((byte[]) msg.obj);
+                            }
+                            break;
+                    }
+                }
+            };
+        }
+
+        @Override
+        public void onConnectSuccess(int clientId) {
+            if (VDBG) Log.v(TAG, "onConnectSuccess");
+
+            Message msg = mHandler.obtainMessage(CALLBACK_CONNECT_SUCCESS);
+            msg.arg1 = clientId;
+            mHandler.sendMessage(msg);
+        }
+
+        @Override
+        public void onConnectFail(int reason) {
+            if (VDBG) Log.v(TAG, "onConnectFail: reason=" + reason);
+
+            Message msg = mHandler.obtainMessage(CALLBACK_CONNECT_FAIL);
+            msg.arg1 = reason;
+            mHandler.sendMessage(msg);
+        }
+
+        @Override
+        public void onIdentityChanged(byte[] mac) {
+            if (VDBG) Log.v(TAG, "onIdentityChanged: mac=" + new String(HexEncoding.encode(mac)));
+
+            Message msg = mHandler.obtainMessage(CALLBACK_IDENTITY_CHANGED);
+            msg.obj = mac;
+            mHandler.sendMessage(msg);
+        }
+    }
+
+    private static class WifiAwareDiscoverySessionCallbackProxy extends
+            IWifiAwareDiscoverySessionCallback.Stub {
+        private static final int CALLBACK_SESSION_STARTED = 0;
+        private static final int CALLBACK_SESSION_CONFIG_SUCCESS = 1;
+        private static final int CALLBACK_SESSION_CONFIG_FAIL = 2;
+        private static final int CALLBACK_SESSION_TERMINATED = 3;
+        private static final int CALLBACK_MATCH = 4;
+        private static final int CALLBACK_MESSAGE_SEND_SUCCESS = 5;
+        private static final int CALLBACK_MESSAGE_SEND_FAIL = 6;
+        private static final int CALLBACK_MESSAGE_RECEIVED = 7;
+        private static final int CALLBACK_MATCH_WITH_DISTANCE = 8;
+
+        private static final String MESSAGE_BUNDLE_KEY_MESSAGE = "message";
+        private static final String MESSAGE_BUNDLE_KEY_MESSAGE2 = "message2";
+
+        private final WeakReference<WifiAwareManager> mAwareManager;
+        private final boolean mIsPublish;
+        private final DiscoverySessionCallback mOriginalCallback;
+        private final int mClientId;
+
+        private final Handler mHandler;
+        private DiscoverySession mSession;
+
+        WifiAwareDiscoverySessionCallbackProxy(WifiAwareManager mgr, Looper looper,
+                boolean isPublish, DiscoverySessionCallback originalCallback,
+                int clientId) {
+            mAwareManager = new WeakReference<>(mgr);
+            mIsPublish = isPublish;
+            mOriginalCallback = originalCallback;
+            mClientId = clientId;
+
+            if (VDBG) {
+                Log.v(TAG, "WifiAwareDiscoverySessionCallbackProxy ctor: isPublish=" + isPublish);
+            }
+
+            mHandler = new Handler(looper) {
+                @Override
+                public void handleMessage(Message msg) {
+                    if (DBG) Log.d(TAG, "What=" + msg.what + ", msg=" + msg);
+
+                    if (mAwareManager.get() == null) {
+                        Log.w(TAG, "WifiAwareDiscoverySessionCallbackProxy: handleMessage post GC");
+                        return;
+                    }
+
+                    switch (msg.what) {
+                        case CALLBACK_SESSION_STARTED:
+                            onProxySessionStarted(msg.arg1);
+                            break;
+                        case CALLBACK_SESSION_CONFIG_SUCCESS:
+                            mOriginalCallback.onSessionConfigUpdated();
+                            break;
+                        case CALLBACK_SESSION_CONFIG_FAIL:
+                            mOriginalCallback.onSessionConfigFailed();
+                            if (mSession == null) {
+                                /*
+                                 * creation failed (as opposed to update
+                                 * failing)
+                                 */
+                                mAwareManager.clear();
+                            }
+                            break;
+                        case CALLBACK_SESSION_TERMINATED:
+                            onProxySessionTerminated(msg.arg1);
+                            break;
+                        case CALLBACK_MATCH:
+                        case CALLBACK_MATCH_WITH_DISTANCE:
+                            {
+                            List<byte[]> matchFilter = null;
+                            byte[] arg = msg.getData().getByteArray(MESSAGE_BUNDLE_KEY_MESSAGE2);
+                            try {
+                                matchFilter = new TlvBufferUtils.TlvIterable(0, 1, arg).toList();
+                            } catch (BufferOverflowException e) {
+                                matchFilter = null;
+                                Log.e(TAG, "onServiceDiscovered: invalid match filter byte array '"
+                                        + new String(HexEncoding.encode(arg))
+                                        + "' - cannot be parsed: e=" + e);
+                            }
+                            if (msg.what == CALLBACK_MATCH) {
+                                mOriginalCallback.onServiceDiscovered(new PeerHandle(msg.arg1),
+                                        msg.getData().getByteArray(MESSAGE_BUNDLE_KEY_MESSAGE),
+                                        matchFilter);
+                            } else {
+                                mOriginalCallback.onServiceDiscoveredWithinRange(
+                                        new PeerHandle(msg.arg1),
+                                        msg.getData().getByteArray(MESSAGE_BUNDLE_KEY_MESSAGE),
+                                        matchFilter, msg.arg2);
+                            }
+                            break;
+                        }
+                        case CALLBACK_MESSAGE_SEND_SUCCESS:
+                            mOriginalCallback.onMessageSendSucceeded(msg.arg1);
+                            break;
+                        case CALLBACK_MESSAGE_SEND_FAIL:
+                            mOriginalCallback.onMessageSendFailed(msg.arg1);
+                            break;
+                        case CALLBACK_MESSAGE_RECEIVED:
+                            mOriginalCallback.onMessageReceived(new PeerHandle(msg.arg1),
+                                    (byte[]) msg.obj);
+                            break;
+                    }
+                }
+            };
+        }
+
+        @Override
+        public void onSessionStarted(int sessionId) {
+            if (VDBG) Log.v(TAG, "onSessionStarted: sessionId=" + sessionId);
+
+            Message msg = mHandler.obtainMessage(CALLBACK_SESSION_STARTED);
+            msg.arg1 = sessionId;
+            mHandler.sendMessage(msg);
+        }
+
+        @Override
+        public void onSessionConfigSuccess() {
+            if (VDBG) Log.v(TAG, "onSessionConfigSuccess");
+
+            Message msg = mHandler.obtainMessage(CALLBACK_SESSION_CONFIG_SUCCESS);
+            mHandler.sendMessage(msg);
+        }
+
+        @Override
+        public void onSessionConfigFail(int reason) {
+            if (VDBG) Log.v(TAG, "onSessionConfigFail: reason=" + reason);
+
+            Message msg = mHandler.obtainMessage(CALLBACK_SESSION_CONFIG_FAIL);
+            msg.arg1 = reason;
+            mHandler.sendMessage(msg);
+        }
+
+        @Override
+        public void onSessionTerminated(int reason) {
+            if (VDBG) Log.v(TAG, "onSessionTerminated: reason=" + reason);
+
+            Message msg = mHandler.obtainMessage(CALLBACK_SESSION_TERMINATED);
+            msg.arg1 = reason;
+            mHandler.sendMessage(msg);
+        }
+
+        private void onMatchCommon(int messageType, int peerId, byte[] serviceSpecificInfo,
+                byte[] matchFilter, int distanceMm) {
+            Bundle data = new Bundle();
+            data.putByteArray(MESSAGE_BUNDLE_KEY_MESSAGE, serviceSpecificInfo);
+            data.putByteArray(MESSAGE_BUNDLE_KEY_MESSAGE2, matchFilter);
+
+            Message msg = mHandler.obtainMessage(messageType);
+            msg.arg1 = peerId;
+            msg.arg2 = distanceMm;
+            msg.setData(data);
+            mHandler.sendMessage(msg);
+        }
+
+        @Override
+        public void onMatch(int peerId, byte[] serviceSpecificInfo, byte[] matchFilter) {
+            if (VDBG) Log.v(TAG, "onMatch: peerId=" + peerId);
+
+            onMatchCommon(CALLBACK_MATCH, peerId, serviceSpecificInfo, matchFilter, 0);
+        }
+
+        @Override
+        public void onMatchWithDistance(int peerId, byte[] serviceSpecificInfo, byte[] matchFilter,
+                int distanceMm) {
+            if (VDBG) {
+                Log.v(TAG, "onMatchWithDistance: peerId=" + peerId + ", distanceMm=" + distanceMm);
+            }
+
+            onMatchCommon(CALLBACK_MATCH_WITH_DISTANCE, peerId, serviceSpecificInfo, matchFilter,
+                    distanceMm);
+        }
+
+        @Override
+        public void onMessageSendSuccess(int messageId) {
+            if (VDBG) Log.v(TAG, "onMessageSendSuccess");
+
+            Message msg = mHandler.obtainMessage(CALLBACK_MESSAGE_SEND_SUCCESS);
+            msg.arg1 = messageId;
+            mHandler.sendMessage(msg);
+        }
+
+        @Override
+        public void onMessageSendFail(int messageId, int reason) {
+            if (VDBG) Log.v(TAG, "onMessageSendFail: reason=" + reason);
+
+            Message msg = mHandler.obtainMessage(CALLBACK_MESSAGE_SEND_FAIL);
+            msg.arg1 = messageId;
+            msg.arg2 = reason;
+            mHandler.sendMessage(msg);
+        }
+
+        @Override
+        public void onMessageReceived(int peerId, byte[] message) {
+            if (VDBG) {
+                Log.v(TAG, "onMessageReceived: peerId=" + peerId);
+            }
+
+            Message msg = mHandler.obtainMessage(CALLBACK_MESSAGE_RECEIVED);
+            msg.arg1 = peerId;
+            msg.obj = message;
+            mHandler.sendMessage(msg);
+        }
+
+        /*
+         * Proxied methods
+         */
+        public void onProxySessionStarted(int sessionId) {
+            if (VDBG) Log.v(TAG, "Proxy: onSessionStarted: sessionId=" + sessionId);
+            if (mSession != null) {
+                Log.e(TAG,
+                        "onSessionStarted: sessionId=" + sessionId + ": session already created!?");
+                throw new IllegalStateException(
+                        "onSessionStarted: sessionId=" + sessionId + ": session already created!?");
+            }
+
+            WifiAwareManager mgr = mAwareManager.get();
+            if (mgr == null) {
+                Log.w(TAG, "onProxySessionStarted: mgr GC'd");
+                return;
+            }
+
+            if (mIsPublish) {
+                PublishDiscoverySession session = new PublishDiscoverySession(mgr,
+                        mClientId, sessionId);
+                mSession = session;
+                mOriginalCallback.onPublishStarted(session);
+            } else {
+                SubscribeDiscoverySession
+                        session = new SubscribeDiscoverySession(mgr, mClientId, sessionId);
+                mSession = session;
+                mOriginalCallback.onSubscribeStarted(session);
+            }
+        }
+
+        public void onProxySessionTerminated(int reason) {
+            if (VDBG) Log.v(TAG, "Proxy: onSessionTerminated: reason=" + reason);
+            if (mSession != null) {
+                mSession.setTerminated();
+                mSession = null;
+            } else {
+                Log.w(TAG, "Proxy: onSessionTerminated called but mSession is null!?");
+            }
+            mAwareManager.clear();
+            mOriginalCallback.onSessionTerminated();
+        }
+    }
+}
diff --git a/android/net/wifi/aware/WifiAwareNetworkInfo.java b/android/net/wifi/aware/WifiAwareNetworkInfo.java
new file mode 100644
index 0000000..fd26817
--- /dev/null
+++ b/android/net/wifi/aware/WifiAwareNetworkInfo.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2018 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.wifi.aware;
+
+import android.annotation.Nullable;
+import android.net.NetworkCapabilities;
+import android.net.TransportInfo;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.net.Inet6Address;
+import java.net.NetworkInterface;
+import java.net.SocketException;
+import java.net.UnknownHostException;
+import java.util.Objects;
+
+/**
+ * Wi-Fi Aware-specific network information. The information can be extracted from the
+ * {@link android.net.NetworkCapabilities} of the network using
+ * {@link NetworkCapabilities#getTransportInfo()}.
+ * The {@link NetworkCapabilities} is provided by the connectivity service to apps, e.g. received
+ * through the
+ * {@link android.net.ConnectivityManager.NetworkCallback#onCapabilitiesChanged(android.net.Network,
+ * android.net.NetworkCapabilities)} callback.
+ * <p>
+ * The Wi-Fi Aware-specific network information include the peer's scoped link-local IPv6 address
+ * for the Wi-Fi Aware link, as well as (optionally) the port and transport protocol specified by
+ * the peer.
+ * The scoped link-local IPv6, port, and transport protocol can then be used to create a
+ * {@link java.net.Socket} connection to the peer.
+ * <p>
+ * Note: these are the peer's IPv6 and port information - not the local device's!
+ */
+public final class WifiAwareNetworkInfo implements TransportInfo, Parcelable {
+    private Inet6Address mIpv6Addr;
+    private int mPort = 0; // a value of 0 is considered invalid
+    private int mTransportProtocol = -1; // a value of -1 is considered invalid
+
+    /** @hide */
+    public WifiAwareNetworkInfo(Inet6Address ipv6Addr) {
+        mIpv6Addr = ipv6Addr;
+    }
+
+    /** @hide */
+    public WifiAwareNetworkInfo(Inet6Address ipv6Addr, int port, int transportProtocol) {
+        mIpv6Addr = ipv6Addr;
+        mPort = port;
+        mTransportProtocol = transportProtocol;
+    }
+
+    /**
+     * Get the scoped link-local IPv6 address of the Wi-Fi Aware peer (not of the local device!).
+     *
+     * @return An IPv6 address.
+     */
+    @Nullable
+    public Inet6Address getPeerIpv6Addr() {
+        return mIpv6Addr;
+    }
+
+    /**
+     * Get the port number to be used to create a network connection to the Wi-Fi Aware peer.
+     * The port information is provided by the app running on the peer which requested the
+     * connection, using the {@link WifiAwareNetworkSpecifier.Builder#setPort(int)}.
+     *
+     * @return A port number on the peer. A value of 0 indicates that no port was specified by the
+     *         peer.
+     */
+    public int getPort() {
+        return mPort;
+    }
+
+    /**
+     * Get the transport protocol to be used to communicate over a network connection to the Wi-Fi
+     * Aware peer. The transport protocol is provided by the app running on the peer which requested
+     * the connection, using the
+     * {@link WifiAwareNetworkSpecifier.Builder#setTransportProtocol(int)}.
+     * <p>
+     * The transport protocol number is assigned by the Internet Assigned Numbers Authority
+     * (IANA) https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml.
+     *
+     * @return A transport protocol id. A value of -1 indicates that no transport protocol was
+     *         specified by the peer.
+     */
+    public int getTransportProtocol() {
+        return mTransportProtocol;
+    }
+
+    // parcelable methods
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeByteArray(mIpv6Addr.getAddress());
+        NetworkInterface ni = mIpv6Addr.getScopedInterface();
+        dest.writeString(ni == null ? null : ni.getName());
+        dest.writeInt(mPort);
+        dest.writeInt(mTransportProtocol);
+    }
+
+    public static final @android.annotation.NonNull Creator<WifiAwareNetworkInfo> CREATOR =
+            new Creator<WifiAwareNetworkInfo>() {
+                @Override
+                public WifiAwareNetworkInfo createFromParcel(Parcel in) {
+                    Inet6Address ipv6Addr;
+                    try {
+                        byte[] addr = in.createByteArray();
+                        String interfaceName = in.readString();
+                        NetworkInterface ni = null;
+                        if (interfaceName != null) {
+                            try {
+                                ni = NetworkInterface.getByName(interfaceName);
+                            } catch (SocketException e) {
+                                e.printStackTrace();
+                            }
+                        }
+                        ipv6Addr = Inet6Address.getByAddress(null, addr, ni);
+                    } catch (UnknownHostException e) {
+                        e.printStackTrace();
+                        return null;
+                    }
+                    int port = in.readInt();
+                    int transportProtocol = in.readInt();
+
+                    return new WifiAwareNetworkInfo(ipv6Addr, port, transportProtocol);
+                }
+
+                @Override
+                public WifiAwareNetworkInfo[] newArray(int size) {
+                    return new WifiAwareNetworkInfo[size];
+                }
+            };
+
+
+    // object methods
+
+    @Override
+    public String toString() {
+        return new StringBuilder("AwareNetworkInfo: IPv6=").append(mIpv6Addr).append(
+                ", port=").append(mPort).append(", transportProtocol=").append(
+                mTransportProtocol).toString();
+    }
+
+    /** @hide */
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+
+        if (!(obj instanceof WifiAwareNetworkInfo)) {
+            return false;
+        }
+
+        WifiAwareNetworkInfo lhs = (WifiAwareNetworkInfo) obj;
+        return Objects.equals(mIpv6Addr, lhs.mIpv6Addr) && mPort == lhs.mPort
+                && mTransportProtocol == lhs.mTransportProtocol;
+    }
+
+    /** @hide */
+    @Override
+    public int hashCode() {
+        return Objects.hash(mIpv6Addr, mPort, mTransportProtocol);
+    }
+}
diff --git a/android/net/wifi/aware/WifiAwareNetworkSpecifier.java b/android/net/wifi/aware/WifiAwareNetworkSpecifier.java
new file mode 100644
index 0000000..0511f24
--- /dev/null
+++ b/android/net/wifi/aware/WifiAwareNetworkSpecifier.java
@@ -0,0 +1,471 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.aware;
+
+import static android.net.wifi.aware.WifiAwareManager.WIFI_AWARE_DATA_PATH_ROLE_INITIATOR;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.net.NetworkSpecifier;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.Process;
+import android.text.TextUtils;
+
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Network specifier object used to request a Wi-Fi Aware network. Apps should use the
+ * {@link WifiAwareNetworkSpecifier.Builder} class to create an instance.
+ */
+public final class WifiAwareNetworkSpecifier extends NetworkSpecifier implements Parcelable {
+    /**
+     * TYPE: in band, specific peer: role, client_id, session_id, peer_id, pmk/passphrase optional
+     * @hide
+     */
+    public static final int NETWORK_SPECIFIER_TYPE_IB = 0;
+
+    /**
+     * TYPE: in band, any peer: role, client_id, session_id, pmk/passphrase optional
+     * [only permitted for RESPONDER]
+     * @hide
+     */
+    public static final int NETWORK_SPECIFIER_TYPE_IB_ANY_PEER = 1;
+
+    /**
+     * TYPE: out-of-band: role, client_id, peer_mac, pmk/passphrase optional
+     * @hide
+     */
+    public static final int NETWORK_SPECIFIER_TYPE_OOB = 2;
+
+    /**
+     * TYPE: out-of-band, any peer: role, client_id, pmk/passphrase optional
+     * [only permitted for RESPONDER]
+     * @hide
+     */
+    public static final int NETWORK_SPECIFIER_TYPE_OOB_ANY_PEER = 3;
+
+    /** @hide */
+    public static final int NETWORK_SPECIFIER_TYPE_MAX_VALID = NETWORK_SPECIFIER_TYPE_OOB_ANY_PEER;
+
+    /**
+     * One of the NETWORK_SPECIFIER_TYPE_* constants. The type of the network specifier object.
+     * @hide
+     */
+    public final int type;
+
+    /**
+     * The role of the device: WifiAwareManager.WIFI_AWARE_DATA_PATH_ROLE_INITIATOR or
+     * WifiAwareManager.WIFI_AWARE_DATA_PATH_ROLE_RESPONDER.
+     * @hide
+     */
+    public final int role;
+
+    /**
+     * The client ID of the device.
+     * @hide
+     */
+    public final int clientId;
+
+    /**
+     * The session ID in which context to request a data-path. Only relevant for IB requests.
+     * @hide
+     */
+    public final int sessionId;
+
+    /**
+     * The peer ID of the device which the data-path should be connected to. Only relevant for
+     * IB requests (i.e. not IB_ANY_PEER or OOB*).
+     * @hide
+     */
+    public final int peerId;
+
+    /**
+     * The peer MAC address of the device which the data-path should be connected to. Only relevant
+     * for OB requests (i.e. not OOB_ANY_PEER or IB*).
+     * @hide
+     */
+    public final byte[] peerMac;
+
+    /**
+     * The PMK of the requested data-path. Can be null. Only one or none of pmk or passphrase should
+     * be specified.
+     * @hide
+     */
+    public final byte[] pmk;
+
+    /**
+     * The Passphrase of the requested data-path. Can be null. Only one or none of the pmk or
+     * passphrase should be specified.
+     * @hide
+     */
+    public final String passphrase;
+
+    /**
+     * The port information to be used for this link. This information will be communicated to the
+     * peer as part of the layer 2 link setup.
+     *
+     * Information only allowed on secure links since a single layer-2 link is set up for all
+     * requestors. Therefore if multiple apps on a single device request links to the same peer
+     * device they all get the same link. However, the link is only set up on the first request -
+     * hence only the first can transmit the port information. But we don't want to expose that
+     * information to other apps. Limiting to secure links would (usually) imply single app usage.
+     *
+     * @hide
+     */
+    public final int port;
+
+    /**
+     * The transport protocol information to be used for this link. This information will be
+     * communicated to the peer as part of the layer 2 link setup.
+     *
+     * Information only allowed on secure links since a single layer-2 link is set up for all
+     * requestors. Therefore if multiple apps on a single device request links to the same peer
+     * device they all get the same link. However, the link is only set up on the first request -
+     * hence only the first can transmit the port information. But we don't want to expose that
+     * information to other apps. Limiting to secure links would (usually) imply single app usage.
+     *
+     * @hide
+     */
+    public final int transportProtocol;
+
+    /**
+     * The UID of the process initializing this network specifier. Validated by receiver using
+     * checkUidIfNecessary() and is used by satisfiedBy() to determine whether matches the
+     * offered network.
+     *
+     * @hide
+     */
+    public final int requestorUid;
+
+    /** @hide */
+    public WifiAwareNetworkSpecifier(int type, int role, int clientId, int sessionId, int peerId,
+            byte[] peerMac, byte[] pmk, String passphrase, int port, int transportProtocol,
+            int requestorUid) {
+        this.type = type;
+        this.role = role;
+        this.clientId = clientId;
+        this.sessionId = sessionId;
+        this.peerId = peerId;
+        this.peerMac = peerMac;
+        this.pmk = pmk;
+        this.passphrase = passphrase;
+        this.port = port;
+        this.transportProtocol = transportProtocol;
+        this.requestorUid = requestorUid;
+    }
+
+    public static final @android.annotation.NonNull Creator<WifiAwareNetworkSpecifier> CREATOR =
+            new Creator<WifiAwareNetworkSpecifier>() {
+                @Override
+                public WifiAwareNetworkSpecifier createFromParcel(Parcel in) {
+                    return new WifiAwareNetworkSpecifier(
+                        in.readInt(), // type
+                        in.readInt(), // role
+                        in.readInt(), // clientId
+                        in.readInt(), // sessionId
+                        in.readInt(), // peerId
+                        in.createByteArray(), // peerMac
+                        in.createByteArray(), // pmk
+                        in.readString(), // passphrase
+                        in.readInt(), // port
+                        in.readInt(), // transportProtocol
+                        in.readInt()); // requestorUid
+                }
+
+                @Override
+                public WifiAwareNetworkSpecifier[] newArray(int size) {
+                    return new WifiAwareNetworkSpecifier[size];
+                }
+            };
+
+    /**
+     * Indicates whether the network specifier specifies an OOB (out-of-band) data-path - i.e. a
+     * data-path created without a corresponding Aware discovery session.
+     *
+     * @hide
+     */
+    public boolean isOutOfBand() {
+        return type == NETWORK_SPECIFIER_TYPE_OOB || type == NETWORK_SPECIFIER_TYPE_OOB_ANY_PEER;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(type);
+        dest.writeInt(role);
+        dest.writeInt(clientId);
+        dest.writeInt(sessionId);
+        dest.writeInt(peerId);
+        dest.writeByteArray(peerMac);
+        dest.writeByteArray(pmk);
+        dest.writeString(passphrase);
+        dest.writeInt(port);
+        dest.writeInt(transportProtocol);
+        dest.writeInt(requestorUid);
+    }
+
+    /** @hide */
+    @Override
+    public boolean satisfiedBy(NetworkSpecifier other) {
+        // MatchAllNetworkSpecifier is taken care in NetworkCapabilities#satisfiedBySpecifier.
+        if (other instanceof WifiAwareAgentNetworkSpecifier) {
+            return ((WifiAwareAgentNetworkSpecifier) other).satisfiesAwareNetworkSpecifier(this);
+        }
+        return equals(other);
+    }
+
+    /** @hide */
+    @Override
+    public int hashCode() {
+        return Objects.hash(type, role, clientId, sessionId, peerId, Arrays.hashCode(peerMac),
+                Arrays.hashCode(pmk), passphrase, port, transportProtocol, requestorUid);
+    }
+
+    /** @hide */
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+
+        if (!(obj instanceof WifiAwareNetworkSpecifier)) {
+            return false;
+        }
+
+        WifiAwareNetworkSpecifier lhs = (WifiAwareNetworkSpecifier) obj;
+
+        return type == lhs.type
+                && role == lhs.role
+                && clientId == lhs.clientId
+                && sessionId == lhs.sessionId
+                && peerId == lhs.peerId
+                && Arrays.equals(peerMac, lhs.peerMac)
+                && Arrays.equals(pmk, lhs.pmk)
+                && Objects.equals(passphrase, lhs.passphrase)
+                && port == lhs.port
+                && transportProtocol == lhs.transportProtocol
+                && requestorUid == lhs.requestorUid;
+    }
+
+    /** @hide */
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder("WifiAwareNetworkSpecifier [");
+        sb.append("type=").append(type)
+                .append(", role=").append(role)
+                .append(", clientId=").append(clientId)
+                .append(", sessionId=").append(sessionId)
+                .append(", peerId=").append(peerId)
+                // masking potential PII (although low impact information)
+                .append(", peerMac=").append((peerMac == null) ? "<null>" : "<non-null>")
+                // masking PII
+                .append(", pmk=").append((pmk == null) ? "<null>" : "<non-null>")
+                // masking PII
+                .append(", passphrase=").append((passphrase == null) ? "<null>" : "<non-null>")
+                .append(", port=").append(port).append(", transportProtocol=")
+                .append(transportProtocol).append(", requestorUid=").append(requestorUid)
+                .append("]");
+        return sb.toString();
+    }
+
+    /** @hide */
+    @Override
+    public void assertValidFromUid(int requestorUid) {
+        if (this.requestorUid != requestorUid) {
+            throw new SecurityException("mismatched UIDs");
+        }
+    }
+
+    /**
+     * A builder class for a Wi-Fi Aware network specifier to set up an Aware connection with a
+     * peer.
+     */
+    public static final class Builder {
+        private DiscoverySession mDiscoverySession;
+        private PeerHandle mPeerHandle;
+        private String mPskPassphrase;
+        private byte[] mPmk;
+        private int mPort = 0; // invalid value
+        private int mTransportProtocol = -1; // invalid value
+
+        /**
+         * Create a builder for {@link WifiAwareNetworkSpecifier} used in requests to set up a
+         * Wi-Fi Aware connection with a peer.
+         *
+         * @param discoverySession A Wi-Fi Aware discovery session in whose context the connection
+         *                         is created.
+         * @param peerHandle The handle of the peer to which the Wi-Fi Aware connection is
+         *                   requested. The peer is discovered through Wi-Fi Aware discovery. The
+         *                   handle can be obtained through
+         * {@link DiscoverySessionCallback#onServiceDiscovered(PeerHandle, byte[], java.util.List)}
+         *                   or
+         *                   {@link DiscoverySessionCallback#onMessageReceived(PeerHandle, byte[])}.
+         */
+        public Builder(@NonNull DiscoverySession discoverySession, @NonNull PeerHandle peerHandle) {
+            if (discoverySession == null) {
+                throw new IllegalArgumentException("Non-null discoverySession required");
+            }
+            if (peerHandle == null) {
+                throw new IllegalArgumentException("Non-null peerHandle required");
+            }
+            mDiscoverySession = discoverySession;
+            mPeerHandle = peerHandle;
+        }
+
+        /**
+         * Configure the PSK Passphrase for the Wi-Fi Aware connection being requested. This method
+         * is optional - if not called, then an Open (unencrypted) connection will be created.
+         *
+         * @param pskPassphrase The (optional) passphrase to be used to encrypt the link.
+         * @return the current {@link Builder} builder, enabling chaining of builder
+         *         methods.
+         */
+        public @NonNull Builder setPskPassphrase(@NonNull String pskPassphrase) {
+            if (!WifiAwareUtils.validatePassphrase(pskPassphrase)) {
+                throw new IllegalArgumentException("Passphrase must meet length requirements");
+            }
+            mPskPassphrase = pskPassphrase;
+            return this;
+        }
+
+        /**
+         * Configure the PMK for the Wi-Fi Aware connection being requested. This method
+         * is optional - if not called, then an Open (unencrypted) connection will be created.
+         *
+         * @param pmk A PMK (pairwise master key, see IEEE 802.11i) specifying the key to use for
+         *            encrypting the data-path. Use the {@link #setPskPassphrase(String)} to
+         *            specify a Passphrase.
+         * @return the current {@link Builder} builder, enabling chaining of builder
+         *         methods.
+         * @hide
+         */
+        @SystemApi
+        public @NonNull Builder setPmk(@NonNull byte[] pmk) {
+            if (!WifiAwareUtils.validatePmk(pmk)) {
+                throw new IllegalArgumentException("PMK must 32 bytes");
+            }
+            mPmk = pmk;
+            return this;
+        }
+
+        /**
+         * Configure the port number which will be used to create a connection over this link. This
+         * configuration should only be done on the server device, e.g. the device creating the
+         * {@link java.net.ServerSocket}.
+         * <p>Notes:
+         * <ul>
+         *     <li>The server device must be the Publisher device!
+         *     <li>The port information can only be specified on secure links, specified using
+         *     {@link #setPskPassphrase(String)}.
+         * </ul>
+         *
+         * @param port A positive integer indicating the port to be used for communication.
+         * @return the current {@link Builder} builder, enabling chaining of builder
+         *         methods.
+         */
+        public @NonNull Builder setPort(@IntRange(from = 0, to = 65535) int port) {
+            if (port <= 0 || port > 65535) {
+                throw new IllegalArgumentException("The port must be a positive value (0, 65535]");
+            }
+            mPort = port;
+            return this;
+        }
+
+        /**
+         * Configure the transport protocol which will be used to create a connection over this
+         * link. This configuration should only be done on the server device, e.g. the device
+         * creating the {@link java.net.ServerSocket} for TCP.
+         * <p>Notes:
+         * <ul>
+         *     <li>The server device must be the Publisher device!
+         *     <li>The transport protocol information can only be specified on secure links,
+         *     specified using {@link #setPskPassphrase(String)}.
+         * </ul>
+         * The transport protocol number is assigned by the Internet Assigned Numbers Authority
+         * (IANA) https://www.iana.org/assignments/protocol-numbers/protocol-numbers.xhtml.
+         *
+         * @param transportProtocol The transport protocol to be used for communication.
+         * @return the current {@link Builder} builder, enabling chaining of builder
+         *         methods.
+         */
+        public @NonNull
+                Builder setTransportProtocol(@IntRange(from = 0, to = 255) int transportProtocol) {
+            if (transportProtocol < 0 || transportProtocol > 255) {
+                throw new IllegalArgumentException(
+                        "The transport protocol must be in range [0, 255]");
+            }
+            mTransportProtocol = transportProtocol;
+            return this;
+        }
+
+        /**
+         * Create a {@link android.net.NetworkRequest.Builder#setNetworkSpecifier(NetworkSpecifier)}
+         * for a WiFi Aware connection (link) to the specified peer. The
+         * {@link android.net.NetworkRequest.Builder#addTransportType(int)} should be set to
+         * {@link android.net.NetworkCapabilities#TRANSPORT_WIFI_AWARE}.
+         * <p> The default builder constructor will initialize a NetworkSpecifier which requests an
+         * open (non-encrypted) link. To request an encrypted link use the
+         * {@link #setPskPassphrase(String)} builder method.
+         *
+         * @return A {@link NetworkSpecifier} to be used to construct
+         * {@link android.net.NetworkRequest.Builder#setNetworkSpecifier(NetworkSpecifier)} to pass
+         * to {@link android.net.ConnectivityManager#requestNetwork(android.net.NetworkRequest,
+         * android.net.ConnectivityManager.NetworkCallback)}
+         * [or other varieties of that API].
+         */
+        public @NonNull WifiAwareNetworkSpecifier build() {
+            if (mDiscoverySession == null) {
+                throw new IllegalStateException("Null discovery session!?");
+            }
+            if (mPeerHandle == null) {
+                throw new IllegalStateException("Null peerHandle!?");
+            }
+            if (mPskPassphrase != null & mPmk != null) {
+                throw new IllegalStateException(
+                        "Can only specify a Passphrase or a PMK - not both!");
+            }
+
+            int role = mDiscoverySession instanceof SubscribeDiscoverySession
+                    ? WIFI_AWARE_DATA_PATH_ROLE_INITIATOR
+                    : WifiAwareManager.WIFI_AWARE_DATA_PATH_ROLE_RESPONDER;
+
+            if (mPort != 0 || mTransportProtocol != -1) {
+                if (role != WifiAwareManager.WIFI_AWARE_DATA_PATH_ROLE_RESPONDER) {
+                    throw new IllegalStateException(
+                            "Port and transport protocol information can only "
+                                    + "be specified on the Publisher device (which is the server");
+                }
+                if (TextUtils.isEmpty(mPskPassphrase) && mPmk == null) {
+                    throw new IllegalStateException("Port and transport protocol information can "
+                            + "only be specified on a secure link");
+                }
+            }
+
+            return new WifiAwareNetworkSpecifier(
+                    WifiAwareNetworkSpecifier.NETWORK_SPECIFIER_TYPE_IB, role,
+                    mDiscoverySession.mClientId, mDiscoverySession.mSessionId, mPeerHandle.peerId,
+                    null, mPmk, mPskPassphrase, mPort, mTransportProtocol, Process.myUid());
+        }
+    }
+}
diff --git a/android/net/wifi/aware/WifiAwareSession.java b/android/net/wifi/aware/WifiAwareSession.java
new file mode 100644
index 0000000..3c97813
--- /dev/null
+++ b/android/net/wifi/aware/WifiAwareSession.java
@@ -0,0 +1,342 @@
+/*
+ * 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.wifi.aware;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.net.NetworkSpecifier;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import dalvik.system.CloseGuard;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * This class represents a Wi-Fi Aware session - an attachment to the Wi-Fi Aware service through
+ * which the app can execute discovery operations.
+ */
+public class WifiAwareSession implements AutoCloseable {
+    private static final String TAG = "WifiAwareSession";
+    private static final boolean DBG = false;
+    private static final boolean VDBG = false; // STOPSHIP if true
+
+    private final WeakReference<WifiAwareManager> mMgr;
+    private final Binder mBinder;
+    private final int mClientId;
+
+    private boolean mTerminated = true;
+    private final CloseGuard mCloseGuard = CloseGuard.get();
+
+    /** @hide */
+    public WifiAwareSession(WifiAwareManager manager, Binder binder, int clientId) {
+        if (VDBG) Log.v(TAG, "New session created: manager=" + manager + ", clientId=" + clientId);
+
+        mMgr = new WeakReference<>(manager);
+        mBinder = binder;
+        mClientId = clientId;
+        mTerminated = false;
+
+        mCloseGuard.open("close");
+    }
+
+    /**
+     * Destroy the Wi-Fi Aware service session and, if no other applications are attached to Aware,
+     * also disable Aware. This method destroys all outstanding operations - i.e. all publish and
+     * subscribes are terminated, and any outstanding data-links are shut-down. However, it is
+     * good practice to destroy these discovery sessions and connections explicitly before a
+     * session-wide destroy.
+     * <p>
+     * An application may re-attach after a destroy using
+     * {@link WifiAwareManager#attach(AttachCallback, Handler)} .
+     */
+    @Override
+    public void close() {
+        WifiAwareManager mgr = mMgr.get();
+        if (mgr == null) {
+            Log.w(TAG, "destroy: called post GC on WifiAwareManager");
+            return;
+        }
+        mgr.disconnect(mClientId, mBinder);
+        mTerminated = true;
+        mMgr.clear();
+        mCloseGuard.close();
+    }
+
+    /** @hide */
+    @Override
+    protected void finalize() throws Throwable {
+        try {
+            if (mCloseGuard != null) {
+                mCloseGuard.warnIfOpen();
+            }
+
+            if (!mTerminated) {
+                close();
+            }
+        } finally {
+            super.finalize();
+        }
+    }
+
+    /**
+     * Access the client ID of the Aware session.
+     *
+     * Note: internal visibility for testing.
+     *
+     * @return The internal client ID.
+     *
+     * @hide
+     */
+    @VisibleForTesting
+    public int getClientId() {
+        return mClientId;
+    }
+
+    /**
+     * Issue a request to the Aware service to create a new Aware publish discovery session, using
+     * the specified {@code publishConfig} configuration. The results of the publish operation
+     * are routed to the callbacks of {@link DiscoverySessionCallback}:
+     * <ul>
+     *     <li>
+     *     {@link DiscoverySessionCallback#onPublishStarted(
+     *PublishDiscoverySession)}
+     *     is called when the publish session is created and provides a handle to the session.
+     *     Further operations on the publish session can be executed on that object.
+     *     <li>{@link DiscoverySessionCallback#onSessionConfigFailed()} is called if the
+     *     publish operation failed.
+     * </ul>
+     * <p>
+     * Other results of the publish session operations will also be routed to callbacks
+     * on the {@code callback} object. The resulting publish session can be modified using
+     * {@link PublishDiscoverySession#updatePublish(PublishConfig)}.
+     * <p>
+     *      An application must use the {@link DiscoverySession#close()} to
+     *      terminate the publish discovery session once it isn't needed. This will free
+     *      resources as well terminate any on-air transmissions.
+     * <p>The application must have the {@link android.Manifest.permission#ACCESS_FINE_LOCATION}
+     * permission to start a publish discovery session.
+     *
+     * @param publishConfig The {@link PublishConfig} specifying the
+     *            configuration of the requested publish session.
+     * @param callback A {@link DiscoverySessionCallback} derived object to be used for
+     *                 session event callbacks.
+     * @param handler The Handler on whose thread to execute the callbacks of the {@code
+     * callback} object. If a null is provided then the application's main thread will be used.
+     */
+    public void publish(@NonNull PublishConfig publishConfig,
+            @NonNull DiscoverySessionCallback callback, @Nullable Handler handler) {
+        WifiAwareManager mgr = mMgr.get();
+        if (mgr == null) {
+            Log.e(TAG, "publish: called post GC on WifiAwareManager");
+            return;
+        }
+        if (mTerminated) {
+            Log.e(TAG, "publish: called after termination");
+            return;
+        }
+        mgr.publish(mClientId, (handler == null) ? Looper.getMainLooper() : handler.getLooper(),
+                publishConfig, callback);
+    }
+
+    /**
+     * Issue a request to the Aware service to create a new Aware subscribe discovery session, using
+     * the specified {@code subscribeConfig} configuration. The results of the subscribe
+     * operation are routed to the callbacks of {@link DiscoverySessionCallback}:
+     * <ul>
+     *     <li>
+     *  {@link DiscoverySessionCallback#onSubscribeStarted(
+     *SubscribeDiscoverySession)}
+     *     is called when the subscribe session is created and provides a handle to the session.
+     *     Further operations on the subscribe session can be executed on that object.
+     *     <li>{@link DiscoverySessionCallback#onSessionConfigFailed()} is called if the
+     *     subscribe operation failed.
+     * </ul>
+     * <p>
+     * Other results of the subscribe session operations will also be routed to callbacks
+     * on the {@code callback} object. The resulting subscribe session can be modified using
+     * {@link SubscribeDiscoverySession#updateSubscribe(SubscribeConfig)}.
+     * <p>
+     *      An application must use the {@link DiscoverySession#close()} to
+     *      terminate the subscribe discovery session once it isn't needed. This will free
+     *      resources as well terminate any on-air transmissions.
+     * <p>The application must have the {@link android.Manifest.permission#ACCESS_FINE_LOCATION}
+     * permission to start a subscribe discovery session.
+     *
+     * @param subscribeConfig The {@link SubscribeConfig} specifying the
+     *            configuration of the requested subscribe session.
+     * @param callback A {@link DiscoverySessionCallback} derived object to be used for
+     *                 session event callbacks.
+     * @param handler The Handler on whose thread to execute the callbacks of the {@code
+     * callback} object. If a null is provided then the application's main thread will be used.
+     */
+    public void subscribe(@NonNull SubscribeConfig subscribeConfig,
+            @NonNull DiscoverySessionCallback callback, @Nullable Handler handler) {
+        WifiAwareManager mgr = mMgr.get();
+        if (mgr == null) {
+            Log.e(TAG, "publish: called post GC on WifiAwareManager");
+            return;
+        }
+        if (mTerminated) {
+            Log.e(TAG, "publish: called after termination");
+            return;
+        }
+        mgr.subscribe(mClientId, (handler == null) ? Looper.getMainLooper() : handler.getLooper(),
+                subscribeConfig, callback);
+    }
+
+    /**
+     * Create a {@link android.net.NetworkRequest.Builder#setNetworkSpecifier(NetworkSpecifier)} for
+     * an unencrypted WiFi Aware connection (link) to the specified peer. The
+     * {@link android.net.NetworkRequest.Builder#addTransportType(int)} should be set to
+     * {@link android.net.NetworkCapabilities#TRANSPORT_WIFI_AWARE}.
+     * <p>
+     *     This API is targeted for applications which can obtain the peer MAC address using OOB
+     *     (out-of-band) discovery. Aware discovery does not provide the MAC address of the peer -
+     *     when using Aware discovery use the alternative network specifier method -
+     *     {@link android.net.wifi.aware.WifiAwareNetworkSpecifier.Builder}.
+     * <p>
+     * To set up an encrypted link use the
+     * {@link #createNetworkSpecifierPassphrase(int, byte[], String)} API.
+     *
+     * @param role  The role of this device:
+     *              {@link WifiAwareManager#WIFI_AWARE_DATA_PATH_ROLE_INITIATOR} or
+     *              {@link WifiAwareManager#WIFI_AWARE_DATA_PATH_ROLE_RESPONDER}
+     * @param peer  The MAC address of the peer's Aware discovery interface. On a RESPONDER this
+     *              value is used to gate the acceptance of a connection request from only that
+     *              peer.
+     *
+     * @return A {@link NetworkSpecifier} to be used to construct
+     * {@link android.net.NetworkRequest.Builder#setNetworkSpecifier(NetworkSpecifier)} to pass to
+     * {@link android.net.ConnectivityManager#requestNetwork(android.net.NetworkRequest,
+     * android.net.ConnectivityManager.NetworkCallback)}
+     * [or other varieties of that API].
+     */
+    public NetworkSpecifier createNetworkSpecifierOpen(
+            @WifiAwareManager.DataPathRole int role, @NonNull byte[] peer) {
+        WifiAwareManager mgr = mMgr.get();
+        if (mgr == null) {
+            Log.e(TAG, "createNetworkSpecifierOpen: called post GC on WifiAwareManager");
+            return null;
+        }
+        if (mTerminated) {
+            Log.e(TAG, "createNetworkSpecifierOpen: called after termination");
+            return null;
+        }
+        return mgr.createNetworkSpecifier(mClientId, role, peer, null, null);
+    }
+
+    /**
+     * Create a {@link android.net.NetworkRequest.Builder#setNetworkSpecifier(NetworkSpecifier)} for
+     * an encrypted WiFi Aware connection (link) to the specified peer. The
+     * {@link android.net.NetworkRequest.Builder#addTransportType(int)} should be set to
+     * {@link android.net.NetworkCapabilities#TRANSPORT_WIFI_AWARE}.
+     * <p>
+     *     This API is targeted for applications which can obtain the peer MAC address using OOB
+     *     (out-of-band) discovery. Aware discovery does not provide the MAC address of the peer -
+     *     when using Aware discovery use the alternative network specifier method -
+     *     {@link android.net.wifi.aware.WifiAwareNetworkSpecifier.Builder}.
+     *
+     * @param role  The role of this device:
+     *              {@link WifiAwareManager#WIFI_AWARE_DATA_PATH_ROLE_INITIATOR} or
+     *              {@link WifiAwareManager#WIFI_AWARE_DATA_PATH_ROLE_RESPONDER}
+     * @param peer  The MAC address of the peer's Aware discovery interface. On a RESPONDER this
+     *              value is used to gate the acceptance of a connection request from only that
+     *              peer.
+     * @param passphrase The passphrase to be used to encrypt the link. The PMK is generated from
+     *                   the passphrase. Use {@link #createNetworkSpecifierOpen(int, byte[])} to
+     *                   specify an open (unencrypted) link.
+     *
+     * @return A {@link NetworkSpecifier} to be used to construct
+     * {@link android.net.NetworkRequest.Builder#setNetworkSpecifier(NetworkSpecifier)} to pass to
+     * {@link android.net.ConnectivityManager#requestNetwork(android.net.NetworkRequest,
+     * android.net.ConnectivityManager.NetworkCallback)}
+     * [or other varieties of that API].
+     */
+    public NetworkSpecifier createNetworkSpecifierPassphrase(
+            @WifiAwareManager.DataPathRole int role, @NonNull byte[] peer,
+            @NonNull String passphrase) {
+        WifiAwareManager mgr = mMgr.get();
+        if (mgr == null) {
+            Log.e(TAG, "createNetworkSpecifierPassphrase: called post GC on WifiAwareManager");
+            return null;
+        }
+        if (mTerminated) {
+            Log.e(TAG, "createNetworkSpecifierPassphrase: called after termination");
+            return null;
+        }
+        if (!WifiAwareUtils.validatePassphrase(passphrase)) {
+            throw new IllegalArgumentException("Passphrase must meet length requirements");
+        }
+
+        return mgr.createNetworkSpecifier(mClientId, role, peer, null, passphrase);
+    }
+
+    /**
+     * Create a {@link android.net.NetworkRequest.Builder#setNetworkSpecifier(NetworkSpecifier)} for
+     * an encrypted WiFi Aware connection (link) to the specified peer. The
+     * {@link android.net.NetworkRequest.Builder#addTransportType(int)} should be set to
+     * {@link android.net.NetworkCapabilities#TRANSPORT_WIFI_AWARE}.
+     * <p>
+     *     This API is targeted for applications which can obtain the peer MAC address using OOB
+     *     (out-of-band) discovery. Aware discovery does not provide the MAC address of the peer -
+     *     when using Aware discovery use the alternative network specifier method -
+     *     {@link android.net.wifi.aware.WifiAwareNetworkSpecifier.Builder}.
+     *
+     * @param role  The role of this device:
+     *              {@link WifiAwareManager#WIFI_AWARE_DATA_PATH_ROLE_INITIATOR} or
+     *              {@link WifiAwareManager#WIFI_AWARE_DATA_PATH_ROLE_RESPONDER}
+     * @param peer  The MAC address of the peer's Aware discovery interface. On a RESPONDER this
+     *              value is used to gate the acceptance of a connection request from only that
+     *              peer.
+     * @param pmk A PMK (pairwise master key, see IEEE 802.11i) specifying the key to use for
+     *            encrypting the data-path. Use the
+     *            {@link #createNetworkSpecifierPassphrase(int, byte[], String)} to specify a
+     *            Passphrase or {@link #createNetworkSpecifierOpen(int, byte[])} to specify an
+     *            open (unencrypted) link.
+     *
+     * @return A {@link NetworkSpecifier} to be used to construct
+     * {@link android.net.NetworkRequest.Builder#setNetworkSpecifier(NetworkSpecifier)} to pass to
+     * {@link android.net.ConnectivityManager#requestNetwork(android.net.NetworkRequest,
+     * android.net.ConnectivityManager.NetworkCallback)}
+     * [or other varieties of that API].
+     *
+     * @hide
+     */
+    @SystemApi
+    public NetworkSpecifier createNetworkSpecifierPmk(
+            @WifiAwareManager.DataPathRole int role, @NonNull byte[] peer, @NonNull byte[] pmk) {
+        WifiAwareManager mgr = mMgr.get();
+        if (mgr == null) {
+            Log.e(TAG, "createNetworkSpecifierPmk: called post GC on WifiAwareManager");
+            return null;
+        }
+        if (mTerminated) {
+            Log.e(TAG, "createNetworkSpecifierPmk: called after termination");
+            return null;
+        }
+        if (!WifiAwareUtils.validatePmk(pmk)) {
+            throw new IllegalArgumentException("PMK must 32 bytes");
+        }
+        return mgr.createNetworkSpecifier(mClientId, role, peer, pmk, null);
+    }
+}
diff --git a/android/net/wifi/aware/WifiAwareUtils.java b/android/net/wifi/aware/WifiAwareUtils.java
new file mode 100644
index 0000000..3ece93d
--- /dev/null
+++ b/android/net/wifi/aware/WifiAwareUtils.java
@@ -0,0 +1,106 @@
+/*
+ * 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.wifi.aware;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.hardware.wifi.V1_0.Constants;
+
+/**
+ * Provides utilities for the Wifi Aware manager/service.
+ *
+ * @hide
+ */
+public class WifiAwareUtils {
+    /**
+     * Per spec: The Service Name is a UTF-8 encoded string from 1 to 255 bytes in length. The
+     * only acceptable single-byte UTF-8 symbols for a Service Name are alphanumeric values (A-Z,
+     * a-z, 0-9), the hyphen ('-'), and the period ('.'). All valid multi-byte UTF-8 characters
+     * are acceptable in a Service Name.
+     */
+    public static void validateServiceName(byte[] serviceNameData) throws IllegalArgumentException {
+        if (serviceNameData == null) {
+            throw new IllegalArgumentException("Invalid service name - null");
+        }
+
+        if (serviceNameData.length < 1 || serviceNameData.length > 255) {
+            throw new IllegalArgumentException("Invalid service name length - must be between "
+                    + "1 and 255 bytes (UTF-8 encoding)");
+        }
+
+        int index = 0;
+        while (index < serviceNameData.length) {
+            byte b = serviceNameData[index];
+            if ((b & 0x80) == 0x00) {
+                if (!((b >= '0' && b <= '9') || (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z')
+                        || b == '-' || b == '.')) {
+                    throw new IllegalArgumentException("Invalid service name - illegal characters,"
+                            + " allowed = (0-9, a-z,A-Z, -, .)");
+                }
+            }
+            ++index;
+        }
+    }
+
+    /**
+     * Validates that the passphrase is a non-null string of the right size (per the HAL min/max
+     * length parameters).
+     *
+     * @param passphrase Passphrase to test
+     * @return true if passphrase is valid, false if not
+     */
+    public static boolean validatePassphrase(String passphrase) {
+        if (passphrase == null
+                || passphrase.length() < Constants.NanParamSizeLimits.MIN_PASSPHRASE_LENGTH
+                || passphrase.length() > Constants.NanParamSizeLimits.MAX_PASSPHRASE_LENGTH) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Validates that the PMK is a non-null byte array of the right size (32 bytes per spec).
+     *
+     * @param pmk PMK to test
+     * @return true if PMK is valid, false if not
+     */
+    public static boolean validatePmk(byte[] pmk) {
+        if (pmk == null || pmk.length != 32) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns true if the App version is older than minVersion.
+     */
+    public static boolean isLegacyVersion(Context context, int minVersion) {
+        try {
+            if (context.getPackageManager().getApplicationInfo(context.getOpPackageName(), 0)
+                    .targetSdkVersion < minVersion) {
+                return true;
+            }
+        } catch (PackageManager.NameNotFoundException e) {
+            // In case of exception, assume known app (more strict checking)
+            // Note: This case will never happen since checkPackage is
+            // called to verify valididity before checking App's version.
+        }
+        return false;
+    }
+}
diff --git a/android/net/wifi/hotspot2/ConfigParser.java b/android/net/wifi/hotspot2/ConfigParser.java
new file mode 100644
index 0000000..e8e8731
--- /dev/null
+++ b/android/net/wifi/hotspot2/ConfigParser.java
@@ -0,0 +1,480 @@
+/**
+ * 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.wifi.hotspot2;
+
+import android.net.wifi.hotspot2.omadm.PpsMoParser;
+import android.text.TextUtils;
+import android.util.Base64;
+import android.util.Log;
+import android.util.Pair;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.LineNumberReader;
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.security.PrivateKey;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+import java.security.cert.CertificateFactory;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Utility class for building PasspointConfiguration from an installation file.
+ */
+public final class ConfigParser {
+    private static final String TAG = "ConfigParser";
+
+    // Header names.
+    private static final String CONTENT_TYPE = "Content-Type";
+    private static final String CONTENT_TRANSFER_ENCODING = "Content-Transfer-Encoding";
+
+    // MIME types.
+    private static final String TYPE_MULTIPART_MIXED = "multipart/mixed";
+    private static final String TYPE_WIFI_CONFIG = "application/x-wifi-config";
+    private static final String TYPE_PASSPOINT_PROFILE = "application/x-passpoint-profile";
+    private static final String TYPE_CA_CERT = "application/x-x509-ca-cert";
+    private static final String TYPE_PKCS12 = "application/x-pkcs12";
+
+    private static final String ENCODING_BASE64 = "base64";
+    private static final String BOUNDARY = "boundary=";
+
+    /**
+     * Class represent a MIME (Multipurpose Internet Mail Extension) part.
+     */
+    private static class MimePart {
+        /**
+         * Content type of the part.
+         */
+        public String type = null;
+
+        /**
+         * Decoded data.
+         */
+        public byte[] data = null;
+
+        /**
+         * Flag indicating if this is the last part (ending with --{boundary}--).
+         */
+        public boolean isLast = false;
+    }
+
+    /**
+     * Class represent the MIME (Multipurpose Internet Mail Extension) header.
+     */
+    private static class MimeHeader {
+        /**
+         * Content type.
+         */
+        public String contentType = null;
+
+        /**
+         * Boundary string (optional), only applies for the outter MIME header.
+         */
+        public String boundary = null;
+
+        /**
+         * Encoding type.
+         */
+        public String encodingType = null;
+    }
+
+    /**
+     * @hide
+     */
+    public ConfigParser() {}
+
+    /**
+     * Parse the Hotspot 2.0 Release 1 configuration data into a {@link PasspointConfiguration}
+     * object.  The configuration data is a base64 encoded MIME multipart data.  Below is
+     * the format of the decoded message:
+     *
+     * Content-Type: multipart/mixed; boundary={boundary}
+     * Content-Transfer-Encoding: base64
+     * [Skip uninterested headers]
+     *
+     * --{boundary}
+     * Content-Type: application/x-passpoint-profile
+     * Content-Transfer-Encoding: base64
+     *
+     * [base64 encoded Passpoint profile data]
+     * --{boundary}
+     * Content-Type: application/x-x509-ca-cert
+     * Content-Transfer-Encoding: base64
+     *
+     * [base64 encoded X509 CA certificate data]
+     * --{boundary}
+     * Content-Type: application/x-pkcs12
+     * Content-Transfer-Encoding: base64
+     *
+     * [base64 encoded PKCS#12 ASN.1 structure containing client certificate chain]
+     * --{boundary}
+     *
+     * @param mimeType MIME type of the encoded data.
+     * @param data A base64 encoded MIME multipart message containing the Passpoint profile
+     *             (required), CA (Certificate Authority) certificate (optional), and client
+     *             certificate chain (optional).
+     * @return {@link PasspointConfiguration}
+     */
+    public static PasspointConfiguration parsePasspointConfig(String mimeType, byte[] data) {
+        // Verify MIME type.
+        if (!TextUtils.equals(mimeType, TYPE_WIFI_CONFIG)) {
+            Log.e(TAG, "Unexpected MIME type: " + mimeType);
+            return null;
+        }
+
+        try {
+            // Decode the data.
+            byte[] decodedData = Base64.decode(new String(data, StandardCharsets.ISO_8859_1),
+                    Base64.DEFAULT);
+            Map<String, byte[]> mimeParts = parseMimeMultipartMessage(new LineNumberReader(
+                    new InputStreamReader(new ByteArrayInputStream(decodedData),
+                            StandardCharsets.ISO_8859_1)));
+            return createPasspointConfig(mimeParts);
+        } catch (IOException | IllegalArgumentException e) {
+            Log.e(TAG, "Failed to parse installation file: " + e.getMessage());
+            return null;
+        }
+    }
+
+    /**
+     * Create a {@link PasspointConfiguration} object from list of MIME (Multipurpose Internet
+     * Mail Extension) parts.
+     *
+     * @param mimeParts Map of content type and content data.
+     * @return {@link PasspointConfiguration}
+     * @throws IOException
+     */
+    private static PasspointConfiguration createPasspointConfig(Map<String, byte[]> mimeParts)
+            throws IOException {
+        byte[] profileData = mimeParts.get(TYPE_PASSPOINT_PROFILE);
+        if (profileData == null) {
+            throw new IOException("Missing Passpoint Profile");
+        }
+
+        PasspointConfiguration config = PpsMoParser.parseMoText(new String(profileData));
+        if (config == null) {
+            throw new IOException("Failed to parse Passpoint profile");
+        }
+
+        // Credential is needed for storing the certificates and private client key.
+        if (config.getCredential() == null) {
+            throw new IOException("Passpoint profile missing credential");
+        }
+
+        // Parse CA (Certificate Authority) certificate.
+        byte[] caCertData = mimeParts.get(TYPE_CA_CERT);
+        if (caCertData != null) {
+            try {
+                config.getCredential().setCaCertificate(parseCACert(caCertData));
+            } catch (CertificateException e) {
+                throw new IOException("Failed to parse CA Certificate");
+            }
+        }
+
+        // Parse PKCS12 data for client private key and certificate chain.
+        byte[] pkcs12Data = mimeParts.get(TYPE_PKCS12);
+        if (pkcs12Data != null) {
+            try {
+                Pair<PrivateKey, List<X509Certificate>> clientKey = parsePkcs12(pkcs12Data);
+                config.getCredential().setClientPrivateKey(clientKey.first);
+                config.getCredential().setClientCertificateChain(
+                        clientKey.second.toArray(new X509Certificate[clientKey.second.size()]));
+            } catch(GeneralSecurityException | IOException e) {
+                throw new IOException("Failed to parse PCKS12 string");
+            }
+        }
+        return config;
+    }
+
+    /**
+     * Parse a MIME (Multipurpose Internet Mail Extension) multipart message from the given
+     * input stream.
+     *
+     * @param in The input stream for reading the message data
+     * @return A map of a content type and content data pair
+     * @throws IOException
+     */
+    private static Map<String, byte[]> parseMimeMultipartMessage(LineNumberReader in)
+            throws IOException {
+        // Parse the outer MIME header.
+        MimeHeader header = parseHeaders(in);
+        if (!TextUtils.equals(header.contentType, TYPE_MULTIPART_MIXED)) {
+            throw new IOException("Invalid content type: " + header.contentType);
+        }
+        if (TextUtils.isEmpty(header.boundary)) {
+            throw new IOException("Missing boundary string");
+        }
+        if (!TextUtils.equals(header.encodingType, ENCODING_BASE64)) {
+            throw new IOException("Unexpected encoding: " + header.encodingType);
+        }
+
+        // Read pass the first boundary string.
+        for (;;) {
+            String line = in.readLine();
+            if (line == null) {
+                throw new IOException("Unexpected EOF before first boundary @ " +
+                        in.getLineNumber());
+            }
+            if (line.equals("--" + header.boundary)) {
+                break;
+            }
+        }
+
+        // Parse each MIME part.
+        Map<String, byte[]> mimeParts = new HashMap<>();
+        boolean isLast = false;
+        do {
+            MimePart mimePart = parseMimePart(in, header.boundary);
+            mimeParts.put(mimePart.type, mimePart.data);
+            isLast = mimePart.isLast;
+        } while(!isLast);
+        return mimeParts;
+    }
+
+    /**
+     * Parse a MIME (Multipurpose Internet Mail Extension) part.  We expect the data to
+     * be encoded in base64.
+     *
+     * @param in Input stream to read the data from
+     * @param boundary Boundary string indicate the end of the part
+     * @return {@link MimePart}
+     * @throws IOException
+     */
+    private static MimePart parseMimePart(LineNumberReader in, String boundary)
+            throws IOException {
+        MimeHeader header = parseHeaders(in);
+        // Expect encoding type to be base64.
+        if (!TextUtils.equals(header.encodingType, ENCODING_BASE64)) {
+            throw new IOException("Unexpected encoding type: " + header.encodingType);
+        }
+
+        // Check for a valid content type.
+        if (!TextUtils.equals(header.contentType, TYPE_PASSPOINT_PROFILE) &&
+                !TextUtils.equals(header.contentType, TYPE_CA_CERT) &&
+                !TextUtils.equals(header.contentType, TYPE_PKCS12)) {
+            throw new IOException("Unexpected content type: " + header.contentType);
+        }
+
+        StringBuilder text = new StringBuilder();
+        boolean isLast = false;
+        String partBoundary = "--" + boundary;
+        String endBoundary = partBoundary + "--";
+        for (;;) {
+            String line = in.readLine();
+            if (line == null) {
+                throw new IOException("Unexpected EOF file in body @ " + in.getLineNumber());
+            }
+            // Check for boundary line.
+            if (line.startsWith(partBoundary)) {
+                if (line.equals(endBoundary)) {
+                    isLast = true;
+                }
+                break;
+            }
+            text.append(line);
+        }
+
+        MimePart part = new MimePart();
+        part.type = header.contentType;
+        part.data = Base64.decode(text.toString(), Base64.DEFAULT);
+        part.isLast = isLast;
+        return part;
+    }
+
+    /**
+     * Parse a MIME (Multipurpose Internet Mail Extension) header from the input stream.
+     * @param in Input stream to read from.
+     * @return {@link MimeHeader}
+     * @throws IOException
+     */
+    private static MimeHeader parseHeaders(LineNumberReader in)
+            throws IOException {
+        MimeHeader header = new MimeHeader();
+
+        // Read the header from the input stream.
+        Map<String, String> headers = readHeaders(in);
+
+        // Parse each header.
+        for (Map.Entry<String, String> entry : headers.entrySet()) {
+            switch (entry.getKey()) {
+                case CONTENT_TYPE:
+                    Pair<String, String> value = parseContentType(entry.getValue());
+                    header.contentType = value.first;
+                    header.boundary = value.second;
+                    break;
+                case CONTENT_TRANSFER_ENCODING:
+                    header.encodingType = entry.getValue();
+                    break;
+                default:
+                    Log.d(TAG, "Ignore header: " + entry.getKey());
+                    break;
+            }
+        }
+        return header;
+    }
+
+    /**
+     * Parse the Content-Type header value.  The value will contain the content type string and
+     * an optional boundary string separated by a ";".  Below are examples of valid Content-Type
+     * header value:
+     *   multipart/mixed; boundary={boundary}
+     *   application/x-passpoint-profile
+     *
+     * @param contentType The Content-Type value string
+     * @return A pair of content type and boundary string
+     * @throws IOException
+     */
+    private static Pair<String, String> parseContentType(String contentType) throws IOException {
+        String[] attributes = contentType.split(";");
+        String type = null;
+        String boundary = null;
+
+        if (attributes.length < 1) {
+            throw new IOException("Invalid Content-Type: " + contentType);
+        }
+
+        // The type is always the first attribute.
+        type = attributes[0].trim();
+        // Look for boundary string from the rest of the attributes.
+        for (int i = 1; i < attributes.length; i++) {
+            String attribute = attributes[i].trim();
+            if (!attribute.startsWith(BOUNDARY)) {
+                Log.d(TAG, "Ignore Content-Type attribute: " + attributes[i]);
+                continue;
+            }
+            boundary = attribute.substring(BOUNDARY.length());
+            // Remove the leading and trailing quote if present.
+            if (boundary.length() > 1 && boundary.startsWith("\"") && boundary.endsWith("\"")) {
+                boundary = boundary.substring(1, boundary.length()-1);
+            }
+        }
+
+        return new Pair<String, String>(type, boundary);
+    }
+
+    /**
+     * Read the headers from the given input stream.  The header section is terminated by
+     * an empty line.
+     *
+     * @param in The input stream to read from
+     * @return Map of key-value pairs.
+     * @throws IOException
+     */
+    private static Map<String, String> readHeaders(LineNumberReader in)
+            throws IOException {
+        Map<String, String> headers = new HashMap<>();
+        String line;
+        String name = null;
+        StringBuilder value = null;
+        for (;;) {
+            line = in.readLine();
+            if (line == null) {
+                throw new IOException("Missing line @ " + in.getLineNumber());
+            }
+
+            // End of headers section.
+            if (line.length() == 0 || line.trim().length() == 0) {
+                // Save the previous header line.
+                if (name != null) {
+                    headers.put(name, value.toString());
+                }
+                break;
+            }
+
+            int nameEnd = line.indexOf(':');
+            if (nameEnd < 0) {
+                if (value != null) {
+                    // Continuation line for the header value.
+                    value.append(' ').append(line.trim());
+                } else {
+                    throw new IOException("Bad header line: '" + line + "' @ " +
+                            in.getLineNumber());
+                }
+            } else {
+                // New header line detected, make sure it doesn't start with a whitespace.
+                if (Character.isWhitespace(line.charAt(0))) {
+                    throw new IOException("Illegal blank prefix in header line '" + line +
+                            "' @ " + in.getLineNumber());
+                }
+
+                if (name != null) {
+                    // Save the previous header line.
+                    headers.put(name, value.toString());
+                }
+
+                // Setup the current header line.
+                name = line.substring(0, nameEnd).trim();
+                value = new StringBuilder();
+                value.append(line.substring(nameEnd+1).trim());
+            }
+        }
+        return headers;
+    }
+
+    /**
+     * Parse a CA (Certificate Authority) certificate data and convert it to a
+     * X509Certificate object.
+     *
+     * @param octets Certificate data
+     * @return X509Certificate
+     * @throws CertificateException
+     */
+    private static X509Certificate parseCACert(byte[] octets) throws CertificateException {
+        CertificateFactory factory = CertificateFactory.getInstance("X.509");
+        return (X509Certificate) factory.generateCertificate(new ByteArrayInputStream(octets));
+    }
+
+    private static Pair<PrivateKey, List<X509Certificate>> parsePkcs12(byte[] octets)
+            throws GeneralSecurityException, IOException {
+        KeyStore ks = KeyStore.getInstance("PKCS12");
+        ByteArrayInputStream in = new ByteArrayInputStream(octets);
+        ks.load(in, new char[0]);
+        in.close();
+
+        // Only expects one set of key and certificate chain.
+        if (ks.size() != 1) {
+            throw new IOException("Unexpected key size: " + ks.size());
+        }
+
+        String alias = ks.aliases().nextElement();
+        if (alias == null) {
+            throw new IOException("No alias found");
+        }
+
+        PrivateKey clientKey = (PrivateKey) ks.getKey(alias, null);
+        List<X509Certificate> clientCertificateChain = null;
+        Certificate[] chain = ks.getCertificateChain(alias);
+        if (chain != null) {
+            clientCertificateChain = new ArrayList<>();
+            for (Certificate certificate : chain) {
+                if (!(certificate instanceof X509Certificate)) {
+                    throw new IOException("Unexpceted certificate type: " +
+                            certificate.getClass());
+                }
+                clientCertificateChain.add((X509Certificate) certificate);
+            }
+        }
+        return new Pair<PrivateKey, List<X509Certificate>>(clientKey, clientCertificateChain);
+    }
+}
diff --git a/android/net/wifi/hotspot2/OsuProvider.java b/android/net/wifi/hotspot2/OsuProvider.java
new file mode 100644
index 0000000..57dff9d
--- /dev/null
+++ b/android/net/wifi/hotspot2/OsuProvider.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License
+ */
+
+package android.net.wifi.hotspot2;
+
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.net.wifi.WifiSsid;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Contained information for a Hotspot 2.0 OSU (Online Sign-Up provider).
+ *
+ * @hide
+ */
+@SystemApi
+public final class OsuProvider implements Parcelable {
+    /**
+     * OSU (Online Sign-Up) method: OMA DM (Open Mobile Alliance Device Management).
+     * For more info, refer to Section 8.3 of the Hotspot 2.0 Release 2 Technical Specification.
+     * @hide
+     */
+    public static final int METHOD_OMA_DM = 0;
+
+    /**
+     * OSU (Online Sign-Up) method: SOAP XML SPP (Subscription Provisioning Protocol).
+     * For more info, refer to Section 8.4 of the Hotspot 2.0 Release 2 Technical Specification.
+     * @hide
+     */
+    public static final int METHOD_SOAP_XML_SPP = 1;
+
+    /**
+     * SSID of the network to connect for service sign-up.
+     */
+    private WifiSsid mOsuSsid;
+
+    /**
+     * Map of friendly names expressed as different language for the OSU provider.
+     */
+    private final Map<String, String> mFriendlyNames;
+
+    /**
+     * Description of the OSU provider.
+     */
+    private final String mServiceDescription;
+
+    /**
+     * URI to browse to for service sign-up.
+     */
+    private final Uri mServerUri;
+
+    /**
+     * Network Access Identifier used for authenticating with the OSU network when OSEN is used.
+     */
+    private final String mNetworkAccessIdentifier;
+
+    /**
+     * List of OSU (Online Sign-Up) method supported.
+     */
+    private final List<Integer> mMethodList;
+
+    /**
+     * Icon data for the OSU (Online Sign-Up) provider.
+     */
+    private final Icon mIcon;
+
+    /** @hide */
+    public OsuProvider(WifiSsid osuSsid, Map<String, String> friendlyNames,
+            String serviceDescription, Uri serverUri, String nai, List<Integer> methodList,
+            Icon icon) {
+        mOsuSsid = osuSsid;
+        mFriendlyNames = friendlyNames;
+        mServiceDescription = serviceDescription;
+        mServerUri = serverUri;
+        mNetworkAccessIdentifier = nai;
+        if (methodList == null) {
+            mMethodList = new ArrayList<>();
+        } else {
+            mMethodList = new ArrayList<>(methodList);
+        }
+        mIcon = icon;
+    }
+
+    /**
+     * Copy constructor.
+     *
+     * @param source The source to copy from
+     * @hide
+     */
+    public OsuProvider(OsuProvider source) {
+        if (source == null) {
+            mOsuSsid = null;
+            mFriendlyNames = null;
+            mServiceDescription = null;
+            mServerUri = null;
+            mNetworkAccessIdentifier = null;
+            mMethodList = new ArrayList<>();
+            mIcon = null;
+            return;
+        }
+
+        mOsuSsid = source.mOsuSsid;
+        mFriendlyNames = source.mFriendlyNames;
+        mServiceDescription = source.mServiceDescription;
+        mServerUri = source.mServerUri;
+        mNetworkAccessIdentifier = source.mNetworkAccessIdentifier;
+        if (source.mMethodList == null) {
+            mMethodList = new ArrayList<>();
+        } else {
+            mMethodList = new ArrayList<>(source.mMethodList);
+        }
+        mIcon = source.mIcon;
+    }
+
+    /** @hide */
+    public WifiSsid getOsuSsid() {
+        return mOsuSsid;
+    }
+
+    /** @hide */
+    public void setOsuSsid(WifiSsid osuSsid) {
+        mOsuSsid = osuSsid;
+    }
+
+    /**
+     * Return the friendly Name for current language from the list of friendly names of OSU
+     * provider.
+     *
+     * The string matching the default locale will be returned if it is found, otherwise the string
+     * in english or the first string in the list will be returned if english is not found.
+     * A null will be returned if the list is empty.
+     *
+     * @return String matching the default locale, null otherwise
+     */
+    public @Nullable String getFriendlyName() {
+        if (mFriendlyNames == null || mFriendlyNames.isEmpty()) return null;
+        String lang = Locale.getDefault().getLanguage();
+        String friendlyName = mFriendlyNames.get(lang);
+        if (friendlyName != null) {
+            return friendlyName;
+        }
+        friendlyName = mFriendlyNames.get("en");
+        if (friendlyName != null) {
+            return friendlyName;
+        }
+        return mFriendlyNames.get(mFriendlyNames.keySet().stream().findFirst().get());
+    }
+
+    /** @hide */
+    public Map<String, String> getFriendlyNameList() {
+        return mFriendlyNames;
+    }
+
+    /** @hide */
+    public String getServiceDescription() {
+        return mServiceDescription;
+    }
+
+    public @Nullable Uri getServerUri() {
+        return mServerUri;
+    }
+
+    /** @hide */
+    public String getNetworkAccessIdentifier() {
+        return mNetworkAccessIdentifier;
+    }
+
+    /** @hide */
+    public List<Integer> getMethodList() {
+        return mMethodList;
+    }
+
+    /** @hide */
+    public Icon getIcon() {
+        return mIcon;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeParcelable(mOsuSsid, flags);
+        dest.writeString(mServiceDescription);
+        dest.writeParcelable(mServerUri, flags);
+        dest.writeString(mNetworkAccessIdentifier);
+        dest.writeList(mMethodList);
+        dest.writeParcelable(mIcon, flags);
+        Bundle bundle = new Bundle();
+        bundle.putSerializable("friendlyNameMap", (HashMap<String, String>) mFriendlyNames);
+        dest.writeBundle(bundle);
+    }
+
+    @Override
+    public boolean equals(Object thatObject) {
+        if (this == thatObject) {
+            return true;
+        }
+        if (!(thatObject instanceof OsuProvider)) {
+            return false;
+        }
+        OsuProvider that = (OsuProvider) thatObject;
+        return (mOsuSsid == null ? that.mOsuSsid == null : mOsuSsid.equals(that.mOsuSsid))
+                && (mFriendlyNames == null) ? that.mFriendlyNames == null
+                            : mFriendlyNames.equals(that.mFriendlyNames)
+                && TextUtils.equals(mServiceDescription, that.mServiceDescription)
+                && (mServerUri == null ? that.mServerUri == null
+                            : mServerUri.equals(that.mServerUri))
+                && TextUtils.equals(mNetworkAccessIdentifier, that.mNetworkAccessIdentifier)
+                && (mMethodList == null ? that.mMethodList == null
+                            : mMethodList.equals(that.mMethodList))
+                && (mIcon == null ? that.mIcon == null : mIcon.sameAs(that.mIcon));
+    }
+
+    @Override
+    public int hashCode() {
+        // mIcon is not hashable, skip the variable.
+        return Objects.hash(mOsuSsid, mServiceDescription, mFriendlyNames,
+                mServerUri, mNetworkAccessIdentifier, mMethodList);
+    }
+
+    @Override
+    public String toString() {
+        return "OsuProvider{mOsuSsid=" + mOsuSsid
+                + " mFriendlyNames=" + mFriendlyNames
+                + " mServiceDescription=" + mServiceDescription
+                + " mServerUri=" + mServerUri
+                + " mNetworkAccessIdentifier=" + mNetworkAccessIdentifier
+                + " mMethodList=" + mMethodList
+                + " mIcon=" + mIcon;
+    }
+
+    public static final @android.annotation.NonNull Creator<OsuProvider> CREATOR =
+            new Creator<OsuProvider>() {
+                @Override
+                public OsuProvider createFromParcel(Parcel in) {
+                    WifiSsid osuSsid = in.readParcelable(null);
+                    String serviceDescription = in.readString();
+                    Uri serverUri = in.readParcelable(null);
+                    String nai = in.readString();
+                    List<Integer> methodList = new ArrayList<>();
+                    in.readList(methodList, null);
+                    Icon icon = in.readParcelable(null);
+                    Bundle bundle = in.readBundle();
+                    Map<String, String> friendlyNamesMap = (HashMap) bundle.getSerializable(
+                            "friendlyNameMap");
+                    return new OsuProvider(osuSsid, friendlyNamesMap, serviceDescription,
+                            serverUri, nai, methodList, icon);
+                }
+
+            @Override
+            public OsuProvider[] newArray(int size) {
+                return new OsuProvider[size];
+            }
+        };
+}
diff --git a/android/net/wifi/hotspot2/PasspointConfiguration.java b/android/net/wifi/hotspot2/PasspointConfiguration.java
new file mode 100644
index 0000000..9095b5d
--- /dev/null
+++ b/android/net/wifi/hotspot2/PasspointConfiguration.java
@@ -0,0 +1,698 @@
+/**
+ * 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.wifi.hotspot2;
+
+import android.net.wifi.hotspot2.pps.Credential;
+import android.net.wifi.hotspot2.pps.HomeSp;
+import android.net.wifi.hotspot2.pps.Policy;
+import android.net.wifi.hotspot2.pps.UpdateParameter;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Class representing Passpoint configuration.  This contains configurations specified in
+ * PerProviderSubscription (PPS) Management Object (MO) tree.
+ *
+ * For more info, refer to Hotspot 2.0 PPS MO defined in section 9.1 of the Hotspot 2.0
+ * Release 2 Technical Specification.
+ */
+public final class PasspointConfiguration implements Parcelable {
+    private static final String TAG = "PasspointConfiguration";
+
+    /**
+     * Number of bytes for certificate SHA-256 fingerprint byte array.
+     */
+    private static final int CERTIFICATE_SHA256_BYTES = 32;
+
+    /**
+     * Maximum bytes for URL string.
+     */
+    private static final int MAX_URL_BYTES = 1023;
+
+    /**
+     * Integer value used for indicating null value in the Parcel.
+     */
+    private static final int NULL_VALUE = -1;
+
+    /**
+     * Configurations under HomeSp subtree.
+     */
+    private HomeSp mHomeSp = null;
+    /**
+     * Set the Home SP (Service Provider) information.
+     *
+     * @param homeSp The Home SP information to set to
+     */
+    public void setHomeSp(HomeSp homeSp) { mHomeSp = homeSp; }
+    /**
+     * Get the Home SP (Service Provider) information.
+     *
+     * @return Home SP information
+     */
+    public HomeSp getHomeSp() { return mHomeSp; }
+
+    /**
+     * Configurations under Credential subtree.
+     */
+    private Credential mCredential = null;
+    /**
+     * Set the credential information.
+     *
+     * @param credential The credential information to set to
+     */
+    public void setCredential(Credential credential) {
+        mCredential = credential;
+    }
+    /**
+     * Get the credential information.
+     *
+     * @return credential information
+     */
+    public Credential getCredential() {
+        return mCredential;
+    }
+
+    /**
+     * Configurations under Policy subtree.
+     */
+    private Policy mPolicy = null;
+    /**
+     * @hide
+     */
+    public void setPolicy(Policy policy) {
+        mPolicy = policy;
+    }
+    /**
+     * @hide
+     */
+    public Policy getPolicy() {
+        return mPolicy;
+    }
+
+    /**
+     * Meta data for performing subscription update.
+     */
+    private UpdateParameter mSubscriptionUpdate = null;
+    /**
+     * @hide
+     */
+    public void setSubscriptionUpdate(UpdateParameter subscriptionUpdate) {
+        mSubscriptionUpdate = subscriptionUpdate;
+    }
+    /**
+     * @hide
+     */
+    public UpdateParameter getSubscriptionUpdate() {
+        return mSubscriptionUpdate;
+    }
+
+    /**
+     * List of HTTPS URL for retrieving trust root certificate and the corresponding SHA-256
+     * fingerprint of the certificate.  The certificates are used for verifying AAA server's
+     * identity during EAP authentication.
+     */
+    private Map<String, byte[]> mTrustRootCertList = null;
+    /**
+     * @hide
+     */
+    public void setTrustRootCertList(Map<String, byte[]> trustRootCertList) {
+        mTrustRootCertList = trustRootCertList;
+    }
+    /**
+     * @hide
+     */
+    public Map<String, byte[]> getTrustRootCertList() {
+        return mTrustRootCertList;
+    }
+
+    /**
+     * Set by the subscription server, updated every time the configuration is updated by
+     * the subscription server.
+     *
+     * Use Integer.MIN_VALUE to indicate unset value.
+     */
+    private int mUpdateIdentifier = Integer.MIN_VALUE;
+    /**
+     * @hide
+     */
+    public void setUpdateIdentifier(int updateIdentifier) {
+        mUpdateIdentifier = updateIdentifier;
+    }
+    /**
+     * @hide
+     */
+    public int getUpdateIdentifier() {
+        return mUpdateIdentifier;
+    }
+
+    /**
+     * The priority of the credential.
+     *
+     * Use Integer.MIN_VALUE to indicate unset value.
+     */
+    private int mCredentialPriority = Integer.MIN_VALUE;
+    /**
+     * @hide
+     */
+    public void setCredentialPriority(int credentialPriority) {
+        mCredentialPriority = credentialPriority;
+    }
+    /**
+     * @hide
+     */
+    public int getCredentialPriority() {
+        return mCredentialPriority;
+    }
+
+    /**
+     * The time this subscription is created. It is in the format of number
+     * of milliseconds since January 1, 1970, 00:00:00 GMT.
+     *
+     * Use Long.MIN_VALUE to indicate unset value.
+     */
+    private long mSubscriptionCreationTimeInMillis = Long.MIN_VALUE;
+    /**
+     * @hide
+     */
+    public void setSubscriptionCreationTimeInMillis(long subscriptionCreationTimeInMillis) {
+        mSubscriptionCreationTimeInMillis = subscriptionCreationTimeInMillis;
+    }
+    /**
+     * @hide
+     */
+    public long getSubscriptionCreationTimeInMillis() {
+        return mSubscriptionCreationTimeInMillis;
+    }
+
+    /**
+     * The time this subscription will expire. It is in the format of number
+     * of milliseconds since January 1, 1970, 00:00:00 GMT.
+     *
+     * Use Long.MIN_VALUE to indicate unset value.
+     */
+    private long mSubscriptionExpirationTimeInMillis = Long.MIN_VALUE;
+    /**
+     * @hide
+     */
+    public void setSubscriptionExpirationTimeInMillis(long subscriptionExpirationTimeInMillis) {
+        mSubscriptionExpirationTimeInMillis = subscriptionExpirationTimeInMillis;
+    }
+    /**
+     * @hide
+     */
+    public long getSubscriptionExpirationTimeInMillis() {
+        return mSubscriptionExpirationTimeInMillis;
+    }
+
+    /**
+     * The type of the subscription.  This is defined by the provider and the value is provider
+     * specific.
+     */
+    private String mSubscriptionType = null;
+    /**
+     * @hide
+     */
+    public void setSubscriptionType(String subscriptionType) {
+        mSubscriptionType = subscriptionType;
+    }
+    /**
+     * @hide
+     */
+    public String getSubscriptionType() {
+        return mSubscriptionType;
+    }
+
+    /**
+     * The time period for usage statistics accumulation. A value of zero means that usage
+     * statistics are not accumulated on a periodic basis (e.g., a one-time limit for
+     * “pay as you go” - PAYG service). A non-zero value specifies the usage interval in minutes.
+     */
+    private long mUsageLimitUsageTimePeriodInMinutes = Long.MIN_VALUE;
+    /**
+     * @hide
+     */
+    public void setUsageLimitUsageTimePeriodInMinutes(long usageLimitUsageTimePeriodInMinutes) {
+        mUsageLimitUsageTimePeriodInMinutes = usageLimitUsageTimePeriodInMinutes;
+    }
+    /**
+     * @hide
+     */
+    public long getUsageLimitUsageTimePeriodInMinutes() {
+        return mUsageLimitUsageTimePeriodInMinutes;
+    }
+
+    /**
+     * The time at which usage statistic accumulation  begins.  It is in the format of number
+     * of milliseconds since January 1, 1970, 00:00:00 GMT.
+     *
+     * Use Long.MIN_VALUE to indicate unset value.
+     */
+    private long mUsageLimitStartTimeInMillis = Long.MIN_VALUE;
+    /**
+     * @hide
+     */
+    public void setUsageLimitStartTimeInMillis(long usageLimitStartTimeInMillis) {
+        mUsageLimitStartTimeInMillis = usageLimitStartTimeInMillis;
+    }
+    /**
+     * @hide
+     */
+    public long getUsageLimitStartTimeInMillis() {
+        return mUsageLimitStartTimeInMillis;
+    }
+
+    /**
+     * The cumulative data limit in megabytes for the {@link #usageLimitUsageTimePeriodInMinutes}.
+     * A value of zero indicate unlimited data usage.
+     *
+     * Use Long.MIN_VALUE to indicate unset value.
+     */
+    private long mUsageLimitDataLimit = Long.MIN_VALUE;
+    /**
+     * @hide
+     */
+    public void setUsageLimitDataLimit(long usageLimitDataLimit) {
+        mUsageLimitDataLimit = usageLimitDataLimit;
+    }
+    /**
+     * @hide
+     */
+    public long getUsageLimitDataLimit() {
+        return mUsageLimitDataLimit;
+    }
+
+    /**
+     * The cumulative time limit in minutes for the {@link #usageLimitUsageTimePeriodInMinutes}.
+     * A value of zero indicate unlimited time usage.
+     */
+    private long mUsageLimitTimeLimitInMinutes = Long.MIN_VALUE;
+    /**
+     * @hide
+     */
+    public void setUsageLimitTimeLimitInMinutes(long usageLimitTimeLimitInMinutes) {
+        mUsageLimitTimeLimitInMinutes = usageLimitTimeLimitInMinutes;
+    }
+    /**
+     * @hide
+     */
+    public long getUsageLimitTimeLimitInMinutes() {
+        return mUsageLimitTimeLimitInMinutes;
+    }
+
+    /**
+     * The map of OSU service provider names whose each element is presented in different
+     * languages for the service provider, which is used for finding a matching
+     * PasspointConfiguration with a given service provider name.
+     */
+    private Map<String, String> mServiceFriendlyNames = null;
+
+    /**
+     * @hide
+     */
+    public void setServiceFriendlyNames(Map<String, String> serviceFriendlyNames) {
+        mServiceFriendlyNames = serviceFriendlyNames;
+    }
+
+    /**
+     * @hide
+     */
+    public Map<String, String> getServiceFriendlyNames() {
+        return mServiceFriendlyNames;
+    }
+
+    /**
+     * Return the friendly Name for current language from the list of friendly names of OSU
+     * provider.
+     * The string matching the default locale will be returned if it is found, otherwise the
+     * first string in the list will be returned.  A null will be returned if the list is empty.
+     *
+     * @return String matching the default locale, null otherwise
+     * @hide
+     */
+    public String getServiceFriendlyName() {
+        if (mServiceFriendlyNames == null || mServiceFriendlyNames.isEmpty()) return null;
+        String lang = Locale.getDefault().getLanguage();
+        String friendlyName = mServiceFriendlyNames.get(lang);
+        if (friendlyName != null) {
+            return friendlyName;
+        }
+        friendlyName = mServiceFriendlyNames.get("en");
+        if (friendlyName != null) {
+            return friendlyName;
+        }
+        return mServiceFriendlyNames.get(mServiceFriendlyNames.keySet().stream().findFirst().get());
+    }
+
+    /**
+     * Constructor for creating PasspointConfiguration with default values.
+     */
+    public PasspointConfiguration() {}
+
+    /**
+     * Copy constructor.
+     *
+     * @param source The source to copy from
+     */
+    public PasspointConfiguration(PasspointConfiguration source) {
+        if (source == null) {
+            return;
+        }
+
+        if (source.mHomeSp != null) {
+            mHomeSp = new HomeSp(source.mHomeSp);
+        }
+        if (source.mCredential != null) {
+            mCredential = new Credential(source.mCredential);
+        }
+        if (source.mPolicy != null) {
+            mPolicy = new Policy(source.mPolicy);
+        }
+        if (source.mTrustRootCertList != null) {
+            mTrustRootCertList = Collections.unmodifiableMap(source.mTrustRootCertList);
+        }
+        if (source.mSubscriptionUpdate != null) {
+            mSubscriptionUpdate = new UpdateParameter(source.mSubscriptionUpdate);
+        }
+        mUpdateIdentifier = source.mUpdateIdentifier;
+        mCredentialPriority = source.mCredentialPriority;
+        mSubscriptionCreationTimeInMillis = source.mSubscriptionCreationTimeInMillis;
+        mSubscriptionExpirationTimeInMillis = source.mSubscriptionExpirationTimeInMillis;
+        mSubscriptionType = source.mSubscriptionType;
+        mUsageLimitDataLimit = source.mUsageLimitDataLimit;
+        mUsageLimitStartTimeInMillis = source.mUsageLimitStartTimeInMillis;
+        mUsageLimitTimeLimitInMinutes = source.mUsageLimitTimeLimitInMinutes;
+        mUsageLimitUsageTimePeriodInMinutes = source.mUsageLimitUsageTimePeriodInMinutes;
+        mServiceFriendlyNames = source.mServiceFriendlyNames;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeParcelable(mHomeSp, flags);
+        dest.writeParcelable(mCredential, flags);
+        dest.writeParcelable(mPolicy, flags);
+        dest.writeParcelable(mSubscriptionUpdate, flags);
+        writeTrustRootCerts(dest, mTrustRootCertList);
+        dest.writeInt(mUpdateIdentifier);
+        dest.writeInt(mCredentialPriority);
+        dest.writeLong(mSubscriptionCreationTimeInMillis);
+        dest.writeLong(mSubscriptionExpirationTimeInMillis);
+        dest.writeString(mSubscriptionType);
+        dest.writeLong(mUsageLimitUsageTimePeriodInMinutes);
+        dest.writeLong(mUsageLimitStartTimeInMillis);
+        dest.writeLong(mUsageLimitDataLimit);
+        dest.writeLong(mUsageLimitTimeLimitInMinutes);
+        Bundle bundle = new Bundle();
+        bundle.putSerializable("serviceFriendlyNames",
+                (HashMap<String, String>) mServiceFriendlyNames);
+        dest.writeBundle(bundle);
+    }
+
+    @Override
+    public boolean equals(Object thatObject) {
+        if (this == thatObject) {
+            return true;
+        }
+        if (!(thatObject instanceof PasspointConfiguration)) {
+            return false;
+        }
+        PasspointConfiguration that = (PasspointConfiguration) thatObject;
+        return (mHomeSp == null ? that.mHomeSp == null : mHomeSp.equals(that.mHomeSp))
+                && (mCredential == null ? that.mCredential == null
+                : mCredential.equals(that.mCredential))
+                && (mPolicy == null ? that.mPolicy == null : mPolicy.equals(that.mPolicy))
+                && (mSubscriptionUpdate == null ? that.mSubscriptionUpdate == null
+                : mSubscriptionUpdate.equals(that.mSubscriptionUpdate))
+                && isTrustRootCertListEquals(mTrustRootCertList, that.mTrustRootCertList)
+                && mUpdateIdentifier == that.mUpdateIdentifier
+                && mCredentialPriority == that.mCredentialPriority
+                && mSubscriptionCreationTimeInMillis == that.mSubscriptionCreationTimeInMillis
+                && mSubscriptionExpirationTimeInMillis == that.mSubscriptionExpirationTimeInMillis
+                && TextUtils.equals(mSubscriptionType, that.mSubscriptionType)
+                && mUsageLimitUsageTimePeriodInMinutes == that.mUsageLimitUsageTimePeriodInMinutes
+                && mUsageLimitStartTimeInMillis == that.mUsageLimitStartTimeInMillis
+                && mUsageLimitDataLimit == that.mUsageLimitDataLimit
+                && mUsageLimitTimeLimitInMinutes == that.mUsageLimitTimeLimitInMinutes
+                && (mServiceFriendlyNames == null ? that.mServiceFriendlyNames == null
+                : mServiceFriendlyNames.equals(that.mServiceFriendlyNames));
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mHomeSp, mCredential, mPolicy, mSubscriptionUpdate, mTrustRootCertList,
+                mUpdateIdentifier, mCredentialPriority, mSubscriptionCreationTimeInMillis,
+                mSubscriptionExpirationTimeInMillis, mUsageLimitUsageTimePeriodInMinutes,
+                mUsageLimitStartTimeInMillis, mUsageLimitDataLimit, mUsageLimitTimeLimitInMinutes,
+                mServiceFriendlyNames);
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append("UpdateIdentifier: ").append(mUpdateIdentifier).append("\n");
+        builder.append("CredentialPriority: ").append(mCredentialPriority).append("\n");
+        builder.append("SubscriptionCreationTime: ").append(
+                mSubscriptionCreationTimeInMillis != Long.MIN_VALUE
+                ? new Date(mSubscriptionCreationTimeInMillis) : "Not specified").append("\n");
+        builder.append("SubscriptionExpirationTime: ").append(
+                mSubscriptionExpirationTimeInMillis != Long.MIN_VALUE
+                ? new Date(mSubscriptionExpirationTimeInMillis) : "Not specified").append("\n");
+        builder.append("UsageLimitStartTime: ").append(mUsageLimitStartTimeInMillis != Long.MIN_VALUE
+                ? new Date(mUsageLimitStartTimeInMillis) : "Not specified").append("\n");
+        builder.append("UsageTimePeriod: ").append(mUsageLimitUsageTimePeriodInMinutes)
+                .append("\n");
+        builder.append("UsageLimitDataLimit: ").append(mUsageLimitDataLimit).append("\n");
+        builder.append("UsageLimitTimeLimit: ").append(mUsageLimitTimeLimitInMinutes).append("\n");
+        if (mHomeSp != null) {
+            builder.append("HomeSP Begin ---\n");
+            builder.append(mHomeSp);
+            builder.append("HomeSP End ---\n");
+        }
+        if (mCredential != null) {
+            builder.append("Credential Begin ---\n");
+            builder.append(mCredential);
+            builder.append("Credential End ---\n");
+        }
+        if (mPolicy != null) {
+            builder.append("Policy Begin ---\n");
+            builder.append(mPolicy);
+            builder.append("Policy End ---\n");
+        }
+        if (mSubscriptionUpdate != null) {
+            builder.append("SubscriptionUpdate Begin ---\n");
+            builder.append(mSubscriptionUpdate);
+            builder.append("SubscriptionUpdate End ---\n");
+        }
+        if (mTrustRootCertList != null) {
+            builder.append("TrustRootCertServers: ").append(mTrustRootCertList.keySet())
+                    .append("\n");
+        }
+        if (mServiceFriendlyNames != null) {
+            builder.append("ServiceFriendlyNames: ").append(mServiceFriendlyNames);
+        }
+        return builder.toString();
+    }
+
+    /**
+     * Validate the R1 configuration data.
+     *
+     * @return true on success or false on failure
+     * @hide
+     */
+    public boolean validate() {
+        // Optional: PerProviderSubscription/<X+>/SubscriptionUpdate
+        if (mSubscriptionUpdate != null && !mSubscriptionUpdate.validate()) {
+            return false;
+        }
+        return validateForCommonR1andR2(true);
+    }
+
+    /**
+     * Validate the R2 configuration data.
+     *
+     * @return true on success or false on failure
+     * @hide
+     */
+    public boolean validateForR2() {
+        // Required: PerProviderSubscription/UpdateIdentifier
+        if (mUpdateIdentifier == Integer.MIN_VALUE) {
+            return false;
+        }
+
+        // Required: PerProviderSubscription/<X+>/SubscriptionUpdate
+        if (mSubscriptionUpdate == null || !mSubscriptionUpdate.validate()) {
+            return false;
+        }
+        return validateForCommonR1andR2(false);
+    }
+
+    private boolean validateForCommonR1andR2(boolean isR1) {
+        // Required: PerProviderSubscription/<X+>/HomeSP
+        if (mHomeSp == null || !mHomeSp.validate()) {
+            return false;
+        }
+
+        // Required: PerProviderSubscription/<X+>/Credential
+        if (mCredential == null || !mCredential.validate(isR1)) {
+            return false;
+        }
+
+        // Optional: PerProviderSubscription/<X+>/Policy
+        if (mPolicy != null && !mPolicy.validate()) {
+            return false;
+        }
+
+        if (mTrustRootCertList != null) {
+            for (Map.Entry<String, byte[]> entry : mTrustRootCertList.entrySet()) {
+                String url = entry.getKey();
+                byte[] certFingerprint = entry.getValue();
+                if (TextUtils.isEmpty(url)) {
+                    Log.d(TAG, "Empty URL");
+                    return false;
+                }
+                if (url.getBytes(StandardCharsets.UTF_8).length > MAX_URL_BYTES) {
+                    Log.d(TAG, "URL bytes exceeded the max: "
+                            + url.getBytes(StandardCharsets.UTF_8).length);
+                    return false;
+                }
+
+                if (certFingerprint == null) {
+                    Log.d(TAG, "Fingerprint not specified");
+                    return false;
+                }
+                if (certFingerprint.length != CERTIFICATE_SHA256_BYTES) {
+                    Log.d(TAG, "Incorrect size of trust root certificate SHA-256 fingerprint: "
+                            + certFingerprint.length);
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    public static final @android.annotation.NonNull Creator<PasspointConfiguration> CREATOR =
+        new Creator<PasspointConfiguration>() {
+            @Override
+            public PasspointConfiguration createFromParcel(Parcel in) {
+                PasspointConfiguration config = new PasspointConfiguration();
+                config.setHomeSp(in.readParcelable(null));
+                config.setCredential(in.readParcelable(null));
+                config.setPolicy(in.readParcelable(null));
+                config.setSubscriptionUpdate(in.readParcelable(null));
+                config.setTrustRootCertList(readTrustRootCerts(in));
+                config.setUpdateIdentifier(in.readInt());
+                config.setCredentialPriority(in.readInt());
+                config.setSubscriptionCreationTimeInMillis(in.readLong());
+                config.setSubscriptionExpirationTimeInMillis(in.readLong());
+                config.setSubscriptionType(in.readString());
+                config.setUsageLimitUsageTimePeriodInMinutes(in.readLong());
+                config.setUsageLimitStartTimeInMillis(in.readLong());
+                config.setUsageLimitDataLimit(in.readLong());
+                config.setUsageLimitTimeLimitInMinutes(in.readLong());
+                Bundle bundle = in.readBundle();
+                Map<String, String> friendlyNamesMap = (HashMap) bundle.getSerializable(
+                        "serviceFriendlyNames");
+                config.setServiceFriendlyNames(friendlyNamesMap);
+                return config;
+            }
+
+            @Override
+            public PasspointConfiguration[] newArray(int size) {
+                return new PasspointConfiguration[size];
+            }
+
+            /**
+             * Helper function for reading trust root certificate info list from a Parcel.
+             *
+             * @param in The Parcel to read from
+             * @return The list of trust root certificate URL with the corresponding certificate
+             *         fingerprint
+             */
+            private Map<String, byte[]> readTrustRootCerts(Parcel in) {
+                int size = in.readInt();
+                if (size == NULL_VALUE) {
+                    return null;
+                }
+                Map<String, byte[]> trustRootCerts = new HashMap<>(size);
+                for (int i = 0; i < size; i++) {
+                    String key = in.readString();
+                    byte[] value = in.createByteArray();
+                    trustRootCerts.put(key, value);
+                }
+                return trustRootCerts;
+            }
+        };
+
+    /**
+     * Helper function for writing trust root certificate information list.
+     *
+     * @param dest The Parcel to write to
+     * @param trustRootCerts The list of trust root certificate URL with the corresponding
+     *                       certificate fingerprint
+     */
+    private static void writeTrustRootCerts(Parcel dest, Map<String, byte[]> trustRootCerts) {
+        if (trustRootCerts == null) {
+            dest.writeInt(NULL_VALUE);
+            return;
+        }
+        dest.writeInt(trustRootCerts.size());
+        for (Map.Entry<String, byte[]> entry : trustRootCerts.entrySet()) {
+            dest.writeString(entry.getKey());
+            dest.writeByteArray(entry.getValue());
+        }
+    }
+
+    /**
+     * Helper function for comparing two trust root certificate list.  Cannot use Map#equals
+     * method since the value type (byte[]) doesn't override equals method.
+     *
+     * @param list1 The first trust root certificate list
+     * @param list2 The second trust root certificate list
+     * @return true if the two list are equal
+     */
+    private static boolean isTrustRootCertListEquals(Map<String, byte[]> list1,
+            Map<String, byte[]> list2) {
+        if (list1 == null || list2 == null) {
+            return list1 == list2;
+        }
+        if (list1.size() != list2.size()) {
+            return false;
+        }
+        for (Map.Entry<String, byte[]> entry : list1.entrySet()) {
+            if (!Arrays.equals(entry.getValue(), list2.get(entry.getKey()))) {
+                return false;
+            }
+        }
+        return true;
+    }
+}
diff --git a/android/net/wifi/hotspot2/ProvisioningCallback.java b/android/net/wifi/hotspot2/ProvisioningCallback.java
new file mode 100644
index 0000000..1d499b6
--- /dev/null
+++ b/android/net/wifi/hotspot2/ProvisioningCallback.java
@@ -0,0 +1,235 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.hotspot2;
+
+import android.annotation.SystemApi;
+import android.net.wifi.WifiManager;
+import android.os.Handler;
+
+/**
+ * Base class for provisioning callbacks. Should be extended by applications and set when calling
+ * {@link WifiManager#startSubscriptionProvisioning(OsuProvider, ProvisioningCallback, Handler)}.
+ *
+ * @hide
+ */
+@SystemApi
+public abstract class ProvisioningCallback {
+
+    /**
+     * The reason code for Provisioning Failure due to connection failure to OSU AP.
+     */
+    public static final int OSU_FAILURE_AP_CONNECTION = 1;
+
+    /**
+     * The reason code for invalid server URL address.
+     */
+    public static final int OSU_FAILURE_SERVER_URL_INVALID = 2;
+
+    /**
+     * The reason code for provisioning failure due to connection failure to the server.
+     */
+    public static final int OSU_FAILURE_SERVER_CONNECTION = 3;
+
+    /**
+     * The reason code for provisioning failure due to invalid server certificate.
+     */
+    public static final int OSU_FAILURE_SERVER_VALIDATION = 4;
+
+    /**
+     * The reason code for provisioning failure due to invalid service provider.
+     */
+    public static final int OSU_FAILURE_SERVICE_PROVIDER_VERIFICATION = 5;
+
+    /**
+     * The reason code for provisioning failure when a provisioning flow is aborted.
+     */
+    public static final int OSU_FAILURE_PROVISIONING_ABORTED = 6;
+
+    /**
+     * The reason code for provisioning failure when a provisioning flow is not possible.
+     */
+    public static final int OSU_FAILURE_PROVISIONING_NOT_AVAILABLE = 7;
+
+    /**
+     * The reason code for provisioning failure due to invalid web url format for an OSU web page.
+     */
+    public static final int OSU_FAILURE_INVALID_URL_FORMAT_FOR_OSU = 8;
+
+    /**
+     * The reason code for provisioning failure when a command received is not the expected command
+     * type.
+     */
+    public static final int OSU_FAILURE_UNEXPECTED_COMMAND_TYPE = 9;
+
+    /**
+     * The reason code for provisioning failure when a SOAP message is not the expected message
+     * type.
+     */
+    public static final int OSU_FAILURE_UNEXPECTED_SOAP_MESSAGE_TYPE = 10;
+
+    /**
+     * The reason code for provisioning failure when a SOAP message exchange fails.
+     */
+    public static final int OSU_FAILURE_SOAP_MESSAGE_EXCHANGE = 11;
+
+    /**
+     * The reason code for provisioning failure when a redirect listener fails to start.
+     */
+    public static final int OSU_FAILURE_START_REDIRECT_LISTENER = 12;
+
+    /**
+     * The reason code for provisioning failure when a redirect listener timed out to receive a HTTP
+     * redirect response.
+     */
+    public static final int OSU_FAILURE_TIMED_OUT_REDIRECT_LISTENER = 13;
+
+    /**
+     * The reason code for provisioning failure when there is no OSU activity to listen to
+     * {@link WifiManager#ACTION_PASSPOINT_LAUNCH_OSU_VIEW} intent.
+     */
+    public static final int OSU_FAILURE_NO_OSU_ACTIVITY_FOUND = 14;
+
+    /**
+     * The reason code for provisioning failure when the status of a SOAP message is not the
+     * expected message status.
+     */
+    public static final int OSU_FAILURE_UNEXPECTED_SOAP_MESSAGE_STATUS = 15;
+
+    /**
+     * The reason code for provisioning failure when there is no PPS MO.
+     * MO.
+     */
+    public static final int OSU_FAILURE_NO_PPS_MO = 16;
+
+    /**
+     * The reason code for provisioning failure when there is no AAAServerTrustRoot node in a PPS
+     * MO.
+     */
+    public static final int OSU_FAILURE_NO_AAA_SERVER_TRUST_ROOT_NODE = 17;
+
+    /**
+     * The reason code for provisioning failure when there is no TrustRoot node for remediation
+     * server in a PPS MO.
+     */
+    public static final int OSU_FAILURE_NO_REMEDIATION_SERVER_TRUST_ROOT_NODE = 18;
+
+    /**
+     * The reason code for provisioning failure when there is no TrustRoot node for policy server in
+     * a PPS MO.
+     */
+    public static final int OSU_FAILURE_NO_POLICY_SERVER_TRUST_ROOT_NODE = 19;
+
+    /**
+     * The reason code for provisioning failure when failing to retrieve trust root certificates
+     * used for validating server certificate for AAA, Remediation and Policy server.
+     */
+    public static final int OSU_FAILURE_RETRIEVE_TRUST_ROOT_CERTIFICATES = 20;
+
+    /**
+     * The reason code for provisioning failure when there is no trust root certificate for AAA
+     * server.
+     */
+    public static final int OSU_FAILURE_NO_AAA_TRUST_ROOT_CERTIFICATE = 21;
+
+    /**
+     * The reason code for provisioning failure when a {@link PasspointConfiguration} is failed to
+     * install.
+     */
+    public static final int OSU_FAILURE_ADD_PASSPOINT_CONFIGURATION = 22;
+
+    /**
+     * The reason code for provisioning failure when an {@link OsuProvider} is not found for
+     * provisioning.
+     */
+    public static final int OSU_FAILURE_OSU_PROVIDER_NOT_FOUND = 23;
+
+    /**
+     * The status code for provisioning flow to indicate connecting to OSU AP
+     */
+    public static final int OSU_STATUS_AP_CONNECTING = 1;
+
+    /**
+     * The status code for provisioning flow to indicate the OSU AP is connected.
+     */
+    public static final int OSU_STATUS_AP_CONNECTED = 2;
+
+    /**
+     * The status code for provisioning flow to indicate connecting to the server.
+     */
+    public static final int OSU_STATUS_SERVER_CONNECTING = 3;
+
+    /**
+     * The status code for provisioning flow to indicate the server certificate is validated.
+     */
+    public static final int OSU_STATUS_SERVER_VALIDATED = 4;
+
+    /**
+     * The status code for provisioning flow to indicate the server is connected
+     */
+    public static final int OSU_STATUS_SERVER_CONNECTED = 5;
+
+    /**
+     * The status code for provisioning flow to indicate starting the first SOAP exchange.
+     */
+    public static final int OSU_STATUS_INIT_SOAP_EXCHANGE = 6;
+
+    /**
+     * The status code for provisioning flow to indicate waiting for a HTTP redirect response.
+     */
+    public static final int OSU_STATUS_WAITING_FOR_REDIRECT_RESPONSE = 7;
+
+    /**
+     * The status code for provisioning flow to indicate a HTTP redirect response is received.
+     */
+    public static final int OSU_STATUS_REDIRECT_RESPONSE_RECEIVED = 8;
+
+    /**
+     * The status code for provisioning flow to indicate starting the second SOAP exchange.
+     */
+    public static final int OSU_STATUS_SECOND_SOAP_EXCHANGE = 9;
+
+    /**
+     * The status code for provisioning flow to indicate starting the third SOAP exchange.
+     */
+    public static final int OSU_STATUS_THIRD_SOAP_EXCHANGE = 10;
+
+    /**
+     * The status code for provisioning flow to indicate starting a step retrieving trust root
+     * certs.
+     */
+    public static final int OSU_STATUS_RETRIEVING_TRUST_ROOT_CERTS = 11;
+
+    /**
+     * Provisioning status for OSU failure
+     *
+     * @param status indicates error condition
+     */
+    public abstract void onProvisioningFailure(int status);
+
+    /**
+     * Provisioning status when OSU is in progress
+     *
+     * @param status indicates status of OSU flow
+     */
+    public abstract void onProvisioningStatus(int status);
+
+    /**
+     * Provisioning complete when provisioning/remediation flow completes
+     */
+    public abstract void onProvisioningComplete();
+}
+
diff --git a/android/net/wifi/hotspot2/omadm/PpsMoParser.java b/android/net/wifi/hotspot2/omadm/PpsMoParser.java
new file mode 100644
index 0000000..984cf7d
--- /dev/null
+++ b/android/net/wifi/hotspot2/omadm/PpsMoParser.java
@@ -0,0 +1,1658 @@
+/**
+ * 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.wifi.hotspot2.omadm;
+
+import android.net.wifi.hotspot2.PasspointConfiguration;
+import android.net.wifi.hotspot2.pps.Credential;
+import android.net.wifi.hotspot2.pps.HomeSp;
+import android.net.wifi.hotspot2.pps.Policy;
+import android.net.wifi.hotspot2.pps.UpdateParameter;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Pair;
+
+import java.io.IOException;
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.xml.sax.SAXException;
+
+/**
+ * Utility class for converting OMA-DM (Open Mobile Alliance's Device Management)
+ * PPS-MO (PerProviderSubscription Management Object) XML tree to a
+ * {@link PasspointConfiguration} object.
+ *
+ * Currently this only supports PerProviderSubscription/HomeSP and
+ * PerProviderSubscription/Credential subtree for Hotspot 2.0 Release 1 support.
+ *
+ * For more info, refer to Hotspot 2.0 PPS MO defined in section 9.1 of the Hotspot 2.0
+ * Release 2 Technical Specification.
+ *
+ * Below is a sample XML string for a Release 1 PPS MO tree:
+ *
+ * <MgmtTree xmlns="syncml:dmddf1.2">
+ *   <VerDTD>1.2</VerDTD>
+ *   <Node>
+ *     <NodeName>PerProviderSubscription</NodeName>
+ *     <RTProperties>
+ *       <Type>
+ *         <DDFName>urn:wfa:mo:hotspot2dot0­perprovidersubscription:1.0</DDFName>
+ *       </Type>
+ *     </RTProperties>
+ *     <Node>
+ *       <NodeName>i001</NodeName>
+ *       <Node>
+ *         <NodeName>HomeSP</NodeName>
+ *         <Node>
+ *           <NodeName>FriendlyName</NodeName>
+ *           <Value>Century House</Value>
+ *         </Node>
+ *         <Node>
+ *           <NodeName>FQDN</NodeName>
+ *           <Value>mi6.co.uk</Value>
+ *         </Node>
+ *         <Node>
+ *           <NodeName>RoamingConsortiumOI</NodeName>
+ *           <Value>112233,445566</Value>
+ *         </Node>
+ *       </Node>
+ *       <Node>
+ *         <NodeName>Credential</NodeName>
+ *         <Node>
+ *           <NodeName>Realm</NodeName>
+ *           <Value>shaken.stirred.com</Value>
+ *         </Node>
+ *         <Node>
+ *           <NodeName>UsernamePassword</NodeName>
+ *           <Node>
+ *             <NodeName>Username</NodeName>
+ *             <Value>james</Value>
+ *           </Node>
+ *           <Node>
+ *             <NodeName>Password</NodeName>
+ *             <Value>Ym9uZDAwNw==</Value>
+ *           </Node>
+ *           <Node>
+ *             <NodeName>EAPMethod</NodeName>
+ *             <Node>
+ *               <NodeName>EAPType</NodeName>
+ *               <Value>21</Value>
+ *             </Node>
+ *             <Node>
+ *               <NodeName>InnerMethod</NodeName>
+ *               <Value>MS-CHAP-V2</Value>
+ *             </Node>
+ *           </Node>
+ *         </Node>
+ *       </Node>
+ *     </Node>
+ *   </Node>
+ * </MgmtTree>
+ */
+public final class PpsMoParser {
+    private static final String TAG = "PpsMoParser";
+
+    /**
+     * XML tags expected in the PPS MO (PerProviderSubscription Management Object) XML tree.
+     */
+    private static final String TAG_MANAGEMENT_TREE = "MgmtTree";
+    private static final String TAG_VER_DTD = "VerDTD";
+    private static final String TAG_NODE = "Node";
+    private static final String TAG_NODE_NAME = "NodeName";
+    private static final String TAG_RT_PROPERTIES = "RTProperties";
+    private static final String TAG_TYPE = "Type";
+    private static final String TAG_DDF_NAME = "DDFName";
+    private static final String TAG_VALUE = "Value";
+
+    /**
+     * Name for PerProviderSubscription node.
+     */
+    private static final String NODE_PER_PROVIDER_SUBSCRIPTION = "PerProviderSubscription";
+
+    /**
+     * Fields under PerProviderSubscription.
+     */
+    private static final String NODE_UPDATE_IDENTIFIER = "UpdateIdentifier";
+    private static final String NODE_AAA_SERVER_TRUST_ROOT = "AAAServerTrustRoot";
+    private static final String NODE_SUBSCRIPTION_UPDATE = "SubscriptionUpdate";
+    private static final String NODE_SUBSCRIPTION_PARAMETER = "SubscriptionParameters";
+    private static final String NODE_TYPE_OF_SUBSCRIPTION = "TypeOfSubscription";
+    private static final String NODE_USAGE_LIMITS = "UsageLimits";
+    private static final String NODE_DATA_LIMIT = "DataLimit";
+    private static final String NODE_START_DATE = "StartDate";
+    private static final String NODE_TIME_LIMIT = "TimeLimit";
+    private static final String NODE_USAGE_TIME_PERIOD = "UsageTimePeriod";
+    private static final String NODE_CREDENTIAL_PRIORITY = "CredentialPriority";
+    private static final String NODE_EXTENSION = "Extension";
+
+    /**
+     * Fields under HomeSP subtree.
+     */
+    private static final String NODE_HOMESP = "HomeSP";
+    private static final String NODE_FQDN = "FQDN";
+    private static final String NODE_FRIENDLY_NAME = "FriendlyName";
+    private static final String NODE_ROAMING_CONSORTIUM_OI = "RoamingConsortiumOI";
+    private static final String NODE_NETWORK_ID = "NetworkID";
+    private static final String NODE_SSID = "SSID";
+    private static final String NODE_HESSID = "HESSID";
+    private static final String NODE_ICON_URL = "IconURL";
+    private static final String NODE_HOME_OI_LIST = "HomeOIList";
+    private static final String NODE_HOME_OI = "HomeOI";
+    private static final String NODE_HOME_OI_REQUIRED = "HomeOIRequired";
+    private static final String NODE_OTHER_HOME_PARTNERS = "OtherHomePartners";
+
+    /**
+     * Fields under Credential subtree.
+     */
+    private static final String NODE_CREDENTIAL = "Credential";
+    private static final String NODE_CREATION_DATE = "CreationDate";
+    private static final String NODE_EXPIRATION_DATE = "ExpirationDate";
+    private static final String NODE_USERNAME_PASSWORD = "UsernamePassword";
+    private static final String NODE_USERNAME = "Username";
+    private static final String NODE_PASSWORD = "Password";
+    private static final String NODE_MACHINE_MANAGED = "MachineManaged";
+    private static final String NODE_SOFT_TOKEN_APP = "SoftTokenApp";
+    private static final String NODE_ABLE_TO_SHARE = "AbleToShare";
+    private static final String NODE_EAP_METHOD = "EAPMethod";
+    private static final String NODE_EAP_TYPE = "EAPType";
+    private static final String NODE_VENDOR_ID = "VendorId";
+    private static final String NODE_VENDOR_TYPE = "VendorType";
+    private static final String NODE_INNER_EAP_TYPE = "InnerEAPType";
+    private static final String NODE_INNER_VENDOR_ID = "InnerVendorID";
+    private static final String NODE_INNER_VENDOR_TYPE = "InnerVendorType";
+    private static final String NODE_INNER_METHOD = "InnerMethod";
+    private static final String NODE_DIGITAL_CERTIFICATE = "DigitalCertificate";
+    private static final String NODE_CERTIFICATE_TYPE = "CertificateType";
+    private static final String NODE_CERT_SHA256_FINGERPRINT = "CertSHA256Fingerprint";
+    private static final String NODE_REALM = "Realm";
+    private static final String NODE_SIM = "SIM";
+    private static final String NODE_SIM_IMSI = "IMSI";
+    private static final String NODE_CHECK_AAA_SERVER_CERT_STATUS = "CheckAAAServerCertStatus";
+
+    /**
+     * Fields under Policy subtree.
+     */
+    private static final String NODE_POLICY = "Policy";
+    private static final String NODE_PREFERRED_ROAMING_PARTNER_LIST =
+            "PreferredRoamingPartnerList";
+    private static final String NODE_FQDN_MATCH = "FQDN_Match";
+    private static final String NODE_PRIORITY = "Priority";
+    private static final String NODE_COUNTRY = "Country";
+    private static final String NODE_MIN_BACKHAUL_THRESHOLD = "MinBackhaulThreshold";
+    private static final String NODE_NETWORK_TYPE = "NetworkType";
+    private static final String NODE_DOWNLINK_BANDWIDTH = "DLBandwidth";
+    private static final String NODE_UPLINK_BANDWIDTH = "ULBandwidth";
+    private static final String NODE_POLICY_UPDATE = "PolicyUpdate";
+    private static final String NODE_UPDATE_INTERVAL = "UpdateInterval";
+    private static final String NODE_UPDATE_METHOD = "UpdateMethod";
+    private static final String NODE_RESTRICTION = "Restriction";
+    private static final String NODE_URI = "URI";
+    private static final String NODE_TRUST_ROOT = "TrustRoot";
+    private static final String NODE_CERT_URL = "CertURL";
+    private static final String NODE_SP_EXCLUSION_LIST = "SPExclusionList";
+    private static final String NODE_REQUIRED_PROTO_PORT_TUPLE = "RequiredProtoPortTuple";
+    private static final String NODE_IP_PROTOCOL = "IPProtocol";
+    private static final String NODE_PORT_NUMBER = "PortNumber";
+    private static final String NODE_MAXIMUM_BSS_LOAD_VALUE = "MaximumBSSLoadValue";
+    private static final String NODE_OTHER = "Other";
+
+    /**
+     * URN (Unique Resource Name) for PerProviderSubscription Management Object Tree.
+     */
+    private static final String PPS_MO_URN =
+            "urn:wfa:mo:hotspot2dot0-perprovidersubscription:1.0";
+
+    /**
+     * Exception for generic parsing errors.
+     */
+    private static class ParsingException extends Exception {
+        public ParsingException(String message) {
+            super(message);
+        }
+    }
+
+    /**
+     * Class representing a node within the PerProviderSubscription tree.
+     * This is used to flatten out and eliminate the extra layering in the XMLNode tree,
+     * to make the data parsing easier and cleaner.
+     *
+     * A PPSNode can be an internal or a leaf node, but not both.
+     *
+     */
+    private static abstract class PPSNode {
+        private final String mName;
+        public PPSNode(String name) {
+            mName = name;
+        }
+
+        /**
+         * @return the name of the node
+         */
+        public String getName() {
+            return mName;
+        }
+
+        /**
+         * Applies for internal node only.
+         *
+         * @return the list of children nodes.
+         */
+        public abstract List<PPSNode> getChildren();
+
+        /**
+         * Applies for leaf node only.
+         *
+         * @return the string value of the node
+         */
+        public abstract String getValue();
+
+        /**
+         * @return a flag indicating if this is a leaf or an internal node
+         */
+        public abstract boolean isLeaf();
+    }
+
+    /**
+     * Class representing a leaf node in a PPS (PerProviderSubscription) tree.
+     */
+    private static class LeafNode extends PPSNode {
+        private final String mValue;
+        public LeafNode(String nodeName, String value) {
+            super(nodeName);
+            mValue = value;
+        }
+
+        @Override
+        public String getValue() {
+            return mValue;
+        }
+
+        @Override
+        public List<PPSNode> getChildren() {
+            return null;
+        }
+
+        @Override
+        public boolean isLeaf() {
+            return true;
+        }
+    }
+
+    /**
+     * Class representing an internal node in a PPS (PerProviderSubscription) tree.
+     */
+    private static class InternalNode extends PPSNode {
+        private final List<PPSNode> mChildren;
+        public InternalNode(String nodeName, List<PPSNode> children) {
+            super(nodeName);
+            mChildren = children;
+        }
+
+        @Override
+        public String getValue() {
+            return null;
+        }
+
+        @Override
+        public List<PPSNode> getChildren() {
+            return mChildren;
+        }
+
+        @Override
+        public boolean isLeaf() {
+            return false;
+        }
+    }
+
+    /**
+     * @hide
+     */
+    public PpsMoParser() {}
+
+    /**
+     * Convert a XML string representation of a PPS MO (PerProviderSubscription
+     * Management Object) tree to a {@link PasspointConfiguration} object.
+     *
+     * @param xmlString XML string representation of a PPS MO tree
+     * @return {@link PasspointConfiguration} or null
+     */
+    public static PasspointConfiguration parseMoText(String xmlString) {
+        // Convert the XML string to a XML tree.
+        XMLParser xmlParser = new XMLParser();
+        XMLNode root = null;
+        try {
+            root = xmlParser.parse(xmlString);
+        } catch(IOException | SAXException e) {
+            return null;
+        }
+        if (root == null) {
+            return null;
+        }
+
+        // Verify root node is a "MgmtTree" node.
+        if (root.getTag() != TAG_MANAGEMENT_TREE) {
+            Log.e(TAG, "Root is not a MgmtTree");
+            return null;
+        }
+
+        String verDtd = null;    // Used for detecting duplicate VerDTD element.
+        PasspointConfiguration config = null;
+        for (XMLNode child : root.getChildren()) {
+            switch(child.getTag()) {
+                case TAG_VER_DTD:
+                    if (verDtd != null) {
+                        Log.e(TAG, "Duplicate VerDTD element");
+                        return null;
+                    }
+                    verDtd = child.getText();
+                    break;
+                case TAG_NODE:
+                    if (config != null) {
+                        Log.e(TAG, "Unexpected multiple Node element under MgmtTree");
+                        return null;
+                    }
+                    try {
+                        config = parsePpsNode(child);
+                    } catch (ParsingException e) {
+                        Log.e(TAG, e.getMessage());
+                        return null;
+                    }
+                    break;
+                default:
+                    Log.e(TAG, "Unknown node: " + child.getTag());
+                    return null;
+            }
+        }
+        return config;
+    }
+
+    /**
+     * Parse a PerProviderSubscription node. Below is the format of the XML tree (with
+     * each XML element represent a node in the tree):
+     *
+     * <Node>
+     *   <NodeName>PerProviderSubscription</NodeName>
+     *   <RTProperties>
+     *     ...
+     *   </RTPProperties>
+     *   <Node>
+     *     <NodeName>UpdateIdentifier</NodeName>
+     *     <Value>...</Value>
+     *   </Node>
+     *   <Node>
+     *     ...
+     *   </Node>
+     * </Node>
+     *
+     * @param node XMLNode that contains PerProviderSubscription node.
+     * @return PasspointConfiguration or null
+     * @throws ParsingException
+     */
+    private static PasspointConfiguration parsePpsNode(XMLNode node)
+            throws ParsingException {
+        PasspointConfiguration config = null;
+        String nodeName = null;
+        int updateIdentifier = Integer.MIN_VALUE;
+        for (XMLNode child : node.getChildren()) {
+            switch (child.getTag()) {
+                case TAG_NODE_NAME:
+                    if (nodeName != null) {
+                        throw new ParsingException("Duplicate NodeName: " + child.getText());
+                    }
+                    nodeName = child.getText();
+                    if (!TextUtils.equals(nodeName, NODE_PER_PROVIDER_SUBSCRIPTION)) {
+                        throw new ParsingException("Unexpected NodeName: " + nodeName);
+                    }
+                    break;
+                case TAG_NODE:
+                    // A node can be either an UpdateIdentifier node or a PerProviderSubscription
+                    // instance node.  Flatten out the XML tree first by converting it to a PPS
+                    // tree to reduce the complexity of the parsing code.
+                    PPSNode ppsNodeRoot = buildPpsNode(child);
+                    if (TextUtils.equals(ppsNodeRoot.getName(), NODE_UPDATE_IDENTIFIER)) {
+                        if (updateIdentifier != Integer.MIN_VALUE) {
+                            throw new ParsingException("Multiple node for UpdateIdentifier");
+                        }
+                        updateIdentifier = parseInteger(getPpsNodeValue(ppsNodeRoot));
+                    } else {
+                        // Only one PerProviderSubscription instance is expected and allowed.
+                        if (config != null) {
+                            throw new ParsingException("Multiple PPS instance");
+                        }
+                        config = parsePpsInstance(ppsNodeRoot);
+                    }
+                    break;
+                case TAG_RT_PROPERTIES:
+                    // Parse and verify URN stored in the RT (Run Time) Properties.
+                    String urn = parseUrn(child);
+                    if (!TextUtils.equals(urn, PPS_MO_URN)) {
+                        throw new ParsingException("Unknown URN: " + urn);
+                    }
+                    break;
+                default:
+                    throw new ParsingException("Unknown tag under PPS node: " + child.getTag());
+            }
+        }
+        if (config != null && updateIdentifier != Integer.MIN_VALUE) {
+            config.setUpdateIdentifier(updateIdentifier);
+        }
+        return config;
+    }
+
+    /**
+     * Parse the URN stored in the RTProperties. Below is the format of the RTPProperties node:
+     *
+     * <RTProperties>
+     *   <Type>
+     *     <DDFName>urn:...</DDFName>
+     *   </Type>
+     * </RTProperties>
+     *
+     * @param node XMLNode that contains RTProperties node.
+     * @return URN String of URN.
+     * @throws ParsingException
+     */
+    private static String parseUrn(XMLNode node) throws ParsingException {
+        if (node.getChildren().size() != 1)
+            throw new ParsingException("Expect RTPProperties node to only have one child");
+
+        XMLNode typeNode = node.getChildren().get(0);
+        if (typeNode.getChildren().size() != 1) {
+            throw new ParsingException("Expect Type node to only have one child");
+        }
+        if (!TextUtils.equals(typeNode.getTag(), TAG_TYPE)) {
+            throw new ParsingException("Unexpected tag for Type: " + typeNode.getTag());
+        }
+
+        XMLNode ddfNameNode = typeNode.getChildren().get(0);
+        if (!ddfNameNode.getChildren().isEmpty()) {
+            throw new ParsingException("Expect DDFName node to have no child");
+        }
+        if (!TextUtils.equals(ddfNameNode.getTag(), TAG_DDF_NAME)) {
+            throw new ParsingException("Unexpected tag for DDFName: " + ddfNameNode.getTag());
+        }
+
+        return ddfNameNode.getText();
+    }
+
+    /**
+     * Convert a XML tree represented by XMLNode to a PPS (PerProviderSubscription) instance tree
+     * represented by PPSNode.  This flattens out the XML tree to allow easier and cleaner parsing
+     * of the PPS configuration data.  Only three types of XML tag are expected: "NodeName",
+     * "Node", and "Value".
+     *
+     * The original XML tree (each XML element represent a node):
+     *
+     * <Node>
+     *   <NodeName>root</NodeName>
+     *   <Node>
+     *     <NodeName>child1</NodeName>
+     *     <Value>value1</Value>
+     *   </Node>
+     *   <Node>
+     *     <NodeName>child2</NodeName>
+     *     <Node>
+     *       <NodeName>grandchild1</NodeName>
+     *       ...
+     *     </Node>
+     *   </Node>
+     *   ...
+     * </Node>
+     *
+     * The converted PPS tree:
+     *
+     * [root] --- [child1, value1]
+     *   |
+     *   ---------[child2] --------[grandchild1] --- ...
+     *
+     * @param node XMLNode pointed to the root of a XML tree
+     * @return PPSNode pointing to the root of a PPS tree
+     * @throws ParsingException
+     */
+    private static PPSNode buildPpsNode(XMLNode node) throws ParsingException {
+        String nodeName = null;
+        String nodeValue = null;
+        List<PPSNode> childNodes = new ArrayList<PPSNode>();
+        // Names of parsed child nodes, use for detecting multiple child nodes with the same name.
+        Set<String> parsedNodes = new HashSet<String>();
+
+        for (XMLNode child : node.getChildren()) {
+            String tag = child.getTag();
+            if (TextUtils.equals(tag, TAG_NODE_NAME)) {
+                if (nodeName != null) {
+                    throw new ParsingException("Duplicate NodeName node");
+                }
+                nodeName = child.getText();
+            } else if (TextUtils.equals(tag, TAG_NODE)) {
+                PPSNode ppsNode = buildPpsNode(child);
+                if (parsedNodes.contains(ppsNode.getName())) {
+                    throw new ParsingException("Duplicate node: " + ppsNode.getName());
+                }
+                parsedNodes.add(ppsNode.getName());
+                childNodes.add(ppsNode);
+            } else if (TextUtils.equals(tag, TAG_VALUE)) {
+               if (nodeValue != null) {
+                   throw new ParsingException("Duplicate Value node");
+               }
+               nodeValue = child.getText();
+            } else {
+                throw new ParsingException("Unknown tag: " + tag);
+            }
+        }
+
+        if (nodeName == null) {
+            throw new ParsingException("Invalid node: missing NodeName");
+        }
+        if (nodeValue == null && childNodes.size() == 0) {
+            throw new ParsingException("Invalid node: " + nodeName +
+                    " missing both value and children");
+        }
+        if (nodeValue != null && childNodes.size() > 0) {
+            throw new ParsingException("Invalid node: " + nodeName +
+                    " contained both value and children");
+        }
+
+        if (nodeValue != null) {
+            return new LeafNode(nodeName, nodeValue);
+        }
+        return new InternalNode(nodeName, childNodes);
+    }
+
+    /**
+     * Return the value of a PPSNode.  An exception will be thrown if the given node
+     * is not a leaf node.
+     *
+     * @param node PPSNode to retrieve the value from
+     * @return String representing the value of the node
+     * @throws ParsingException
+     */
+    private static String getPpsNodeValue(PPSNode node) throws ParsingException {
+        if (!node.isLeaf()) {
+            throw new ParsingException("Cannot get value from a non-leaf node: " + node.getName());
+        }
+        return node.getValue();
+    }
+
+    /**
+     * Parse a PPS (PerProviderSubscription) configurations from a PPS tree.
+     *
+     * @param root PPSNode representing the root of the PPS tree
+     * @return PasspointConfiguration
+     * @throws ParsingException
+     */
+    private static PasspointConfiguration parsePpsInstance(PPSNode root)
+            throws ParsingException {
+        if (root.isLeaf()) {
+            throw new ParsingException("Leaf node not expected for PPS instance");
+        }
+
+        PasspointConfiguration config = new PasspointConfiguration();
+        for (PPSNode child : root.getChildren()) {
+            switch(child.getName()) {
+                case NODE_HOMESP:
+                    config.setHomeSp(parseHomeSP(child));
+                    break;
+                case NODE_CREDENTIAL:
+                    config.setCredential(parseCredential(child));
+                    break;
+                case NODE_POLICY:
+                    config.setPolicy(parsePolicy(child));
+                    break;
+                case NODE_AAA_SERVER_TRUST_ROOT:
+                    config.setTrustRootCertList(parseAAAServerTrustRootList(child));
+                    break;
+                case NODE_SUBSCRIPTION_UPDATE:
+                    config.setSubscriptionUpdate(parseUpdateParameter(child));
+                    break;
+                case NODE_SUBSCRIPTION_PARAMETER:
+                    parseSubscriptionParameter(child, config);
+                    break;
+                case NODE_CREDENTIAL_PRIORITY:
+                    config.setCredentialPriority(parseInteger(getPpsNodeValue(child)));
+                    break;
+                case NODE_EXTENSION:
+                    // All vendor specific information will be under this node.
+                    Log.d(TAG, "Ignore Extension node for vendor specific information");
+                    break;
+                default:
+                    throw new ParsingException("Unknown node: " + child.getName());
+            }
+        }
+        return config;
+    }
+
+    /**
+     * Parse configurations under PerProviderSubscription/HomeSP subtree.
+     *
+     * @param node PPSNode representing the root of the PerProviderSubscription/HomeSP subtree
+     * @return HomeSP
+     * @throws ParsingException
+     */
+    private static HomeSp parseHomeSP(PPSNode node) throws ParsingException {
+        if (node.isLeaf()) {
+            throw new ParsingException("Leaf node not expected for HomeSP");
+        }
+
+        HomeSp homeSp = new HomeSp();
+        for (PPSNode child : node.getChildren()) {
+            switch (child.getName()) {
+                case NODE_FQDN:
+                    homeSp.setFqdn(getPpsNodeValue(child));
+                    break;
+                case NODE_FRIENDLY_NAME:
+                    homeSp.setFriendlyName(getPpsNodeValue(child));
+                    break;
+                case NODE_ROAMING_CONSORTIUM_OI:
+                    homeSp.setRoamingConsortiumOis(
+                            parseRoamingConsortiumOI(getPpsNodeValue(child)));
+                    break;
+                case NODE_ICON_URL:
+                    homeSp.setIconUrl(getPpsNodeValue(child));
+                    break;
+                case NODE_NETWORK_ID:
+                    homeSp.setHomeNetworkIds(parseNetworkIds(child));
+                    break;
+                case NODE_HOME_OI_LIST:
+                    Pair<List<Long>, List<Long>> homeOIs = parseHomeOIList(child);
+                    homeSp.setMatchAllOis(convertFromLongList(homeOIs.first));
+                    homeSp.setMatchAnyOis(convertFromLongList(homeOIs.second));
+                    break;
+                case NODE_OTHER_HOME_PARTNERS:
+                    homeSp.setOtherHomePartners(parseOtherHomePartners(child));
+                    break;
+                default:
+                    throw new ParsingException("Unknown node under HomeSP: " + child.getName());
+            }
+        }
+        return homeSp;
+    }
+
+    /**
+     * Parse the roaming consortium OI string, which contains a list of OIs separated by ",".
+     *
+     * @param oiStr string containing list of OIs (Organization Identifiers) separated by ","
+     * @return long[]
+     * @throws ParsingException
+     */
+    private static long[] parseRoamingConsortiumOI(String oiStr)
+            throws ParsingException {
+        String[] oiStrArray = oiStr.split(",");
+        long[] oiArray = new long[oiStrArray.length];
+        for (int i = 0; i < oiStrArray.length; i++) {
+            oiArray[i] = parseLong(oiStrArray[i], 16);
+        }
+        return oiArray;
+    }
+
+    /**
+     * Parse configurations under PerProviderSubscription/HomeSP/NetworkID subtree.
+     *
+     * @param node PPSNode representing the root of the PerProviderSubscription/HomeSP/NetworkID
+     *             subtree
+     * @return HashMap<String, Long> representing list of <SSID, HESSID> pair.
+     * @throws ParsingException
+     */
+    static private Map<String, Long> parseNetworkIds(PPSNode node)
+            throws ParsingException {
+        if (node.isLeaf()) {
+            throw new ParsingException("Leaf node not expected for NetworkID");
+        }
+
+        Map<String, Long> networkIds = new HashMap<>();
+        for (PPSNode child : node.getChildren()) {
+            Pair<String, Long> networkId = parseNetworkIdInstance(child);
+            networkIds.put(networkId.first, networkId.second);
+        }
+        return networkIds;
+    }
+
+    /**
+     * Parse configurations under PerProviderSubscription/HomeSP/NetworkID/<X+> subtree.
+     * The instance name (<X+>) is irrelevant and must be unique for each instance, which
+     * is verified when the PPS tree is constructed {@link #buildPpsNode}.
+     *
+     * @param node PPSNode representing the root of the
+     *             PerProviderSubscription/HomeSP/NetworkID/<X+> subtree
+     * @return Pair<String, Long> representing <SSID, HESSID> pair.
+     * @throws ParsingException
+     */
+    static private Pair<String, Long> parseNetworkIdInstance(PPSNode node)
+            throws ParsingException {
+        if (node.isLeaf()) {
+            throw new ParsingException("Leaf node not expected for NetworkID instance");
+        }
+
+        String ssid = null;
+        Long hessid = null;
+        for (PPSNode child : node.getChildren()) {
+            switch (child.getName()) {
+                case NODE_SSID:
+                    ssid = getPpsNodeValue(child);
+                    break;
+                case NODE_HESSID:
+                    hessid = parseLong(getPpsNodeValue(child), 16);
+                    break;
+                default:
+                    throw new ParsingException("Unknown node under NetworkID instance: " +
+                            child.getName());
+            }
+        }
+        if (ssid == null)
+            throw new ParsingException("NetworkID instance missing SSID");
+
+        return new Pair<String, Long>(ssid, hessid);
+    }
+
+    /**
+     * Parse configurations under PerProviderSubscription/HomeSP/HomeOIList subtree.
+     *
+     * @param node PPSNode representing the root of the PerProviderSubscription/HomeSP/HomeOIList
+     *             subtree
+     * @return Pair<List<Long>, List<Long>> containing both MatchAllOIs and MatchAnyOIs list.
+     * @throws ParsingException
+     */
+    private static Pair<List<Long>, List<Long>> parseHomeOIList(PPSNode node)
+            throws ParsingException {
+        if (node.isLeaf()) {
+            throw new ParsingException("Leaf node not expected for HomeOIList");
+        }
+
+        List<Long> matchAllOIs = new ArrayList<Long>();
+        List<Long> matchAnyOIs = new ArrayList<Long>();
+        for (PPSNode child : node.getChildren()) {
+            Pair<Long, Boolean> homeOI = parseHomeOIInstance(child);
+            if (homeOI.second.booleanValue()) {
+                matchAllOIs.add(homeOI.first);
+            } else {
+                matchAnyOIs.add(homeOI.first);
+            }
+        }
+        return new Pair<List<Long>, List<Long>>(matchAllOIs, matchAnyOIs);
+    }
+
+    /**
+     * Parse configurations under PerProviderSubscription/HomeSP/HomeOIList/<X+> subtree.
+     * The instance name (<X+>) is irrelevant and must be unique for each instance, which
+     * is verified when the PPS tree is constructed {@link #buildPpsNode}.
+     *
+     * @param node PPSNode representing the root of the
+     *             PerProviderSubscription/HomeSP/HomeOIList/<X+> subtree
+     * @return Pair<Long, Boolean> containing a HomeOI and a HomeOIRequired flag
+     * @throws ParsingException
+     */
+    private static Pair<Long, Boolean> parseHomeOIInstance(PPSNode node) throws ParsingException {
+        if (node.isLeaf()) {
+            throw new ParsingException("Leaf node not expected for HomeOI instance");
+        }
+
+        Long oi = null;
+        Boolean required = null;
+        for (PPSNode child : node.getChildren()) {
+            switch (child.getName()) {
+                case NODE_HOME_OI:
+                    try {
+                        oi = Long.valueOf(getPpsNodeValue(child), 16);
+                    } catch (NumberFormatException e) {
+                        throw new ParsingException("Invalid HomeOI: " + getPpsNodeValue(child));
+                    }
+                    break;
+                case NODE_HOME_OI_REQUIRED:
+                    required = Boolean.valueOf(getPpsNodeValue(child));
+                    break;
+                default:
+                    throw new ParsingException("Unknown node under NetworkID instance: " +
+                            child.getName());
+            }
+        }
+        if (oi == null) {
+            throw new ParsingException("HomeOI instance missing OI field");
+        }
+        if (required == null) {
+            throw new ParsingException("HomeOI instance missing required field");
+        }
+        return new Pair<Long, Boolean>(oi, required);
+    }
+
+    /**
+     * Parse configurations under PerProviderSubscription/HomeSP/OtherHomePartners subtree.
+     * This contains a list of FQDN (Fully Qualified Domain Name) that are considered
+     * home partners.
+     *
+     * @param node PPSNode representing the root of the
+     *             PerProviderSubscription/HomeSP/OtherHomePartners subtree
+     * @return String[] list of partner's FQDN
+     * @throws ParsingException
+     */
+    private static String[] parseOtherHomePartners(PPSNode node) throws ParsingException {
+        if (node.isLeaf()) {
+            throw new ParsingException("Leaf node not expected for OtherHomePartners");
+        }
+        List<String> otherHomePartners = new ArrayList<String>();
+        for (PPSNode child : node.getChildren()) {
+            String fqdn = parseOtherHomePartnerInstance(child);
+            otherHomePartners.add(fqdn);
+        }
+        return otherHomePartners.toArray(new String[otherHomePartners.size()]);
+    }
+
+    /**
+     * Parse configurations under PerProviderSubscription/HomeSP/OtherHomePartners/<X+> subtree.
+     * The instance name (<X+>) is irrelevant and must be unique for each instance, which
+     * is verified when the PPS tree is constructed {@link #buildPpsNode}.
+     *
+     * @param node PPSNode representing the root of the
+     *             PerProviderSubscription/HomeSP/OtherHomePartners/<X+> subtree
+     * @return String FQDN of the partner
+     * @throws ParsingException
+     */
+    private static String parseOtherHomePartnerInstance(PPSNode node) throws ParsingException {
+        if (node.isLeaf()) {
+            throw new ParsingException("Leaf node not expected for OtherHomePartner instance");
+        }
+        String fqdn = null;
+        for (PPSNode child : node.getChildren()) {
+            switch (child.getName()) {
+                case NODE_FQDN:
+                    fqdn = getPpsNodeValue(child);
+                    break;
+                default:
+                    throw new ParsingException(
+                            "Unknown node under OtherHomePartner instance: " + child.getName());
+            }
+        }
+        if (fqdn == null) {
+            throw new ParsingException("OtherHomePartner instance missing FQDN field");
+        }
+        return fqdn;
+    }
+
+    /**
+     * Parse configurations under PerProviderSubscription/Credential subtree.
+     *
+     * @param node PPSNode representing the root of the PerProviderSubscription/Credential subtree
+     * @return Credential
+     * @throws ParsingException
+     */
+    private static Credential parseCredential(PPSNode node) throws ParsingException {
+        if (node.isLeaf()) {
+            throw new ParsingException("Leaf node not expected for HomeSP");
+        }
+
+        Credential credential = new Credential();
+        for (PPSNode child: node.getChildren()) {
+            switch (child.getName()) {
+                case NODE_CREATION_DATE:
+                    credential.setCreationTimeInMillis(parseDate(getPpsNodeValue(child)));
+                    break;
+                case NODE_EXPIRATION_DATE:
+                    credential.setExpirationTimeInMillis(parseDate(getPpsNodeValue(child)));
+                    break;
+                case NODE_USERNAME_PASSWORD:
+                    credential.setUserCredential(parseUserCredential(child));
+                    break;
+                case NODE_DIGITAL_CERTIFICATE:
+                    credential.setCertCredential(parseCertificateCredential(child));
+                    break;
+                case NODE_REALM:
+                    credential.setRealm(getPpsNodeValue(child));
+                    break;
+                case NODE_CHECK_AAA_SERVER_CERT_STATUS:
+                    credential.setCheckAaaServerCertStatus(
+                            Boolean.parseBoolean(getPpsNodeValue(child)));
+                    break;
+                case NODE_SIM:
+                    credential.setSimCredential(parseSimCredential(child));
+                    break;
+                default:
+                    throw new ParsingException("Unknown node under Credential: " +
+                            child.getName());
+            }
+        }
+        return credential;
+    }
+
+    /**
+     * Parse configurations under PerProviderSubscription/Credential/UsernamePassword subtree.
+     *
+     * @param node PPSNode representing the root of the
+     *             PerProviderSubscription/Credential/UsernamePassword subtree
+     * @return Credential.UserCredential
+     * @throws ParsingException
+     */
+    private static Credential.UserCredential parseUserCredential(PPSNode node)
+            throws ParsingException {
+        if (node.isLeaf()) {
+            throw new ParsingException("Leaf node not expected for UsernamePassword");
+        }
+
+        Credential.UserCredential userCred = new Credential.UserCredential();
+        for (PPSNode child : node.getChildren()) {
+            switch (child.getName()) {
+                case NODE_USERNAME:
+                    userCred.setUsername(getPpsNodeValue(child));
+                    break;
+                case NODE_PASSWORD:
+                    userCred.setPassword(getPpsNodeValue(child));
+                    break;
+                case NODE_MACHINE_MANAGED:
+                    userCred.setMachineManaged(Boolean.parseBoolean(getPpsNodeValue(child)));
+                    break;
+                case NODE_SOFT_TOKEN_APP:
+                    userCred.setSoftTokenApp(getPpsNodeValue(child));
+                    break;
+                case NODE_ABLE_TO_SHARE:
+                    userCred.setAbleToShare(Boolean.parseBoolean(getPpsNodeValue(child)));
+                    break;
+                case NODE_EAP_METHOD:
+                    parseEAPMethod(child, userCred);
+                    break;
+                default:
+                    throw new ParsingException("Unknown node under UsernamPassword: " +
+                            child.getName());
+            }
+        }
+        return userCred;
+    }
+
+    /**
+     * Parse configurations under PerProviderSubscription/Credential/UsernamePassword/EAPMethod
+     * subtree.
+     *
+     * @param node PPSNode representing the root of the
+     *             PerProviderSubscription/Credential/UsernamePassword/EAPMethod subtree
+     * @param userCred UserCredential to be updated with EAP method values.
+     * @throws ParsingException
+     */
+    private static void parseEAPMethod(PPSNode node, Credential.UserCredential userCred)
+            throws ParsingException {
+        if (node.isLeaf()) {
+            throw new ParsingException("Leaf node not expected for EAPMethod");
+        }
+
+        for (PPSNode child : node.getChildren()) {
+            switch(child.getName()) {
+                case NODE_EAP_TYPE:
+                    userCred.setEapType(parseInteger(getPpsNodeValue(child)));
+                    break;
+                case NODE_INNER_METHOD:
+                    userCred.setNonEapInnerMethod(getPpsNodeValue(child));
+                    break;
+                case NODE_VENDOR_ID:
+                case NODE_VENDOR_TYPE:
+                case NODE_INNER_EAP_TYPE:
+                case NODE_INNER_VENDOR_ID:
+                case NODE_INNER_VENDOR_TYPE:
+                    // Only EAP-TTLS is currently supported for user credential, which doesn't
+                    // use any of these parameters.
+                    Log.d(TAG, "Ignore unsupported EAP method parameter: " + child.getName());
+                    break;
+                default:
+                    throw new ParsingException("Unknown node under EAPMethod: " + child.getName());
+            }
+        }
+    }
+
+    /**
+     * Parse configurations under PerProviderSubscription/Credential/DigitalCertificate subtree.
+     *
+     * @param node PPSNode representing the root of the
+     *             PerProviderSubscription/Credential/DigitalCertificate subtree
+     * @return Credential.CertificateCredential
+     * @throws ParsingException
+     */
+    private static Credential.CertificateCredential parseCertificateCredential(PPSNode node)
+            throws ParsingException {
+        if (node.isLeaf()) {
+            throw new ParsingException("Leaf node not expected for DigitalCertificate");
+        }
+
+        Credential.CertificateCredential certCred = new Credential.CertificateCredential();
+        for (PPSNode child : node.getChildren()) {
+            switch (child.getName()) {
+                case NODE_CERTIFICATE_TYPE:
+                    certCred.setCertType(getPpsNodeValue(child));
+                    break;
+                case NODE_CERT_SHA256_FINGERPRINT:
+                    certCred.setCertSha256Fingerprint(parseHexString(getPpsNodeValue(child)));
+                    break;
+                default:
+                    throw new ParsingException("Unknown node under DigitalCertificate: " +
+                            child.getName());
+            }
+        }
+        return certCred;
+    }
+
+    /**
+     * Parse configurations under PerProviderSubscription/Credential/SIM subtree.
+     *
+     * @param node PPSNode representing the root of the PerProviderSubscription/Credential/SIM
+     *             subtree
+     * @return Credential.SimCredential
+     * @throws ParsingException
+     */
+    private static Credential.SimCredential parseSimCredential(PPSNode node)
+            throws ParsingException {
+        if (node.isLeaf()) {
+            throw new ParsingException("Leaf node not expected for SIM");
+        }
+
+        Credential.SimCredential simCred = new Credential.SimCredential();
+        for (PPSNode child : node.getChildren()) {
+            switch (child.getName()) {
+                case NODE_SIM_IMSI:
+                    simCred.setImsi(getPpsNodeValue(child));
+                    break;
+                case NODE_EAP_TYPE:
+                    simCred.setEapType(parseInteger(getPpsNodeValue(child)));
+                    break;
+                default:
+                    throw new ParsingException("Unknown node under SIM: " + child.getName());
+            }
+        }
+        return simCred;
+    }
+
+    /**
+     * Parse configurations under PerProviderSubscription/Policy subtree.
+     *
+     * @param node PPSNode representing the root of the PerProviderSubscription/Policy subtree
+     * @return {@link Policy}
+     * @throws ParsingException
+     */
+    private static Policy parsePolicy(PPSNode node) throws ParsingException {
+        if (node.isLeaf()) {
+            throw new ParsingException("Leaf node not expected for Policy");
+        }
+
+        Policy policy = new Policy();
+        for (PPSNode child : node.getChildren()) {
+            switch (child.getName()) {
+                case NODE_PREFERRED_ROAMING_PARTNER_LIST:
+                    policy.setPreferredRoamingPartnerList(parsePreferredRoamingPartnerList(child));
+                    break;
+                case NODE_MIN_BACKHAUL_THRESHOLD:
+                    parseMinBackhaulThreshold(child, policy);
+                    break;
+                case NODE_POLICY_UPDATE:
+                    policy.setPolicyUpdate(parseUpdateParameter(child));
+                    break;
+                case NODE_SP_EXCLUSION_LIST:
+                    policy.setExcludedSsidList(parseSpExclusionList(child));
+                    break;
+                case NODE_REQUIRED_PROTO_PORT_TUPLE:
+                    policy.setRequiredProtoPortMap(parseRequiredProtoPortTuple(child));
+                    break;
+                case NODE_MAXIMUM_BSS_LOAD_VALUE:
+                    policy.setMaximumBssLoadValue(parseInteger(getPpsNodeValue(child)));
+                    break;
+                default:
+                    throw new ParsingException("Unknown node under Policy: " + child.getName());
+            }
+        }
+        return policy;
+    }
+
+    /**
+     * Parse configurations under PerProviderSubscription/Policy/PreferredRoamingPartnerList
+     * subtree.
+     *
+     * @param node PPSNode representing the root of the
+     *             PerProviderSubscription/Policy/PreferredRoamingPartnerList subtree
+     * @return List of {@link Policy#RoamingPartner}
+     * @throws ParsingException
+     */
+    private static List<Policy.RoamingPartner> parsePreferredRoamingPartnerList(PPSNode node)
+            throws ParsingException {
+        if (node.isLeaf()) {
+            throw new ParsingException("Leaf node not expected for PreferredRoamingPartnerList");
+        }
+        List<Policy.RoamingPartner> partnerList = new ArrayList<>();
+        for (PPSNode child : node.getChildren()) {
+            partnerList.add(parsePreferredRoamingPartner(child));
+        }
+        return partnerList;
+    }
+
+    /**
+     * Parse configurations under PerProviderSubscription/Policy/PreferredRoamingPartnerList/<X+>
+     * subtree.
+     *
+     * @param node PPSNode representing the root of the
+     *             PerProviderSubscription/Policy/PreferredRoamingPartnerList/<X+> subtree
+     * @return {@link Policy#RoamingPartner}
+     * @throws ParsingException
+     */
+    private static Policy.RoamingPartner parsePreferredRoamingPartner(PPSNode node)
+            throws ParsingException {
+        if (node.isLeaf()) {
+            throw new ParsingException("Leaf node not expected for PreferredRoamingPartner "
+                    + "instance");
+        }
+
+        Policy.RoamingPartner roamingPartner = new Policy.RoamingPartner();
+        for (PPSNode child : node.getChildren()) {
+            switch (child.getName()) {
+                case NODE_FQDN_MATCH:
+                    // FQDN_Match field is in the format of "[FQDN],[MatchInfo]", where [MatchInfo]
+                    // is either "exactMatch" for exact match of FQDN or "includeSubdomains" for
+                    // matching all FQDNs with the same sub-domain.
+                    String fqdnMatch = getPpsNodeValue(child);
+                    String[] fqdnMatchArray = fqdnMatch.split(",");
+                    if (fqdnMatchArray.length != 2) {
+                        throw new ParsingException("Invalid FQDN_Match: " + fqdnMatch);
+                    }
+                    roamingPartner.setFqdn(fqdnMatchArray[0]);
+                    if (TextUtils.equals(fqdnMatchArray[1], "exactMatch")) {
+                        roamingPartner.setFqdnExactMatch(true);
+                    } else if (TextUtils.equals(fqdnMatchArray[1], "includeSubdomains")) {
+                        roamingPartner.setFqdnExactMatch(false);
+                    } else {
+                        throw new ParsingException("Invalid FQDN_Match: " + fqdnMatch);
+                    }
+                    break;
+                case NODE_PRIORITY:
+                    roamingPartner.setPriority(parseInteger(getPpsNodeValue(child)));
+                    break;
+                case NODE_COUNTRY:
+                    roamingPartner.setCountries(getPpsNodeValue(child));
+                    break;
+                default:
+                    throw new ParsingException("Unknown node under PreferredRoamingPartnerList "
+                            + "instance " + child.getName());
+            }
+        }
+        return roamingPartner;
+    }
+
+    /**
+     * Parse configurations under PerProviderSubscription/Policy/MinBackhaulThreshold subtree
+     * into the given policy.
+     *
+     * @param node PPSNode representing the root of the
+     *             PerProviderSubscription/Policy/MinBackhaulThreshold subtree
+     * @param policy The policy to store the MinBackhualThreshold configuration
+     * @throws ParsingException
+     */
+    private static void parseMinBackhaulThreshold(PPSNode node, Policy policy)
+            throws ParsingException {
+        if (node.isLeaf()) {
+            throw new ParsingException("Leaf node not expected for MinBackhaulThreshold");
+        }
+        for (PPSNode child : node.getChildren()) {
+            parseMinBackhaulThresholdInstance(child, policy);
+        }
+    }
+
+    /**
+     * Parse configurations under PerProviderSubscription/Policy/MinBackhaulThreshold/<X+> subtree
+     * into the given policy.
+     *
+     * @param node PPSNode representing the root of the
+     *             PerProviderSubscription/Policy/MinBackhaulThreshold/<X+> subtree
+     * @param policy The policy to store the MinBackhaulThreshold configuration
+     * @throws ParsingException
+     */
+    private static void parseMinBackhaulThresholdInstance(PPSNode node, Policy policy)
+            throws ParsingException {
+        if (node.isLeaf()) {
+            throw new ParsingException("Leaf node not expected for MinBackhaulThreshold instance");
+        }
+        String networkType = null;
+        long downlinkBandwidth = Long.MIN_VALUE;
+        long uplinkBandwidth = Long.MIN_VALUE;
+        for (PPSNode child : node.getChildren()) {
+            switch (child.getName()) {
+                case NODE_NETWORK_TYPE:
+                    networkType = getPpsNodeValue(child);
+                    break;
+                case NODE_DOWNLINK_BANDWIDTH:
+                    downlinkBandwidth = parseLong(getPpsNodeValue(child), 10);
+                    break;
+                case NODE_UPLINK_BANDWIDTH:
+                    uplinkBandwidth = parseLong(getPpsNodeValue(child), 10);
+                    break;
+                default:
+                    throw new ParsingException("Unknown node under MinBackhaulThreshold instance "
+                            + child.getName());
+            }
+        }
+        if (networkType == null) {
+            throw new ParsingException("Missing NetworkType field");
+        }
+
+        if (TextUtils.equals(networkType, "home")) {
+            policy.setMinHomeDownlinkBandwidth(downlinkBandwidth);
+            policy.setMinHomeUplinkBandwidth(uplinkBandwidth);
+        } else if (TextUtils.equals(networkType, "roaming")) {
+            policy.setMinRoamingDownlinkBandwidth(downlinkBandwidth);
+            policy.setMinRoamingUplinkBandwidth(uplinkBandwidth);
+        } else {
+            throw new ParsingException("Invalid network type: " + networkType);
+        }
+    }
+
+    /**
+     * Parse update parameters. This contained configurations from either
+     * PerProviderSubscription/Policy/PolicyUpdate or PerProviderSubscription/SubscriptionUpdate
+     * subtree.
+     *
+     * @param node PPSNode representing the root of the PerProviderSubscription/Policy/PolicyUpdate
+     *             or PerProviderSubscription/SubscriptionUpdate subtree
+     * @return {@link UpdateParameter}
+     * @throws ParsingException
+     */
+    private static UpdateParameter parseUpdateParameter(PPSNode node)
+            throws ParsingException {
+        if (node.isLeaf()) {
+            throw new ParsingException("Leaf node not expected for Update Parameters");
+        }
+
+        UpdateParameter updateParam = new UpdateParameter();
+        for (PPSNode child : node.getChildren()) {
+            switch(child.getName()) {
+                case NODE_UPDATE_INTERVAL:
+                    updateParam.setUpdateIntervalInMinutes(parseLong(getPpsNodeValue(child), 10));
+                    break;
+                case NODE_UPDATE_METHOD:
+                    updateParam.setUpdateMethod(getPpsNodeValue(child));
+                    break;
+                case NODE_RESTRICTION:
+                    updateParam.setRestriction(getPpsNodeValue(child));
+                    break;
+                case NODE_URI:
+                    updateParam.setServerUri(getPpsNodeValue(child));
+                    break;
+                case NODE_USERNAME_PASSWORD:
+                    Pair<String, String> usernamePassword = parseUpdateUserCredential(child);
+                    updateParam.setUsername(usernamePassword.first);
+                    updateParam.setBase64EncodedPassword(usernamePassword.second);
+                    break;
+                case NODE_TRUST_ROOT:
+                    Pair<String, byte[]> trustRoot = parseTrustRoot(child);
+                    updateParam.setTrustRootCertUrl(trustRoot.first);
+                    updateParam.setTrustRootCertSha256Fingerprint(trustRoot.second);
+                    break;
+                case NODE_OTHER:
+                    Log.d(TAG, "Ignore unsupported paramter: " + child.getName());
+                    break;
+                default:
+                    throw new ParsingException("Unknown node under Update Parameters: "
+                            + child.getName());
+            }
+        }
+        return updateParam;
+    }
+
+    /**
+     * Parse username and password parameters associated with policy or subscription update.
+     * This contained configurations under either
+     * PerProviderSubscription/Policy/PolicyUpdate/UsernamePassword or
+     * PerProviderSubscription/SubscriptionUpdate/UsernamePassword subtree.
+     *
+     * @param node PPSNode representing the root of the UsernamePassword subtree
+     * @return Pair of username and password
+     * @throws ParsingException
+     */
+    private static Pair<String, String> parseUpdateUserCredential(PPSNode node)
+            throws ParsingException {
+        if (node.isLeaf()) {
+            throw new ParsingException("Leaf node not expected for UsernamePassword");
+        }
+
+        String username = null;
+        String password = null;
+        for (PPSNode child : node.getChildren()) {
+            switch (child.getName()) {
+                case NODE_USERNAME:
+                    username = getPpsNodeValue(child);
+                    break;
+                case NODE_PASSWORD:
+                    password = getPpsNodeValue(child);
+                    break;
+                default:
+                    throw new ParsingException("Unknown node under UsernamePassword: "
+                            + child.getName());
+            }
+        }
+        return Pair.create(username, password);
+    }
+
+    /**
+     * Parse the trust root parameters associated with policy update, subscription update, or AAA
+     * server trust root.
+     *
+     * This contained configurations under either
+     * PerProviderSubscription/Policy/PolicyUpdate/TrustRoot or
+     * PerProviderSubscription/SubscriptionUpdate/TrustRoot or
+     * PerProviderSubscription/AAAServerTrustRoot/<X+> subtree.
+     *
+     * @param node PPSNode representing the root of the TrustRoot subtree
+     * @return Pair of Certificate URL and fingerprint
+     * @throws ParsingException
+     */
+    private static Pair<String, byte[]> parseTrustRoot(PPSNode node)
+            throws ParsingException {
+        if (node.isLeaf()) {
+            throw new ParsingException("Leaf node not expected for TrustRoot");
+        }
+
+        String certUrl = null;
+        byte[] certFingerprint = null;
+        for (PPSNode child : node.getChildren()) {
+            switch (child.getName()) {
+                case NODE_CERT_URL:
+                    certUrl = getPpsNodeValue(child);
+                    break;
+                case NODE_CERT_SHA256_FINGERPRINT:
+                    certFingerprint = parseHexString(getPpsNodeValue(child));
+                    break;
+                default:
+                    throw new ParsingException("Unknown node under TrustRoot: "
+                            + child.getName());
+            }
+        }
+        return Pair.create(certUrl, certFingerprint);
+    }
+
+    /**
+     * Parse configurations under PerProviderSubscription/Policy/SPExclusionList subtree.
+     *
+     * @param node PPSNode representing the root of the
+     *             PerProviderSubscription/Policy/SPExclusionList subtree
+     * @return Array of excluded SSIDs
+     * @throws ParsingException
+     */
+    private static String[] parseSpExclusionList(PPSNode node) throws ParsingException {
+        if (node.isLeaf()) {
+            throw new ParsingException("Leaf node not expected for SPExclusionList");
+        }
+        List<String> ssidList = new ArrayList<>();
+        for (PPSNode child : node.getChildren()) {
+            ssidList.add(parseSpExclusionInstance(child));
+        }
+        return ssidList.toArray(new String[ssidList.size()]);
+    }
+
+    /**
+     * Parse configurations under PerProviderSubscription/Policy/SPExclusionList/<X+> subtree.
+     *
+     * @param node PPSNode representing the root of the
+     *             PerProviderSubscription/Policy/SPExclusionList/<X+> subtree
+     * @return String
+     * @throws ParsingException
+     */
+    private static String parseSpExclusionInstance(PPSNode node) throws ParsingException {
+        if (node.isLeaf()) {
+            throw new ParsingException("Leaf node not expected for SPExclusion instance");
+        }
+        String ssid = null;
+        for (PPSNode child : node.getChildren()) {
+            switch (child.getName()) {
+                case NODE_SSID:
+                    ssid = getPpsNodeValue(child);
+                    break;
+                default:
+                    throw new ParsingException("Unknown node under SPExclusion instance");
+            }
+        }
+        return ssid;
+    }
+
+    /**
+     * Parse configurations under PerProviderSubscription/Policy/RequiredProtoPortTuple subtree.
+     *
+     * @param node PPSNode representing the root of the
+     *             PerProviderSubscription/Policy/RequiredProtoPortTuple subtree
+     * @return Map of IP Protocol to Port Number tuples
+     * @throws ParsingException
+     */
+    private static Map<Integer, String> parseRequiredProtoPortTuple(PPSNode node)
+            throws ParsingException {
+        if (node.isLeaf()) {
+            throw new ParsingException("Leaf node not expected for RequiredProtoPortTuple");
+        }
+        Map<Integer, String> protoPortTupleMap = new HashMap<>();
+        for (PPSNode child : node.getChildren()) {
+            Pair<Integer, String> protoPortTuple = parseProtoPortTuple(child);
+            protoPortTupleMap.put(protoPortTuple.first, protoPortTuple.second);
+        }
+        return protoPortTupleMap;
+    }
+
+    /**
+     * Parse configurations under PerProviderSubscription/Policy/RequiredProtoPortTuple/<X+>
+     * subtree.
+     *
+     * @param node PPSNode representing the root of the
+     *             PerProviderSubscription/Policy/RequiredProtoPortTuple/<X+> subtree
+     * @return Pair of IP Protocol to Port Number tuple
+     * @throws ParsingException
+     */
+    private static Pair<Integer, String> parseProtoPortTuple(PPSNode node)
+            throws ParsingException {
+        if (node.isLeaf()) {
+            throw new ParsingException("Leaf node not expected for RequiredProtoPortTuple "
+                    + "instance");
+        }
+        int proto = Integer.MIN_VALUE;
+        String ports = null;
+        for (PPSNode child : node.getChildren()) {
+            switch (child.getName()) {
+                case NODE_IP_PROTOCOL:
+                    proto = parseInteger(getPpsNodeValue(child));
+                    break;
+                case NODE_PORT_NUMBER:
+                    ports = getPpsNodeValue(child);
+                    break;
+                default:
+                    throw new ParsingException("Unknown node under RequiredProtoPortTuple instance"
+                            + child.getName());
+            }
+        }
+        if (proto == Integer.MIN_VALUE) {
+            throw new ParsingException("Missing IPProtocol field");
+        }
+        if (ports == null) {
+            throw new ParsingException("Missing PortNumber field");
+        }
+        return Pair.create(proto, ports);
+    }
+
+    /**
+     * Parse configurations under PerProviderSubscription/AAAServerTrustRoot subtree.
+     *
+     * @param node PPSNode representing the root of PerProviderSubscription/AAAServerTrustRoot
+     *             subtree
+     * @return Map of certificate URL with the corresponding certificate fingerprint
+     * @throws ParsingException
+     */
+    private static Map<String, byte[]> parseAAAServerTrustRootList(PPSNode node)
+            throws ParsingException {
+        if (node.isLeaf()) {
+            throw new ParsingException("Leaf node not expected for AAAServerTrustRoot");
+        }
+        Map<String, byte[]> certList = new HashMap<>();
+        for (PPSNode child : node.getChildren()) {
+            Pair<String, byte[]> certTuple = parseTrustRoot(child);
+            certList.put(certTuple.first, certTuple.second);
+        }
+        return certList;
+    }
+
+    /**
+     * Parse configurations under PerProviderSubscription/SubscriptionParameter subtree.
+     *
+     * @param node PPSNode representing the root of PerProviderSubscription/SubscriptionParameter
+     *             subtree
+     * @param config Instance of {@link PasspointConfiguration}
+     * @throws ParsingException
+     */
+    private static void parseSubscriptionParameter(PPSNode node, PasspointConfiguration config)
+            throws ParsingException {
+        if (node.isLeaf()) {
+            throw new ParsingException("Leaf node not expected for SubscriptionParameter");
+        }
+        for (PPSNode child : node.getChildren()) {
+            switch (child.getName()) {
+                case NODE_CREATION_DATE:
+                    config.setSubscriptionCreationTimeInMillis(parseDate(getPpsNodeValue(child)));
+                    break;
+                case NODE_EXPIRATION_DATE:
+                    config.setSubscriptionExpirationTimeInMillis(parseDate(getPpsNodeValue(child)));
+                    break;
+                case NODE_TYPE_OF_SUBSCRIPTION:
+                    config.setSubscriptionType(getPpsNodeValue(child));
+                    break;
+                case NODE_USAGE_LIMITS:
+                    parseUsageLimits(child, config);
+                    break;
+                default:
+                    throw new ParsingException("Unknown node under SubscriptionParameter"
+                            + child.getName());
+            }
+        }
+    }
+
+    /**
+     * Parse configurations under PerProviderSubscription/SubscriptionParameter/UsageLimits
+     * subtree.
+     *
+     * @param node PPSNode representing the root of
+     *             PerProviderSubscription/SubscriptionParameter/UsageLimits subtree
+     * @param config Instance of {@link PasspointConfiguration}
+     * @throws ParsingException
+     */
+    private static void parseUsageLimits(PPSNode node, PasspointConfiguration config)
+            throws ParsingException {
+        if (node.isLeaf()) {
+            throw new ParsingException("Leaf node not expected for UsageLimits");
+        }
+        for (PPSNode child : node.getChildren()) {
+            switch (child.getName()) {
+                case NODE_DATA_LIMIT:
+                    config.setUsageLimitDataLimit(parseLong(getPpsNodeValue(child), 10));
+                    break;
+                case NODE_START_DATE:
+                    config.setUsageLimitStartTimeInMillis(parseDate(getPpsNodeValue(child)));
+                    break;
+                case NODE_TIME_LIMIT:
+                    config.setUsageLimitTimeLimitInMinutes(parseLong(getPpsNodeValue(child), 10));
+                    break;
+                case NODE_USAGE_TIME_PERIOD:
+                    config.setUsageLimitUsageTimePeriodInMinutes(
+                            parseLong(getPpsNodeValue(child), 10));
+                    break;
+                default:
+                    throw new ParsingException("Unknown node under UsageLimits"
+                            + child.getName());
+            }
+        }
+    }
+
+    /**
+     * Convert a hex string to a byte array.
+     *
+     * @param str String containing hex values
+     * @return byte[]
+     * @throws ParsingException
+     */
+    private static byte[] parseHexString(String str) throws ParsingException {
+        if ((str.length() & 1) == 1) {
+            throw new ParsingException("Odd length hex string: " + str.length());
+        }
+
+        byte[] result = new byte[str.length() / 2];
+        for (int i = 0; i < result.length; i++) {
+          int index = i * 2;
+          try {
+              result[i] = (byte) Integer.parseInt(str.substring(index, index + 2), 16);
+          } catch (NumberFormatException e) {
+              throw new ParsingException("Invalid hex string: " + str);
+          }
+        }
+        return result;
+    }
+
+    /**
+     * Convert a date string to the number of milliseconds since January 1, 1970, 00:00:00 GMT.
+     *
+     * @param dateStr String in the format of yyyy-MM-dd'T'HH:mm:ss'Z'
+     * @return number of milliseconds
+     * @throws ParsingException
+     */
+    private static long parseDate(String dateStr) throws ParsingException {
+        try {
+            DateFormat format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
+            return format.parse(dateStr).getTime();
+        } catch (ParseException pe) {
+            throw new ParsingException("Badly formatted time: " + dateStr);
+        }
+    }
+
+    /**
+     * Parse an integer string.
+     *
+     * @param value String of integer value
+     * @return int
+     * @throws ParsingException
+     */
+    private static int parseInteger(String value) throws ParsingException {
+        try {
+            return Integer.parseInt(value);
+        } catch (NumberFormatException e) {
+            throw new ParsingException("Invalid integer value: " + value);
+        }
+    }
+
+    /**
+     * Parse a string representing a long integer.
+     *
+     * @param value String of long integer value
+     * @return long
+     * @throws ParsingException
+     */
+    private static long parseLong(String value, int radix) throws ParsingException {
+        try {
+            return Long.parseLong(value, radix);
+        } catch (NumberFormatException e) {
+            throw new ParsingException("Invalid long integer value: " + value);
+        }
+    }
+
+    /**
+     * Convert a List<Long> to a primitive long array long[].
+     *
+     * @param list List to be converted
+     * @return long[]
+     */
+    private static long[] convertFromLongList(List<Long> list) {
+        Long[] objectArray = list.toArray(new Long[list.size()]);
+        long[] primitiveArray = new long[objectArray.length];
+        for (int i = 0; i < objectArray.length; i++) {
+            primitiveArray[i] = objectArray[i].longValue();
+        }
+        return primitiveArray;
+    }
+}
diff --git a/android/net/wifi/hotspot2/omadm/XMLNode.java b/android/net/wifi/hotspot2/omadm/XMLNode.java
new file mode 100644
index 0000000..959d505
--- /dev/null
+++ b/android/net/wifi/hotspot2/omadm/XMLNode.java
@@ -0,0 +1,109 @@
+/**
+ * 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.wifi.hotspot2.omadm;
+
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * A class represent a node in an XML tree. Each node is an XML element.
+ * Used by {@link XMLParser} for parsing/converting each XML element to XMLNode.
+ *
+ * @hide
+ */
+public class XMLNode {
+    private final String mTag;
+    private final List<XMLNode> mChildren;
+    private final XMLNode mParent;
+    private StringBuilder mTextBuilder;
+    private String mText;
+
+    public XMLNode(XMLNode parent, String tag) {
+        mTag = tag;
+        mParent = parent;
+        mChildren = new ArrayList<>();
+        mTextBuilder = new StringBuilder();
+        mText = null;
+    }
+
+    /**
+     * Adding a text to this node. Invoked by {@link XMLParser#characters}.
+     *
+     * @param text String to be added
+     */
+    public void addText(String text) {
+        mTextBuilder.append(text);
+    }
+
+    /**
+     * Adding a child node to this node. Invoked by {@link XMLParser#startElement}.
+     *
+     * @param child XMLNode to be added
+     */
+    public void addChild(XMLNode child) {
+        mChildren.add(child);
+    }
+
+    /**
+     * Invoked when the end of the XML element is detected. Used for further processing
+     * of the text enclosed within this XML element. Invoked by {@link XMLParser#endElement}.
+     */
+    public void close() {
+        // Remove the leading and the trailing whitespaces.
+        mText = mTextBuilder.toString().trim();
+        mTextBuilder = null;
+    }
+
+    public String getTag() {
+        return mTag;
+    }
+
+    public XMLNode getParent() {
+        return mParent;
+    }
+
+    public String getText() {
+        return mText;
+    }
+
+    public List<XMLNode> getChildren() {
+        return mChildren;
+    }
+
+    @Override
+    public boolean equals(Object thatObject) {
+        if (this == thatObject) {
+            return true;
+        }
+        if (!(thatObject instanceof XMLNode)) {
+            return false;
+        }
+        XMLNode that = (XMLNode) thatObject;
+
+        return TextUtils.equals(mTag, that.mTag) &&
+                TextUtils.equals(mText, that.mText) &&
+                mChildren.equals(that.mChildren);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mTag, mText, mChildren);
+    }
+}
diff --git a/android/net/wifi/hotspot2/omadm/XMLParser.java b/android/net/wifi/hotspot2/omadm/XMLParser.java
new file mode 100644
index 0000000..948052c
--- /dev/null
+++ b/android/net/wifi/hotspot2/omadm/XMLParser.java
@@ -0,0 +1,108 @@
+/*
+ * 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.wifi.hotspot2.omadm;
+
+import org.xml.sax.Attributes;
+import org.xml.sax.InputSource;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import android.text.TextUtils;
+
+import java.io.IOException;
+import java.io.StringReader;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+
+/**
+ * Class for parsing an XML string to an XML tree represented by {@link XMLNode}.
+ *
+ * The original XML string:
+ * <root>
+ *   <tag1>text1</tag1>
+ *   <tag2>
+ *     <tag3>text3</tag3>
+ *   </tag2>
+ * </root>
+ *
+ * The XML tree representation:
+ *                  [root]
+ *                     |
+ *                     |
+ *   [tag1, text1]-----|-----[tag2]
+ *                             |
+ *                             |
+ *                       [tag3, text3]
+ *
+ * @hide
+ */
+public class XMLParser extends DefaultHandler {
+    private XMLNode mRoot = null;
+    private XMLNode mCurrent = null;
+
+    public XMLNode parse(String text) throws IOException, SAXException {
+        if (TextUtils.isEmpty(text)) {
+            throw new IOException("XML string not provided");
+        }
+
+        // Reset pointers.
+        mRoot = null;
+        mCurrent = null;
+
+        try {
+            SAXParser parser = SAXParserFactory.newInstance().newSAXParser();
+            parser.parse(new InputSource(new StringReader(text)), this);
+            return mRoot;
+        } catch (ParserConfigurationException pce) {
+            throw new SAXException(pce);
+        }
+    }
+
+    @Override
+    public void startElement(String uri, String localName, String qName, Attributes attributes)
+            throws SAXException {
+        XMLNode parent = mCurrent;
+
+        mCurrent = new XMLNode(parent, qName);
+
+        if (mRoot == null) {
+            mRoot = mCurrent;
+        } else if (parent == null) {
+            throw new SAXException("More than one root nodes");
+        } else {
+            parent.addChild(mCurrent);
+        }
+    }
+
+    @Override
+    public void endElement(String uri, String localName, String qName) throws SAXException {
+        if (!qName.equals(mCurrent.getTag())) {
+            throw new SAXException("End tag '" + qName + "' doesn't match current node: " +
+                    mCurrent);
+        }
+
+        mCurrent.close();
+        mCurrent = mCurrent.getParent();
+    }
+
+    @Override
+    public void characters(char[] ch, int start, int length) throws SAXException {
+        mCurrent.addText(new String(ch, start, length));
+    }
+}
diff --git a/android/net/wifi/hotspot2/pps/Credential.java b/android/net/wifi/hotspot2/pps/Credential.java
new file mode 100644
index 0000000..9409c03
--- /dev/null
+++ b/android/net/wifi/hotspot2/pps/Credential.java
@@ -0,0 +1,1296 @@
+/**
+ * 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.wifi.hotspot2.pps;
+
+import android.net.wifi.EAPConstants;
+import android.net.wifi.ParcelUtil;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.PrivateKey;
+import java.security.cert.CertificateEncodingException;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+import java.util.Date;
+import java.util.HashSet;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * Class representing Credential subtree in the PerProviderSubscription (PPS)
+ * Management Object (MO) tree.
+ * For more info, refer to Hotspot 2.0 PPS MO defined in section 9.1 of the Hotspot 2.0
+ * Release 2 Technical Specification.
+ *
+ * In addition to the fields in the Credential subtree, this will also maintain necessary
+ * information for the private key and certificates associated with this credential.
+ */
+public final class Credential implements Parcelable {
+    private static final String TAG = "Credential";
+
+    /**
+     * Max string length for realm.  Refer to Credential/Realm node in Hotspot 2.0 Release 2
+     * Technical Specification Section 9.1 for more info.
+     */
+    private static final int MAX_REALM_BYTES = 253;
+
+    /**
+     * The time this credential is created. It is in the format of number
+     * of milliseconds since January 1, 1970, 00:00:00 GMT.
+     * Using Long.MIN_VALUE to indicate unset value.
+     */
+    private long mCreationTimeInMillis = Long.MIN_VALUE;
+    /**
+     * @hide
+     */
+    public void setCreationTimeInMillis(long creationTimeInMillis) {
+        mCreationTimeInMillis = creationTimeInMillis;
+    }
+    /**
+     * @hide
+     */
+    public long getCreationTimeInMillis() {
+        return mCreationTimeInMillis;
+    }
+
+    /**
+     * The time this credential will expire. It is in the format of number
+     * of milliseconds since January 1, 1970, 00:00:00 GMT.
+    * Using Long.MIN_VALUE to indicate unset value.
+     */
+    private long mExpirationTimeInMillis = Long.MIN_VALUE;
+    /**
+     * @hide
+     */
+    public void setExpirationTimeInMillis(long expirationTimeInMillis) {
+        mExpirationTimeInMillis = expirationTimeInMillis;
+    }
+    /**
+     * @hide
+     */
+    public long getExpirationTimeInMillis() {
+        return mExpirationTimeInMillis;
+    }
+
+    /**
+     * The realm associated with this credential.  It will be used to determine
+     * if this credential can be used to authenticate with a given hotspot by
+     * comparing the realm specified in that hotspot's ANQP element.
+     */
+    private String mRealm = null;
+    /**
+     * Set the realm associated with this credential.
+     *
+     * @param realm The realm to set to
+     */
+    public void setRealm(String realm) {
+        mRealm = realm;
+    }
+    /**
+     * Get the realm associated with this credential.
+     *
+     * @return the realm associated with this credential
+     */
+    public String getRealm() {
+        return mRealm;
+    }
+
+    /**
+     * When set to true, the device should check AAA (Authentication, Authorization,
+     * and Accounting) server's certificate during EAP (Extensible Authentication
+     * Protocol) authentication.
+     */
+    private boolean mCheckAaaServerCertStatus = false;
+    /**
+     * @hide
+     */
+    public void setCheckAaaServerCertStatus(boolean checkAaaServerCertStatus) {
+        mCheckAaaServerCertStatus = checkAaaServerCertStatus;
+    }
+    /**
+     * @hide
+     */
+    public boolean getCheckAaaServerCertStatus() {
+        return mCheckAaaServerCertStatus;
+    }
+
+    /**
+     * Username-password based credential.
+     * Contains the fields under PerProviderSubscription/Credential/UsernamePassword subtree.
+     */
+    public static final class UserCredential implements Parcelable {
+        /**
+         * Maximum string length for username.  Refer to Credential/UsernamePassword/Username
+         * node in Hotspot 2.0 Release 2 Technical Specification Section 9.1 for more info.
+         */
+        private static final int MAX_USERNAME_BYTES = 63;
+
+        /**
+         * Maximum string length for password.  Refer to Credential/UsernamePassword/Password
+         * in Hotspot 2.0 Release 2 Technical Specification Section 9.1 for more info.
+         */
+        private static final int MAX_PASSWORD_BYTES = 255;
+
+        /**
+         * Supported authentication methods.
+         * @hide
+         */
+        public static final String AUTH_METHOD_PAP = "PAP";
+        /** @hide */
+        public static final String AUTH_METHOD_MSCHAP = "MS-CHAP";
+        /** @hide */
+        public static final String AUTH_METHOD_MSCHAPV2 = "MS-CHAP-V2";
+
+        /**
+         * Supported Non-EAP inner methods.  Refer to
+         * Credential/UsernamePassword/EAPMethod/InnerEAPType in Hotspot 2.0 Release 2 Technical
+         * Specification Section 9.1 for more info.
+         */
+        private static final Set<String> SUPPORTED_AUTH = new HashSet<String>(
+                Arrays.asList(AUTH_METHOD_PAP, AUTH_METHOD_MSCHAP, AUTH_METHOD_MSCHAPV2));
+
+        /**
+         * Username of the credential.
+         */
+        private String mUsername = null;
+        /**
+         * Set the username associated with this user credential.
+         *
+         * @param username The username to set to
+         */
+        public void setUsername(String username) {
+            mUsername = username;
+        }
+        /**
+         * Get the username associated with this user credential.
+         *
+         * @return the username associated with this user credential
+         */
+        public String getUsername() {
+            return mUsername;
+        }
+
+        /**
+         * Base64-encoded password.
+         */
+        private String mPassword = null;
+        /**
+         * Set the Base64-encoded password associated with this user credential.
+         *
+         * @param password The password to set to
+         */
+        public void setPassword(String password) {
+            mPassword = password;
+        }
+        /**
+         * Get the Base64-encoded password associated with this user credential.
+         *
+         * @return the Base64-encoded password associated with this user credential
+         */
+        public String getPassword() {
+            return mPassword;
+        }
+
+        /**
+         * Flag indicating if the password is machine managed.
+         */
+        private boolean mMachineManaged = false;
+        /**
+         * @hide
+         */
+        public void setMachineManaged(boolean machineManaged) {
+            mMachineManaged = machineManaged;
+        }
+        /**
+         * @hide
+         */
+        public boolean getMachineManaged() {
+            return mMachineManaged;
+        }
+
+        /**
+         * The name of the application used to generate the password.
+         */
+        private String mSoftTokenApp = null;
+        /**
+         * @hide
+         */
+        public void setSoftTokenApp(String softTokenApp) {
+            mSoftTokenApp = softTokenApp;
+        }
+        /**
+         * @hide
+         */
+        public String getSoftTokenApp() {
+            return mSoftTokenApp;
+        }
+
+        /**
+         * Flag indicating if this credential is usable on other mobile devices as well.
+         */
+        private boolean mAbleToShare = false;
+        /**
+         * @hide
+         */
+        public void setAbleToShare(boolean ableToShare) {
+            mAbleToShare = ableToShare;
+        }
+        /**
+         * @hide
+         */
+        public boolean getAbleToShare() {
+            return mAbleToShare;
+        }
+
+        /**
+         * EAP (Extensible Authentication Protocol) method type.
+         * Refer to
+         * <a href="http://www.iana.org/assignments/eap-numbers/eap-numbers.xml#eap-numbers-4">
+         * EAP Numbers</a> for valid values.
+         * Using Integer.MIN_VALUE to indicate unset value.
+         */
+        private int mEapType = Integer.MIN_VALUE;
+        /**
+         * Set the EAP (Extensible Authentication Protocol) method type associated with this
+         * user credential.
+         * Refer to
+         * <a href="http://www.iana.org/assignments/eap-numbers/eap-numbers.xml#eap-numbers-4">
+         * EAP Numbers</a> for valid values.
+         *
+         * @param eapType The EAP method type associated with this user credential
+         */
+        public void setEapType(int eapType) {
+            mEapType = eapType;
+        }
+        /**
+         * Get the EAP (Extensible Authentication Protocol) method type associated with this
+         * user credential.
+         *
+         * @return EAP method type
+         */
+        public int getEapType() {
+            return mEapType;
+        }
+
+        /**
+         * Non-EAP inner authentication method.
+         */
+        private String mNonEapInnerMethod = null;
+        /**
+         * Set the inner non-EAP method associated with this user credential.
+         *
+         * @param nonEapInnerMethod The non-EAP inner method to set to
+         */
+        public void setNonEapInnerMethod(String nonEapInnerMethod) {
+            mNonEapInnerMethod = nonEapInnerMethod;
+        }
+        /**
+         * Get the inner non-EAP method associated with this user credential.
+         *
+         * @return Non-EAP inner method associated with this user credential
+         */
+        public String getNonEapInnerMethod() {
+            return mNonEapInnerMethod;
+        }
+
+        /**
+         * Constructor for creating UserCredential with default values.
+         */
+        public UserCredential() {}
+
+        /**
+         * Copy constructor.
+         *
+         * @param source The source to copy from
+         */
+        public UserCredential(UserCredential source) {
+            if (source != null) {
+                mUsername = source.mUsername;
+                mPassword = source.mPassword;
+                mMachineManaged = source.mMachineManaged;
+                mSoftTokenApp = source.mSoftTokenApp;
+                mAbleToShare = source.mAbleToShare;
+                mEapType = source.mEapType;
+                mNonEapInnerMethod = source.mNonEapInnerMethod;
+            }
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeString(mUsername);
+            dest.writeString(mPassword);
+            dest.writeInt(mMachineManaged ? 1 : 0);
+            dest.writeString(mSoftTokenApp);
+            dest.writeInt(mAbleToShare ? 1 : 0);
+            dest.writeInt(mEapType);
+            dest.writeString(mNonEapInnerMethod);
+        }
+
+        @Override
+        public boolean equals(Object thatObject) {
+            if (this == thatObject) {
+                return true;
+            }
+            if (!(thatObject instanceof UserCredential)) {
+                return false;
+            }
+
+            UserCredential that = (UserCredential) thatObject;
+            return TextUtils.equals(mUsername, that.mUsername)
+                    && TextUtils.equals(mPassword, that.mPassword)
+                    && mMachineManaged == that.mMachineManaged
+                    && TextUtils.equals(mSoftTokenApp, that.mSoftTokenApp)
+                    && mAbleToShare == that.mAbleToShare
+                    && mEapType == that.mEapType
+                    && TextUtils.equals(mNonEapInnerMethod, that.mNonEapInnerMethod);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mUsername, mPassword, mMachineManaged, mSoftTokenApp,
+                    mAbleToShare, mEapType, mNonEapInnerMethod);
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder builder = new StringBuilder();
+            builder.append("Username: ").append(mUsername).append("\n");
+            builder.append("MachineManaged: ").append(mMachineManaged).append("\n");
+            builder.append("SoftTokenApp: ").append(mSoftTokenApp).append("\n");
+            builder.append("AbleToShare: ").append(mAbleToShare).append("\n");
+            builder.append("EAPType: ").append(mEapType).append("\n");
+            builder.append("AuthMethod: ").append(mNonEapInnerMethod).append("\n");
+            return builder.toString();
+        }
+
+        /**
+         * Validate the configuration data.
+         *
+         * @return true on success or false on failure
+         * @hide
+         */
+        public boolean validate() {
+            if (TextUtils.isEmpty(mUsername)) {
+                Log.d(TAG, "Missing username");
+                return false;
+            }
+            if (mUsername.getBytes(StandardCharsets.UTF_8).length > MAX_USERNAME_BYTES) {
+                Log.d(TAG, "username exceeding maximum length: "
+                        + mUsername.getBytes(StandardCharsets.UTF_8).length);
+                return false;
+            }
+
+            if (TextUtils.isEmpty(mPassword)) {
+                Log.d(TAG, "Missing password");
+                return false;
+            }
+            if (mPassword.getBytes(StandardCharsets.UTF_8).length > MAX_PASSWORD_BYTES) {
+                Log.d(TAG, "password exceeding maximum length: "
+                        + mPassword.getBytes(StandardCharsets.UTF_8).length);
+                return false;
+            }
+
+            // Only supports EAP-TTLS for user credential.
+            if (mEapType != EAPConstants.EAP_TTLS) {
+                Log.d(TAG, "Invalid EAP Type for user credential: " + mEapType);
+                return false;
+            }
+
+            // Verify Non-EAP inner method for EAP-TTLS.
+            if (!SUPPORTED_AUTH.contains(mNonEapInnerMethod)) {
+                Log.d(TAG, "Invalid non-EAP inner method for EAP-TTLS: " + mNonEapInnerMethod);
+                return false;
+            }
+            return true;
+        }
+
+        public static final @android.annotation.NonNull Creator<UserCredential> CREATOR =
+            new Creator<UserCredential>() {
+                @Override
+                public UserCredential createFromParcel(Parcel in) {
+                    UserCredential userCredential = new UserCredential();
+                    userCredential.setUsername(in.readString());
+                    userCredential.setPassword(in.readString());
+                    userCredential.setMachineManaged(in.readInt() != 0);
+                    userCredential.setSoftTokenApp(in.readString());
+                    userCredential.setAbleToShare(in.readInt() != 0);
+                    userCredential.setEapType(in.readInt());
+                    userCredential.setNonEapInnerMethod(in.readString());
+                    return userCredential;
+                }
+
+                @Override
+                public UserCredential[] newArray(int size) {
+                    return new UserCredential[size];
+                }
+            };
+    }
+    private UserCredential mUserCredential = null;
+    /**
+     * Set the user credential information.
+     *
+     * @param userCredential The user credential to set to
+     */
+    public void setUserCredential(UserCredential userCredential) {
+        mUserCredential = userCredential;
+    }
+    /**
+     * Get the user credential information.
+     *
+     * @return user credential information
+     */
+    public UserCredential getUserCredential() {
+        return mUserCredential;
+    }
+
+    /**
+     * Certificate based credential.  This is used for EAP-TLS.
+     * Contains fields under PerProviderSubscription/Credential/DigitalCertificate subtree.
+     */
+    public static final class CertificateCredential implements Parcelable {
+        /**
+         * Supported certificate types.
+         * @hide
+         */
+        public static final String CERT_TYPE_X509V3 = "x509v3";
+
+        /**
+         * Certificate SHA-256 fingerprint length.
+         */
+        private static final int CERT_SHA256_FINGER_PRINT_LENGTH = 32;
+
+        /**
+         * Certificate type.
+         */
+        private String mCertType = null;
+        /**
+         * Set the certificate type associated with this certificate credential.
+         *
+         * @param certType The certificate type to set to
+         */
+        public void setCertType(String certType) {
+            mCertType = certType;
+        }
+        /**
+         * Get the certificate type associated with this certificate credential.
+         *
+         * @return certificate type
+         */
+        public String getCertType() {
+            return mCertType;
+        }
+
+        /**
+         * The SHA-256 fingerprint of the certificate.
+         */
+        private byte[] mCertSha256Fingerprint = null;
+        /**
+         * Set the certificate SHA-256 fingerprint associated with this certificate credential.
+         *
+         * @param certSha256Fingerprint The certificate fingerprint to set to
+         */
+        public void setCertSha256Fingerprint(byte[] certSha256Fingerprint) {
+            mCertSha256Fingerprint = certSha256Fingerprint;
+        }
+        /**
+         * Get the certificate SHA-256 fingerprint associated with this certificate credential.
+         *
+         * @return certificate SHA-256 fingerprint
+         */
+        public byte[] getCertSha256Fingerprint() {
+            return mCertSha256Fingerprint;
+        }
+
+        /**
+         * Constructor for creating CertificateCredential with default values.
+         */
+        public CertificateCredential() {}
+
+        /**
+         * Copy constructor.
+         *
+         * @param source The source to copy from
+         */
+        public CertificateCredential(CertificateCredential source) {
+            if (source != null) {
+                mCertType = source.mCertType;
+                if (source.mCertSha256Fingerprint != null) {
+                    mCertSha256Fingerprint = Arrays.copyOf(source.mCertSha256Fingerprint,
+                                                          source.mCertSha256Fingerprint.length);
+                }
+            }
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeString(mCertType);
+            dest.writeByteArray(mCertSha256Fingerprint);
+        }
+
+        @Override
+        public boolean equals(Object thatObject) {
+            if (this == thatObject) {
+                return true;
+            }
+            if (!(thatObject instanceof CertificateCredential)) {
+                return false;
+            }
+
+            CertificateCredential that = (CertificateCredential) thatObject;
+            return TextUtils.equals(mCertType, that.mCertType)
+                    && Arrays.equals(mCertSha256Fingerprint, that.mCertSha256Fingerprint);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mCertType, Arrays.hashCode(mCertSha256Fingerprint));
+        }
+
+        @Override
+        public String toString() {
+            return "CertificateType: " + mCertType + "\n";
+        }
+
+        /**
+         * Validate the configuration data.
+         *
+         * @return true on success or false on failure
+         * @hide
+         */
+        public boolean validate() {
+            if (!TextUtils.equals(CERT_TYPE_X509V3, mCertType)) {
+                Log.d(TAG, "Unsupported certificate type: " + mCertType);
+                return false;
+            }
+            if (mCertSha256Fingerprint == null
+                    || mCertSha256Fingerprint.length != CERT_SHA256_FINGER_PRINT_LENGTH) {
+                Log.d(TAG, "Invalid SHA-256 fingerprint");
+                return false;
+            }
+            return true;
+        }
+
+        public static final @android.annotation.NonNull Creator<CertificateCredential> CREATOR =
+            new Creator<CertificateCredential>() {
+                @Override
+                public CertificateCredential createFromParcel(Parcel in) {
+                    CertificateCredential certCredential = new CertificateCredential();
+                    certCredential.setCertType(in.readString());
+                    certCredential.setCertSha256Fingerprint(in.createByteArray());
+                    return certCredential;
+                }
+
+                @Override
+                public CertificateCredential[] newArray(int size) {
+                    return new CertificateCredential[size];
+                }
+            };
+    }
+    private CertificateCredential mCertCredential = null;
+    /**
+     * Set the certificate credential information.
+     *
+     * @param certCredential The certificate credential to set to
+     */
+    public void setCertCredential(CertificateCredential certCredential) {
+        mCertCredential = certCredential;
+    }
+    /**
+     * Get the certificate credential information.
+     *
+     * @return certificate credential information
+     */
+    public CertificateCredential getCertCredential() {
+        return mCertCredential;
+    }
+
+    /**
+     * SIM (Subscriber Identify Module) based credential.
+     * Contains fields under PerProviderSubscription/Credential/SIM subtree.
+     */
+    public static final class SimCredential implements Parcelable {
+        /**
+         * Maximum string length for IMSI.
+         */
+        private static final int MAX_IMSI_LENGTH = 15;
+
+        /**
+         * International Mobile Subscriber Identity, is used to identify the user
+         * of a cellular network and is a unique identification associated with all
+         * cellular networks
+         */
+        private String mImsi = null;
+        /**
+         * Set the IMSI (International Mobile Subscriber Identity) associated with this SIM
+         * credential.
+         *
+         * @param imsi The IMSI to set to
+         */
+        public void setImsi(String imsi) {
+            mImsi = imsi;
+        }
+        /**
+         * Get the IMSI (International Mobile Subscriber Identity) associated with this SIM
+         * credential.
+         *
+         * @return IMSI associated with this SIM credential
+         */
+        public String getImsi() {
+            return mImsi;
+        }
+
+        /**
+         * EAP (Extensible Authentication Protocol) method type for using SIM credential.
+         * Refer to http://www.iana.org/assignments/eap-numbers/eap-numbers.xml#eap-numbers-4
+         * for valid values.
+         * Using Integer.MIN_VALUE to indicate unset value.
+         */
+        private int mEapType = Integer.MIN_VALUE;
+        /**
+         * Set the EAP (Extensible Authentication Protocol) method type associated with this
+         * SIM credential.
+         *
+         * @param eapType The EAP method type to set to
+         */
+        public void setEapType(int eapType) {
+            mEapType = eapType;
+        }
+        /**
+         * Get the EAP (Extensible Authentication Protocol) method type associated with this
+         * SIM credential.
+         *
+         * @return EAP method type associated with this SIM credential
+         */
+        public int getEapType() {
+            return mEapType;
+        }
+
+        /**
+         * Constructor for creating SimCredential with default values.
+         */
+        public SimCredential() {}
+
+        /**
+         * Copy constructor
+         *
+         * @param source The source to copy from
+         */
+        public SimCredential(SimCredential source) {
+            if (source != null) {
+                mImsi = source.mImsi;
+                mEapType = source.mEapType;
+            }
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public boolean equals(Object thatObject) {
+            if (this == thatObject) {
+                return true;
+            }
+            if (!(thatObject instanceof SimCredential)) {
+                return false;
+            }
+
+            SimCredential that = (SimCredential) thatObject;
+            return TextUtils.equals(mImsi, that.mImsi)
+                    && mEapType == that.mEapType;
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mImsi, mEapType);
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder builder = new StringBuilder();
+            builder.append("IMSI: ").append(mImsi).append("\n");
+            builder.append("EAPType: ").append(mEapType).append("\n");
+            return builder.toString();
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeString(mImsi);
+            dest.writeInt(mEapType);
+        }
+
+        /**
+         * Validate the configuration data.
+         *
+         * @return true on success or false on failure
+         * @hide
+         */
+        public boolean validate() {
+            // Note: this only validate the format of IMSI string itself.  Additional verification
+            // will be done by WifiService at the time of provisioning to verify against the IMSI
+            // of the SIM card installed in the device.
+            if (!verifyImsi()) {
+                return false;
+            }
+            if (mEapType != EAPConstants.EAP_SIM && mEapType != EAPConstants.EAP_AKA
+                    && mEapType != EAPConstants.EAP_AKA_PRIME) {
+                Log.d(TAG, "Invalid EAP Type for SIM credential: " + mEapType);
+                return false;
+            }
+            return true;
+        }
+
+        public static final @android.annotation.NonNull Creator<SimCredential> CREATOR =
+            new Creator<SimCredential>() {
+                @Override
+                public SimCredential createFromParcel(Parcel in) {
+                    SimCredential simCredential = new SimCredential();
+                    simCredential.setImsi(in.readString());
+                    simCredential.setEapType(in.readInt());
+                    return simCredential;
+                }
+
+                @Override
+                public SimCredential[] newArray(int size) {
+                    return new SimCredential[size];
+                }
+            };
+
+        /**
+         * Verify the IMSI (International Mobile Subscriber Identity) string.  The string
+         * should contain zero or more numeric digits, and might ends with a "*" for prefix
+         * matching.
+         *
+         * @return true if IMSI is valid, false otherwise.
+         */
+        private boolean verifyImsi() {
+            if (TextUtils.isEmpty(mImsi)) {
+                Log.d(TAG, "Missing IMSI");
+                return false;
+            }
+            if (mImsi.length() > MAX_IMSI_LENGTH) {
+                Log.d(TAG, "IMSI exceeding maximum length: " + mImsi.length());
+                return false;
+            }
+
+            // Locate the first non-digit character.
+            int nonDigit;
+            char stopChar = '\0';
+            for (nonDigit = 0; nonDigit < mImsi.length(); nonDigit++) {
+                stopChar = mImsi.charAt(nonDigit);
+                if (stopChar < '0' || stopChar > '9') {
+                    break;
+                }
+            }
+
+            if (nonDigit == mImsi.length()) {
+                return true;
+            }
+            else if (nonDigit == mImsi.length()-1 && stopChar == '*') {
+                // Prefix matching.
+                return true;
+            }
+            return false;
+        }
+    }
+    private SimCredential mSimCredential = null;
+    /**
+     * Set the SIM credential information.
+     *
+     * @param simCredential The SIM credential to set to
+     */
+    public void setSimCredential(SimCredential simCredential) {
+        mSimCredential = simCredential;
+    }
+    /**
+     * Get the SIM credential information.
+     *
+     * @return SIM credential information
+     */
+    public SimCredential getSimCredential() {
+        return mSimCredential;
+    }
+
+    /**
+     * CA (Certificate Authority) X509 certificates.
+     */
+    private X509Certificate[] mCaCertificates = null;
+
+    /**
+     * Set the CA (Certification Authority) certificate associated with this credential.
+     *
+     * @param caCertificate The CA certificate to set to
+     */
+    public void setCaCertificate(X509Certificate caCertificate) {
+        mCaCertificates = null;
+        if (caCertificate != null) {
+            mCaCertificates = new X509Certificate[] {caCertificate};
+        }
+    }
+
+    /**
+     * Set the CA (Certification Authority) certificates associated with this credential.
+     *
+     * @param caCertificates The list of CA certificates to set to
+     * @hide
+     */
+    public void setCaCertificates(X509Certificate[] caCertificates) {
+        mCaCertificates = caCertificates;
+    }
+
+    /**
+     * Get the CA (Certification Authority) certificate associated with this credential.
+     *
+     * @return CA certificate associated with this credential, {@code null} if certificate is not
+     * set or certificate is more than one.
+     */
+    public X509Certificate getCaCertificate() {
+        return mCaCertificates == null || mCaCertificates.length > 1 ? null : mCaCertificates[0];
+    }
+
+    /**
+     * Get the CA (Certification Authority) certificates associated with this credential.
+     *
+     * @return The list of CA certificates associated with this credential
+     * @hide
+     */
+    public X509Certificate[] getCaCertificates() {
+        return mCaCertificates;
+    }
+
+    /**
+     * Client side X509 certificate chain.
+     */
+    private X509Certificate[] mClientCertificateChain = null;
+    /**
+     * Set the client certificate chain associated with this credential.
+     *
+     * @param certificateChain The client certificate chain to set to
+     */
+    public void setClientCertificateChain(X509Certificate[] certificateChain) {
+        mClientCertificateChain = certificateChain;
+    }
+    /**
+     * Get the client certificate chain associated with this credential.
+     *
+     * @return client certificate chain associated with this credential
+     */
+    public X509Certificate[] getClientCertificateChain() {
+        return mClientCertificateChain;
+    }
+
+    /**
+     * Client side private key.
+     */
+    private PrivateKey mClientPrivateKey = null;
+    /**
+     * Set the client private key associated with this credential.
+     *
+     * @param clientPrivateKey the client private key to set to
+     */
+    public void setClientPrivateKey(PrivateKey clientPrivateKey) {
+        mClientPrivateKey = clientPrivateKey;
+    }
+    /**
+     * Get the client private key associated with this credential.
+     *
+     * @return client private key associated with this credential.
+     */
+    public PrivateKey getClientPrivateKey() {
+        return mClientPrivateKey;
+    }
+
+    /**
+     * Constructor for creating Credential with default values.
+     */
+    public Credential() {}
+
+    /**
+     * Copy constructor.
+     *
+     * @param source The source to copy from
+     */
+    public Credential(Credential source) {
+        if (source != null) {
+            mCreationTimeInMillis = source.mCreationTimeInMillis;
+            mExpirationTimeInMillis = source.mExpirationTimeInMillis;
+            mRealm = source.mRealm;
+            mCheckAaaServerCertStatus = source.mCheckAaaServerCertStatus;
+            if (source.mUserCredential != null) {
+                mUserCredential = new UserCredential(source.mUserCredential);
+            }
+            if (source.mCertCredential != null) {
+                mCertCredential = new CertificateCredential(source.mCertCredential);
+            }
+            if (source.mSimCredential != null) {
+                mSimCredential = new SimCredential(source.mSimCredential);
+            }
+            if (source.mClientCertificateChain != null) {
+                mClientCertificateChain = Arrays.copyOf(source.mClientCertificateChain,
+                                                        source.mClientCertificateChain.length);
+            }
+            if (source.mCaCertificates != null) {
+                mCaCertificates = Arrays.copyOf(source.mCaCertificates,
+                        source.mCaCertificates.length);
+            }
+
+            mClientPrivateKey = source.mClientPrivateKey;
+        }
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeLong(mCreationTimeInMillis);
+        dest.writeLong(mExpirationTimeInMillis);
+        dest.writeString(mRealm);
+        dest.writeInt(mCheckAaaServerCertStatus ? 1 : 0);
+        dest.writeParcelable(mUserCredential, flags);
+        dest.writeParcelable(mCertCredential, flags);
+        dest.writeParcelable(mSimCredential, flags);
+        ParcelUtil.writeCertificates(dest, mCaCertificates);
+        ParcelUtil.writeCertificates(dest, mClientCertificateChain);
+        ParcelUtil.writePrivateKey(dest, mClientPrivateKey);
+    }
+
+    @Override
+    public boolean equals(Object thatObject) {
+        if (this == thatObject) {
+            return true;
+        }
+        if (!(thatObject instanceof Credential)) {
+            return false;
+        }
+
+        Credential that = (Credential) thatObject;
+        return TextUtils.equals(mRealm, that.mRealm)
+                && mCreationTimeInMillis == that.mCreationTimeInMillis
+                && mExpirationTimeInMillis == that.mExpirationTimeInMillis
+                && mCheckAaaServerCertStatus == that.mCheckAaaServerCertStatus
+                && (mUserCredential == null ? that.mUserCredential == null
+                    : mUserCredential.equals(that.mUserCredential))
+                && (mCertCredential == null ? that.mCertCredential == null
+                    : mCertCredential.equals(that.mCertCredential))
+                && (mSimCredential == null ? that.mSimCredential == null
+                    : mSimCredential.equals(that.mSimCredential))
+                && isX509CertificatesEquals(mCaCertificates, that.mCaCertificates)
+                && isX509CertificatesEquals(mClientCertificateChain, that.mClientCertificateChain)
+                && isPrivateKeyEquals(mClientPrivateKey, that.mClientPrivateKey);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mCreationTimeInMillis, mExpirationTimeInMillis, mRealm,
+                mCheckAaaServerCertStatus, mUserCredential, mCertCredential, mSimCredential,
+                mClientPrivateKey, Arrays.hashCode(mCaCertificates),
+                Arrays.hashCode(mClientCertificateChain));
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append("Realm: ").append(mRealm).append("\n");
+        builder.append("CreationTime: ").append(mCreationTimeInMillis != Long.MIN_VALUE
+                ? new Date(mCreationTimeInMillis) : "Not specified").append("\n");
+        builder.append("ExpirationTime: ").append(mExpirationTimeInMillis != Long.MIN_VALUE
+                ? new Date(mExpirationTimeInMillis) : "Not specified").append("\n");
+        builder.append("CheckAAAServerStatus: ").append(mCheckAaaServerCertStatus).append("\n");
+        if (mUserCredential != null) {
+            builder.append("UserCredential Begin ---\n");
+            builder.append(mUserCredential);
+            builder.append("UserCredential End ---\n");
+        }
+        if (mCertCredential != null) {
+            builder.append("CertificateCredential Begin ---\n");
+            builder.append(mCertCredential);
+            builder.append("CertificateCredential End ---\n");
+        }
+        if (mSimCredential != null) {
+            builder.append("SIMCredential Begin ---\n");
+            builder.append(mSimCredential);
+            builder.append("SIMCredential End ---\n");
+        }
+        return builder.toString();
+    }
+
+    /**
+     * Validate the configuration data.
+     *
+     * @param isR1 {@code true} if the configuration is for R1
+     * @return true on success or false on failure
+     * @hide
+     */
+    public boolean validate(boolean isR1) {
+        if (TextUtils.isEmpty(mRealm)) {
+            Log.d(TAG, "Missing realm");
+            return false;
+        }
+        if (mRealm.getBytes(StandardCharsets.UTF_8).length > MAX_REALM_BYTES) {
+            Log.d(TAG, "realm exceeding maximum length: "
+                    + mRealm.getBytes(StandardCharsets.UTF_8).length);
+            return false;
+        }
+
+        // Verify the credential.
+        if (mUserCredential != null) {
+            if (!verifyUserCredential(isR1)) {
+                return false;
+            }
+        } else if (mCertCredential != null) {
+            if (!verifyCertCredential(isR1)) {
+                return false;
+            }
+        } else if (mSimCredential != null) {
+            if (!verifySimCredential()) {
+                return false;
+            }
+        } else {
+            Log.d(TAG, "Missing required credential");
+            return false;
+        }
+
+        return true;
+    }
+
+    public static final @android.annotation.NonNull Creator<Credential> CREATOR =
+        new Creator<Credential>() {
+            @Override
+            public Credential createFromParcel(Parcel in) {
+                Credential credential = new Credential();
+                credential.setCreationTimeInMillis(in.readLong());
+                credential.setExpirationTimeInMillis(in.readLong());
+                credential.setRealm(in.readString());
+                credential.setCheckAaaServerCertStatus(in.readInt() != 0);
+                credential.setUserCredential(in.readParcelable(null));
+                credential.setCertCredential(in.readParcelable(null));
+                credential.setSimCredential(in.readParcelable(null));
+                credential.setCaCertificates(ParcelUtil.readCertificates(in));
+                credential.setClientCertificateChain(ParcelUtil.readCertificates(in));
+                credential.setClientPrivateKey(ParcelUtil.readPrivateKey(in));
+                return credential;
+            }
+
+            @Override
+            public Credential[] newArray(int size) {
+                return new Credential[size];
+            }
+        };
+
+    /**
+     * Verify user credential.
+     *
+     * @param isR1 {@code true} if credential is for R1
+     * @return true if user credential is valid, false otherwise.
+     */
+    private boolean verifyUserCredential(boolean isR1) {
+        if (mUserCredential == null) {
+            Log.d(TAG, "Missing user credential");
+            return false;
+        }
+        if (mCertCredential != null || mSimCredential != null) {
+            Log.d(TAG, "Contained more than one type of credential");
+            return false;
+        }
+        if (!mUserCredential.validate()) {
+            return false;
+        }
+
+        // CA certificate is required for R1 Passpoint profile.
+        // For R2, it is downloaded using cert URL provided in PPS MO after validation completes.
+        if (isR1 && mCaCertificates == null) {
+            Log.d(TAG, "Missing CA Certificate for user credential");
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Verify certificate credential, which is used for EAP-TLS.  This will verify
+     * that the necessary client key and certificates are provided.
+     *
+     * @param isR1 {@code true} if credential is for R1
+     * @return true if certificate credential is valid, false otherwise.
+     */
+    private boolean verifyCertCredential(boolean isR1) {
+        if (mCertCredential == null) {
+            Log.d(TAG, "Missing certificate credential");
+            return false;
+        }
+        if (mUserCredential != null || mSimCredential != null) {
+            Log.d(TAG, "Contained more than one type of credential");
+            return false;
+        }
+
+        if (!mCertCredential.validate()) {
+            return false;
+        }
+
+        // Verify required key and certificates for certificate credential.
+        // CA certificate is required for R1 Passpoint profile.
+        // For R2, it is downloaded using cert URL provided in PPS MO after validation completes.
+        if (isR1 && mCaCertificates == null) {
+            Log.d(TAG, "Missing CA Certificate for certificate credential");
+            return false;
+        }
+        if (mClientPrivateKey == null) {
+            Log.d(TAG, "Missing client private key for certificate credential");
+            return false;
+        }
+        try {
+            // Verify SHA-256 fingerprint for client certificate.
+            if (!verifySha256Fingerprint(mClientCertificateChain,
+                    mCertCredential.getCertSha256Fingerprint())) {
+                Log.d(TAG, "SHA-256 fingerprint mismatch");
+                return false;
+            }
+        } catch (NoSuchAlgorithmException | CertificateEncodingException e) {
+            Log.d(TAG, "Failed to verify SHA-256 fingerprint: " + e.getMessage());
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Verify SIM credential.
+     *
+     * @return true if SIM credential is valid, false otherwise.
+     */
+    private boolean verifySimCredential() {
+        if (mSimCredential == null) {
+            Log.d(TAG, "Missing SIM credential");
+            return false;
+        }
+        if (mUserCredential != null || mCertCredential != null) {
+            Log.d(TAG, "Contained more than one type of credential");
+            return false;
+        }
+        return mSimCredential.validate();
+    }
+
+    private static boolean isPrivateKeyEquals(PrivateKey key1, PrivateKey key2) {
+        if (key1 == null && key2 == null) {
+            return true;
+        }
+
+        /* Return false if only one of them is null */
+        if (key1 == null || key2 == null) {
+            return false;
+        }
+
+        return TextUtils.equals(key1.getAlgorithm(), key2.getAlgorithm()) &&
+                Arrays.equals(key1.getEncoded(), key2.getEncoded());
+    }
+
+    /**
+     * Verify two X.509 certificates are identical.
+     *
+     * @param cert1 a certificate to compare
+     * @param cert2 a certificate to compare
+     * @return {@code true} if given certificates are the same each other, {@code false} otherwise.
+     * @hide
+     */
+    public static boolean isX509CertificateEquals(X509Certificate cert1, X509Certificate cert2) {
+        if (cert1 == null && cert2 == null) {
+            return true;
+        }
+
+        /* Return false if only one of them is null */
+        if (cert1 == null || cert2 == null) {
+            return false;
+        }
+
+        boolean result = false;
+        try {
+            result = Arrays.equals(cert1.getEncoded(), cert2.getEncoded());
+        } catch (CertificateEncodingException e) {
+            /* empty, return false. */
+        }
+        return result;
+    }
+
+    private static boolean isX509CertificatesEquals(X509Certificate[] certs1,
+                                                    X509Certificate[] certs2) {
+        if (certs1 == null && certs2 == null) {
+            return true;
+        }
+
+        /* Return false if only one of them is null */
+        if (certs1 == null || certs2 == null) {
+            return false;
+        }
+
+        if (certs1.length != certs2.length) {
+            return false;
+        }
+
+        for (int i = 0; i < certs1.length; i++) {
+            if (!isX509CertificateEquals(certs1[i], certs2[i])) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Verify that the digest for a certificate in the certificate chain matches expected
+     * fingerprint.  The certificate that matches the fingerprint is the client certificate.
+     *
+     * @param certChain Chain of certificates
+     * @param expectedFingerprint The expected SHA-256 digest of the client certificate
+     * @return true if the certificate chain contains a matching certificate, false otherwise
+     * @throws NoSuchAlgorithmException
+     * @throws CertificateEncodingException
+     */
+    private static boolean verifySha256Fingerprint(X509Certificate[] certChain,
+                                                   byte[] expectedFingerprint)
+            throws NoSuchAlgorithmException, CertificateEncodingException {
+        if (certChain == null) {
+            return false;
+        }
+        MessageDigest digester = MessageDigest.getInstance("SHA-256");
+        for (X509Certificate certificate : certChain) {
+            digester.reset();
+            byte[] fingerprint = digester.digest(certificate.getEncoded());
+            if (Arrays.equals(expectedFingerprint, fingerprint)) {
+                return true;
+            }
+        }
+        return false;
+    }
+}
diff --git a/android/net/wifi/hotspot2/pps/HomeSp.java b/android/net/wifi/hotspot2/pps/HomeSp.java
new file mode 100644
index 0000000..49a76c3
--- /dev/null
+++ b/android/net/wifi/hotspot2/pps/HomeSp.java
@@ -0,0 +1,415 @@
+/**
+ * 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.wifi.hotspot2.pps;
+
+import android.os.Parcelable;
+import android.os.Parcel;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Class representing HomeSP subtree in PerProviderSubscription (PPS)
+ * Management Object (MO) tree.
+ *
+ * For more info, refer to Hotspot 2.0 PPS MO defined in section 9.1 of the Hotspot 2.0
+ * Release 2 Technical Specification.
+ */
+public final class HomeSp implements Parcelable {
+    private static final String TAG = "HomeSp";
+
+    /**
+     * Maximum number of bytes allowed for a SSID.
+     */
+    private static final int MAX_SSID_BYTES = 32;
+
+    /**
+     * Integer value used for indicating null value in the Parcel.
+     */
+    private static final int NULL_VALUE = -1;
+
+    /**
+     * FQDN (Fully Qualified Domain Name) of this home service provider.
+     */
+    private String mFqdn = null;
+    /**
+     * Set the FQDN (Fully Qualified Domain Name) associated with this home service provider.
+     *
+     * @param fqdn The FQDN to set to
+     */
+    public void setFqdn(String fqdn) {
+        mFqdn = fqdn;
+    }
+    /**
+     * Get the FQDN (Fully Qualified Domain Name) associated with this home service provider.
+     *
+     * @return the FQDN associated with this home service provider
+     */
+    public String getFqdn() {
+        return mFqdn;
+    }
+
+    /**
+     * Friendly name of this home service provider.
+     */
+    private String mFriendlyName = null;
+    /**
+     * Set the friendly name associated with this home service provider.
+     *
+     * @param friendlyName The friendly name to set to
+     */
+    public void setFriendlyName(String friendlyName) {
+        mFriendlyName = friendlyName;
+    }
+    /**
+     * Get the friendly name associated with this home service provider.
+     *
+     * @return the friendly name associated with this home service provider
+     */
+    public String getFriendlyName() {
+        return mFriendlyName;
+    }
+
+    /**
+     * Icon URL of this home service provider.
+     */
+    private String mIconUrl = null;
+    /**
+     * @hide
+     */
+    public void setIconUrl(String iconUrl) {
+        mIconUrl = iconUrl;
+    }
+    /**
+     * @hide
+     */
+    public String getIconUrl() {
+        return mIconUrl;
+    }
+
+    /**
+     * <SSID, HESSID> duple of the networks that are consider home networks.
+     *
+     * According to the Section 9.1.2 of the Hotspot 2.0 Release 2 Technical Specification,
+     * all nodes in the PSS MO are encoded using UTF-8 unless stated otherwise.  Thus, the SSID
+     * string is assumed to be encoded using UTF-8.
+     */
+    private Map<String, Long> mHomeNetworkIds = null;
+    /**
+     * @hide
+     */
+    public void setHomeNetworkIds(Map<String, Long> homeNetworkIds) {
+        mHomeNetworkIds = homeNetworkIds;
+    }
+    /**
+     * @hide
+     */
+    public Map<String, Long> getHomeNetworkIds() {
+        return mHomeNetworkIds;
+    }
+
+    /**
+     * Used for determining if this provider is a member of a given Hotspot provider.
+     * Every Organization Identifiers (OIs) in this list are required to match an OI in the
+     * the Roaming Consortium advertised by a Hotspot, in order to consider this provider
+     * as a member of that Hotspot provider (e.g. successful authentication with such Hotspot
+     * is possible).
+     *
+     * Refer to HomeSP/HomeOIList subtree in PerProviderSubscription (PPS) Management Object
+     * (MO) tree for more detail.
+     */
+    private long[] mMatchAllOis = null;
+    /**
+     * @hide
+     */
+    public void setMatchAllOis(long[] matchAllOis) {
+        mMatchAllOis = matchAllOis;
+    }
+    /**
+     * @hide
+     */
+    public long[] getMatchAllOis() {
+        return mMatchAllOis;
+    }
+
+    /**
+     * Used for determining if this provider is a member of a given Hotspot provider.
+     * Matching of any Organization Identifiers (OIs) in this list with an OI in the
+     * Roaming Consortium advertised by a Hotspot, will consider this provider as a member
+     * of that Hotspot provider (e.g. successful authentication with such Hotspot
+     * is possible).
+     *
+     * {@link #mMatchAllOIs} will have precedence over this one, meaning this list will
+     * only be used for matching if {@link #mMatchAllOIs} is null or empty.
+     *
+     * Refer to HomeSP/HomeOIList subtree in PerProviderSubscription (PPS) Management Object
+     * (MO) tree for more detail.
+     */
+    private long[] mMatchAnyOis = null;
+    /**
+     * @hide
+     */
+    public void setMatchAnyOis(long[] matchAnyOis) {
+        mMatchAnyOis = matchAnyOis;
+    }
+    /**
+     * @hide
+     */
+    public long[] getMatchAnyOis() {
+        return mMatchAnyOis;
+    }
+
+    /**
+     * List of FQDN (Fully Qualified Domain Name) of partner providers.
+     * These providers should also be regarded as home Hotspot operators.
+     * This relationship is most likely achieved via a commercial agreement or
+     * operator merges between the providers.
+     */
+    private String[] mOtherHomePartners = null;
+    /**
+     * @hide
+     */
+    public void setOtherHomePartners(String[] otherHomePartners) {
+        mOtherHomePartners = otherHomePartners;
+    }
+    /**
+     * @hide
+     */
+    public String[] getOtherHomePartners() {
+        return mOtherHomePartners;
+    }
+
+    /**
+     * List of Organization Identifiers (OIs) identifying a roaming consortium of
+     * which this provider is a member.
+     */
+    private long[] mRoamingConsortiumOis = null;
+    /**
+     * Set the Organization Identifiers (OIs) identifying a roaming consortium of which this
+     * provider is a member.
+     *
+     * @param roamingConsortiumOis Array of roaming consortium OIs
+     */
+    public void setRoamingConsortiumOis(long[] roamingConsortiumOis) {
+        mRoamingConsortiumOis = roamingConsortiumOis;
+    }
+    /**
+     * Get the Organization Identifiers (OIs) identifying a roaming consortium of which this
+     * provider is a member.
+     *
+     * @return array of roaming consortium OIs
+     */
+    public long[] getRoamingConsortiumOis() {
+        return mRoamingConsortiumOis;
+    }
+
+    /**
+     * Constructor for creating HomeSp with default values.
+     */
+    public HomeSp() {}
+
+    /**
+     * Copy constructor.
+     *
+     * @param source The source to copy from
+     */
+    public HomeSp(HomeSp source) {
+        if (source == null) {
+            return;
+        }
+        mFqdn = source.mFqdn;
+        mFriendlyName = source.mFriendlyName;
+        mIconUrl = source.mIconUrl;
+        if (source.mHomeNetworkIds != null) {
+            mHomeNetworkIds = Collections.unmodifiableMap(source.mHomeNetworkIds);
+        }
+        if (source.mMatchAllOis != null) {
+            mMatchAllOis = Arrays.copyOf(source.mMatchAllOis, source.mMatchAllOis.length);
+        }
+        if (source.mMatchAnyOis != null) {
+            mMatchAnyOis = Arrays.copyOf(source.mMatchAnyOis, source.mMatchAnyOis.length);
+        }
+        if (source.mOtherHomePartners != null) {
+            mOtherHomePartners = Arrays.copyOf(source.mOtherHomePartners,
+                    source.mOtherHomePartners.length);
+        }
+        if (source.mRoamingConsortiumOis != null) {
+            mRoamingConsortiumOis = Arrays.copyOf(source.mRoamingConsortiumOis,
+                    source.mRoamingConsortiumOis.length);
+        }
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeString(mFqdn);
+        dest.writeString(mFriendlyName);
+        dest.writeString(mIconUrl);
+        writeHomeNetworkIds(dest, mHomeNetworkIds);
+        dest.writeLongArray(mMatchAllOis);
+        dest.writeLongArray(mMatchAnyOis);
+        dest.writeStringArray(mOtherHomePartners);
+        dest.writeLongArray(mRoamingConsortiumOis);
+    }
+
+    @Override
+    public boolean equals(Object thatObject) {
+        if (this == thatObject) {
+            return true;
+        }
+        if (!(thatObject instanceof HomeSp)) {
+            return false;
+        }
+        HomeSp that = (HomeSp) thatObject;
+
+        return TextUtils.equals(mFqdn, that.mFqdn)
+                && TextUtils.equals(mFriendlyName, that.mFriendlyName)
+                && TextUtils.equals(mIconUrl, that.mIconUrl)
+                && (mHomeNetworkIds == null ? that.mHomeNetworkIds == null
+                        : mHomeNetworkIds.equals(that.mHomeNetworkIds))
+                && Arrays.equals(mMatchAllOis, that.mMatchAllOis)
+                && Arrays.equals(mMatchAnyOis, that.mMatchAnyOis)
+                && Arrays.equals(mOtherHomePartners, that.mOtherHomePartners)
+                && Arrays.equals(mRoamingConsortiumOis, that.mRoamingConsortiumOis);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mFqdn, mFriendlyName, mIconUrl, mHomeNetworkIds, mMatchAllOis,
+                mMatchAnyOis, mOtherHomePartners, mRoamingConsortiumOis);
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append("FQDN: ").append(mFqdn).append("\n");
+        builder.append("FriendlyName: ").append(mFriendlyName).append("\n");
+        builder.append("IconURL: ").append(mIconUrl).append("\n");
+        builder.append("HomeNetworkIDs: ").append(mHomeNetworkIds).append("\n");
+        builder.append("MatchAllOIs: ").append(mMatchAllOis).append("\n");
+        builder.append("MatchAnyOIs: ").append(mMatchAnyOis).append("\n");
+        builder.append("OtherHomePartners: ").append(mOtherHomePartners).append("\n");
+        builder.append("RoamingConsortiumOIs: ").append(mRoamingConsortiumOis).append("\n");
+        return builder.toString();
+    }
+
+    /**
+     * Validate HomeSp data.
+     *
+     * @return true on success or false on failure
+     * @hide
+     */
+    public boolean validate() {
+        if (TextUtils.isEmpty(mFqdn)) {
+            Log.d(TAG, "Missing FQDN");
+            return false;
+        }
+        if (TextUtils.isEmpty(mFriendlyName)) {
+            Log.d(TAG, "Missing friendly name");
+            return false;
+        }
+        // Verify SSIDs specified in the NetworkID
+        if (mHomeNetworkIds != null) {
+            for (Map.Entry<String, Long> entry : mHomeNetworkIds.entrySet()) {
+                if (entry.getKey() == null ||
+                        entry.getKey().getBytes(StandardCharsets.UTF_8).length > MAX_SSID_BYTES) {
+                    Log.d(TAG, "Invalid SSID in HomeNetworkIDs");
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    public static final @android.annotation.NonNull Creator<HomeSp> CREATOR =
+        new Creator<HomeSp>() {
+            @Override
+            public HomeSp createFromParcel(Parcel in) {
+                HomeSp homeSp = new HomeSp();
+                homeSp.setFqdn(in.readString());
+                homeSp.setFriendlyName(in.readString());
+                homeSp.setIconUrl(in.readString());
+                homeSp.setHomeNetworkIds(readHomeNetworkIds(in));
+                homeSp.setMatchAllOis(in.createLongArray());
+                homeSp.setMatchAnyOis(in.createLongArray());
+                homeSp.setOtherHomePartners(in.createStringArray());
+                homeSp.setRoamingConsortiumOis(in.createLongArray());
+                return homeSp;
+            }
+
+            @Override
+            public HomeSp[] newArray(int size) {
+                return new HomeSp[size];
+            }
+
+            /**
+             * Helper function for reading a Home Network IDs map from a Parcel.
+             *
+             * @param in The Parcel to read from
+             * @return Map of home network IDs
+             */
+            private Map<String, Long> readHomeNetworkIds(Parcel in) {
+                int size = in.readInt();
+                if (size == NULL_VALUE) {
+                    return null;
+                }
+                Map<String, Long> networkIds = new HashMap<>(size);
+                for (int i = 0; i < size; i++) {
+                    String key = in.readString();
+                    Long value = null;
+                    long readValue = in.readLong();
+                    if (readValue != NULL_VALUE) {
+                        value = Long.valueOf(readValue);
+                    }
+                    networkIds.put(key, value);
+                }
+                return networkIds;
+            }
+        };
+
+    /**
+     * Helper function for writing Home Network IDs map to a Parcel.
+     *
+     * @param dest The Parcel to write to
+     * @param networkIds The map of home network IDs
+     */
+    private static void writeHomeNetworkIds(Parcel dest, Map<String, Long> networkIds) {
+        if (networkIds == null) {
+            dest.writeInt(NULL_VALUE);
+            return;
+        }
+        dest.writeInt(networkIds.size());
+        for (Map.Entry<String, Long> entry : networkIds.entrySet()) {
+            dest.writeString(entry.getKey());
+            if (entry.getValue() == null) {
+                dest.writeLong(NULL_VALUE);
+            } else {
+                dest.writeLong(entry.getValue());
+            }
+        }
+    }
+}
diff --git a/android/net/wifi/hotspot2/pps/Policy.java b/android/net/wifi/hotspot2/pps/Policy.java
new file mode 100644
index 0000000..b0a2cc3
--- /dev/null
+++ b/android/net/wifi/hotspot2/pps/Policy.java
@@ -0,0 +1,576 @@
+/**
+ * Copyright (c) 2017, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.hotspot2.pps;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * Class representing Policy subtree in PerProviderSubscription (PPS)
+ * Management Object (MO) tree.
+ *
+ * The Policy specifies additional criteria for Passpoint network selections, such as preferred
+ * roaming partner, minimum backhaul bandwidth, and etc. It also provides the meta data for
+ * updating the policy.
+ *
+ * For more info, refer to Hotspot 2.0 PPS MO defined in section 9.1 of the Hotspot 2.0
+ * Release 2 Technical Specification.
+ *
+ * @hide
+ */
+public final class Policy implements Parcelable {
+    private static final String TAG = "Policy";
+
+    /**
+     * Maximum number of SSIDs in the exclusion list.
+     */
+    private static final int MAX_EXCLUSION_SSIDS = 128;
+
+    /**
+     * Maximum byte for SSID.
+     */
+    private static final int MAX_SSID_BYTES = 32;
+
+    /**
+     * Maximum bytes for port string in {@link #requiredProtoPortMap}.
+     */
+    private static final int MAX_PORT_STRING_BYTES = 64;
+
+    /**
+     * Integer value used for indicating null value in the Parcel.
+     */
+    private static final int NULL_VALUE = -1;
+
+    /**
+     * Minimum available downlink/uplink bandwidth (in kilobits per second) required when
+     * selecting a network from home providers.
+     *
+     * The bandwidth is calculated as the LinkSpeed * (1 – LinkLoad/255), where LinkSpeed
+     * and LinkLoad parameters are drawn from the WAN Metrics ANQP element at that hotspot.
+     *
+     * Using Long.MIN_VALUE to indicate unset value.
+     */
+    private long mMinHomeDownlinkBandwidth = Long.MIN_VALUE;
+    public void setMinHomeDownlinkBandwidth(long minHomeDownlinkBandwidth) {
+        mMinHomeDownlinkBandwidth = minHomeDownlinkBandwidth;
+    }
+    public long getMinHomeDownlinkBandwidth() {
+        return mMinHomeDownlinkBandwidth;
+    }
+    private long mMinHomeUplinkBandwidth = Long.MIN_VALUE;
+    public void setMinHomeUplinkBandwidth(long minHomeUplinkBandwidth) {
+        mMinHomeUplinkBandwidth = minHomeUplinkBandwidth;
+    }
+    public long getMinHomeUplinkBandwidth() {
+        return mMinHomeUplinkBandwidth;
+    }
+
+    /**
+     * Minimum available downlink/uplink bandwidth (in kilobits per second) required when
+     * selecting a network from roaming providers.
+     *
+     * The bandwidth is calculated as the LinkSpeed * (1 – LinkLoad/255), where LinkSpeed
+     * and LinkLoad parameters are drawn from the WAN Metrics ANQP element at that hotspot.
+     *
+     * Using Long.MIN_VALUE to indicate unset value.
+     */
+    private long mMinRoamingDownlinkBandwidth = Long.MIN_VALUE;
+    public void setMinRoamingDownlinkBandwidth(long minRoamingDownlinkBandwidth) {
+        mMinRoamingDownlinkBandwidth = minRoamingDownlinkBandwidth;
+    }
+    public long getMinRoamingDownlinkBandwidth() {
+        return mMinRoamingDownlinkBandwidth;
+    }
+    private long mMinRoamingUplinkBandwidth = Long.MIN_VALUE;
+    public void setMinRoamingUplinkBandwidth(long minRoamingUplinkBandwidth) {
+        mMinRoamingUplinkBandwidth = minRoamingUplinkBandwidth;
+    }
+    public long getMinRoamingUplinkBandwidth() {
+        return mMinRoamingUplinkBandwidth;
+    }
+
+    /**
+     * List of SSIDs that are not preferred by the Home SP.
+     */
+    private String[] mExcludedSsidList = null;
+    public void setExcludedSsidList(String[] excludedSsidList) {
+        mExcludedSsidList = excludedSsidList;
+    }
+    public String[] getExcludedSsidList() {
+        return mExcludedSsidList;
+    }
+
+    /**
+     * List of IP protocol and port number required by one or more operator supported application.
+     * The port string contained one or more port numbers delimited by ",".
+     */
+    private Map<Integer, String> mRequiredProtoPortMap = null;
+    public void setRequiredProtoPortMap(Map<Integer, String> requiredProtoPortMap) {
+        mRequiredProtoPortMap = requiredProtoPortMap;
+    }
+    public Map<Integer, String> getRequiredProtoPortMap() {
+        return mRequiredProtoPortMap;
+    }
+
+    /**
+     * This specifies the maximum acceptable BSS load policy.  This is used to prevent device
+     * from joining an AP whose channel is overly congested with traffic.
+     * Using Integer.MIN_VALUE to indicate unset value.
+     */
+    private int mMaximumBssLoadValue = Integer.MIN_VALUE;
+    public void setMaximumBssLoadValue(int maximumBssLoadValue) {
+        mMaximumBssLoadValue = maximumBssLoadValue;
+    }
+    public int getMaximumBssLoadValue() {
+        return mMaximumBssLoadValue;
+    }
+
+    /**
+     * Policy associated with a roaming provider.  This specifies a priority associated
+     * with a roaming provider for given list of countries.
+     *
+     * Contains field under PerProviderSubscription/Policy/PreferredRoamingPartnerList.
+     */
+    public static final class RoamingPartner implements Parcelable {
+        /**
+         * FQDN of the roaming partner.
+         */
+        private String mFqdn = null;
+        public void setFqdn(String fqdn) {
+            mFqdn = fqdn;
+        }
+        public String getFqdn() {
+            return mFqdn;
+        }
+
+        /**
+         * Flag indicating the exact match of FQDN is required for FQDN matching.
+         *
+         * When this flag is set to false, sub-domain matching is used.  For example, when
+         * {@link #fqdn} s set to "example.com", "host.example.com" would be a match.
+         */
+        private boolean mFqdnExactMatch = false;
+        public void setFqdnExactMatch(boolean fqdnExactMatch) {
+            mFqdnExactMatch = fqdnExactMatch;
+        }
+        public boolean getFqdnExactMatch() {
+            return mFqdnExactMatch;
+        }
+
+        /**
+         * Priority associated with this roaming partner policy.
+         * Using Integer.MIN_VALUE to indicate unset value.
+         */
+        private int mPriority = Integer.MIN_VALUE;
+        public void setPriority(int priority) {
+            mPriority = priority;
+        }
+        public int getPriority() {
+            return mPriority;
+        }
+
+        /**
+         * A string contained One or more, comma delimited (i.e., ",") ISO/IEC 3166-1 two
+         * character country strings or the country-independent value, "*".
+         */
+        private String mCountries = null;
+        public void setCountries(String countries) {
+            mCountries = countries;
+        }
+        public String getCountries() {
+            return mCountries;
+        }
+
+        public RoamingPartner() {}
+
+        public RoamingPartner(RoamingPartner source) {
+            if (source != null) {
+                mFqdn = source.mFqdn;
+                mFqdnExactMatch = source.mFqdnExactMatch;
+                mPriority = source.mPriority;
+                mCountries = source.mCountries;
+            }
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeString(mFqdn);
+            dest.writeInt(mFqdnExactMatch ? 1 : 0);
+            dest.writeInt(mPriority);
+            dest.writeString(mCountries);
+        }
+
+        @Override
+        public boolean equals(Object thatObject) {
+            if (this == thatObject) {
+                return true;
+            }
+            if (!(thatObject instanceof RoamingPartner)) {
+                return false;
+            }
+
+            RoamingPartner that = (RoamingPartner) thatObject;
+            return TextUtils.equals(mFqdn, that.mFqdn)
+                    && mFqdnExactMatch == that.mFqdnExactMatch
+                    && mPriority == that.mPriority
+                    && TextUtils.equals(mCountries, that.mCountries);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hash(mFqdn, mFqdnExactMatch, mPriority, mCountries);
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder builder = new StringBuilder();
+            builder.append("FQDN: ").append(mFqdn).append("\n");
+            builder.append("ExactMatch: ").append("mFqdnExactMatch").append("\n");
+            builder.append("Priority: ").append(mPriority).append("\n");
+            builder.append("Countries: ").append(mCountries).append("\n");
+            return builder.toString();
+        }
+
+        /**
+         * Validate RoamingParnter data.
+         *
+         * @return true on success
+         * @hide
+         */
+        public boolean validate() {
+            if (TextUtils.isEmpty(mFqdn)) {
+                Log.d(TAG, "Missing FQDN");
+                return false;
+            }
+            if (TextUtils.isEmpty(mCountries)) {
+                Log.d(TAG, "Missing countries");
+                return false;
+            }
+            return true;
+        }
+
+        public static final @android.annotation.NonNull Creator<RoamingPartner> CREATOR =
+            new Creator<RoamingPartner>() {
+                @Override
+                public RoamingPartner createFromParcel(Parcel in) {
+                    RoamingPartner roamingPartner = new RoamingPartner();
+                    roamingPartner.setFqdn(in.readString());
+                    roamingPartner.setFqdnExactMatch(in.readInt() != 0);
+                    roamingPartner.setPriority(in.readInt());
+                    roamingPartner.setCountries(in.readString());
+                    return roamingPartner;
+                }
+
+                @Override
+                public RoamingPartner[] newArray(int size) {
+                    return new RoamingPartner[size];
+                }
+            };
+    }
+    private List<RoamingPartner> mPreferredRoamingPartnerList = null;
+    public void setPreferredRoamingPartnerList(List<RoamingPartner> partnerList) {
+        mPreferredRoamingPartnerList = partnerList;
+    }
+    public List<RoamingPartner> getPreferredRoamingPartnerList() {
+        return mPreferredRoamingPartnerList;
+    }
+
+    /**
+     * Meta data used for policy update.
+     */
+    private UpdateParameter mPolicyUpdate = null;
+    public void setPolicyUpdate(UpdateParameter policyUpdate) {
+        mPolicyUpdate = policyUpdate;
+    }
+    public UpdateParameter getPolicyUpdate() {
+        return mPolicyUpdate;
+    }
+
+    /**
+     * Constructor for creating Policy with default values.
+     */
+    public Policy() {}
+
+    /**
+     * Copy constructor.
+     *
+     * @param source The source to copy from
+     */
+    public Policy(Policy source) {
+        if (source == null) {
+            return;
+        }
+        mMinHomeDownlinkBandwidth = source.mMinHomeDownlinkBandwidth;
+        mMinHomeUplinkBandwidth = source.mMinHomeUplinkBandwidth;
+        mMinRoamingDownlinkBandwidth = source.mMinRoamingDownlinkBandwidth;
+        mMinRoamingUplinkBandwidth = source.mMinRoamingUplinkBandwidth;
+        mMaximumBssLoadValue = source.mMaximumBssLoadValue;
+        if (source.mExcludedSsidList != null) {
+            mExcludedSsidList = Arrays.copyOf(source.mExcludedSsidList,
+                    source.mExcludedSsidList.length);
+        }
+        if (source.mRequiredProtoPortMap != null) {
+            mRequiredProtoPortMap = Collections.unmodifiableMap(source.mRequiredProtoPortMap);
+        }
+        if (source.mPreferredRoamingPartnerList != null) {
+            mPreferredRoamingPartnerList = Collections.unmodifiableList(
+                    source.mPreferredRoamingPartnerList);
+        }
+        if (source.mPolicyUpdate != null) {
+            mPolicyUpdate = new UpdateParameter(source.mPolicyUpdate);
+        }
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeLong(mMinHomeDownlinkBandwidth);
+        dest.writeLong(mMinHomeUplinkBandwidth);
+        dest.writeLong(mMinRoamingDownlinkBandwidth);
+        dest.writeLong(mMinRoamingUplinkBandwidth);
+        dest.writeStringArray(mExcludedSsidList);
+        writeProtoPortMap(dest, mRequiredProtoPortMap);
+        dest.writeInt(mMaximumBssLoadValue);
+        writeRoamingPartnerList(dest, flags, mPreferredRoamingPartnerList);
+        dest.writeParcelable(mPolicyUpdate, flags);
+    }
+
+    @Override
+    public boolean equals(Object thatObject) {
+        if (this == thatObject) {
+            return true;
+        }
+        if (!(thatObject instanceof Policy)) {
+            return false;
+        }
+        Policy that = (Policy) thatObject;
+
+        return mMinHomeDownlinkBandwidth == that.mMinHomeDownlinkBandwidth
+                && mMinHomeUplinkBandwidth == that.mMinHomeUplinkBandwidth
+                && mMinRoamingDownlinkBandwidth == that.mMinRoamingDownlinkBandwidth
+                && mMinRoamingUplinkBandwidth == that.mMinRoamingUplinkBandwidth
+                && Arrays.equals(mExcludedSsidList, that.mExcludedSsidList)
+                && (mRequiredProtoPortMap == null ? that.mRequiredProtoPortMap == null
+                        : mRequiredProtoPortMap.equals(that.mRequiredProtoPortMap))
+                && mMaximumBssLoadValue == that.mMaximumBssLoadValue
+                && (mPreferredRoamingPartnerList == null
+                        ? that.mPreferredRoamingPartnerList == null
+                        : mPreferredRoamingPartnerList.equals(that.mPreferredRoamingPartnerList))
+                && (mPolicyUpdate == null ? that.mPolicyUpdate == null
+                        : mPolicyUpdate.equals(that.mPolicyUpdate));
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mMinHomeDownlinkBandwidth, mMinHomeUplinkBandwidth,
+                mMinRoamingDownlinkBandwidth, mMinRoamingUplinkBandwidth, mExcludedSsidList,
+                mRequiredProtoPortMap, mMaximumBssLoadValue, mPreferredRoamingPartnerList,
+                mPolicyUpdate);
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append("MinHomeDownlinkBandwidth: ").append(mMinHomeDownlinkBandwidth)
+                .append("\n");
+        builder.append("MinHomeUplinkBandwidth: ").append(mMinHomeUplinkBandwidth).append("\n");
+        builder.append("MinRoamingDownlinkBandwidth: ").append(mMinRoamingDownlinkBandwidth)
+                .append("\n");
+        builder.append("MinRoamingUplinkBandwidth: ").append(mMinRoamingUplinkBandwidth)
+                .append("\n");
+        builder.append("ExcludedSSIDList: ").append(mExcludedSsidList).append("\n");
+        builder.append("RequiredProtoPortMap: ").append(mRequiredProtoPortMap).append("\n");
+        builder.append("MaximumBSSLoadValue: ").append(mMaximumBssLoadValue).append("\n");
+        builder.append("PreferredRoamingPartnerList: ").append(mPreferredRoamingPartnerList)
+                .append("\n");
+        if (mPolicyUpdate != null) {
+            builder.append("PolicyUpdate Begin ---\n");
+            builder.append(mPolicyUpdate);
+            builder.append("PolicyUpdate End ---\n");
+        }
+        return builder.toString();
+    }
+
+    /**
+     * Validate Policy data.
+     *
+     * @return true on success
+     * @hide
+     */
+    public boolean validate() {
+        if (mPolicyUpdate == null) {
+            Log.d(TAG, "PolicyUpdate not specified");
+            return false;
+        }
+        if (!mPolicyUpdate.validate()) {
+            return false;
+        }
+
+        // Validate SSID exclusion list.
+        if (mExcludedSsidList != null) {
+            if (mExcludedSsidList.length > MAX_EXCLUSION_SSIDS) {
+                Log.d(TAG, "SSID exclusion list size exceeded the max: "
+                        + mExcludedSsidList.length);
+                return false;
+            }
+            for (String ssid : mExcludedSsidList) {
+                if (ssid.getBytes(StandardCharsets.UTF_8).length > MAX_SSID_BYTES) {
+                    Log.d(TAG, "Invalid SSID: " + ssid);
+                    return false;
+                }
+            }
+        }
+        // Validate required protocol to port map.
+        if (mRequiredProtoPortMap != null) {
+            for (Map.Entry<Integer, String> entry : mRequiredProtoPortMap.entrySet()) {
+                String portNumber = entry.getValue();
+                if (portNumber.getBytes(StandardCharsets.UTF_8).length > MAX_PORT_STRING_BYTES) {
+                    Log.d(TAG, "PortNumber string bytes exceeded the max: " + portNumber);
+                    return false;
+                }
+            }
+        }
+        // Validate preferred roaming partner list.
+        if (mPreferredRoamingPartnerList != null) {
+            for (RoamingPartner partner : mPreferredRoamingPartnerList) {
+                if (!partner.validate()) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    public static final @android.annotation.NonNull Creator<Policy> CREATOR =
+        new Creator<Policy>() {
+            @Override
+            public Policy createFromParcel(Parcel in) {
+                Policy policy = new Policy();
+                policy.setMinHomeDownlinkBandwidth(in.readLong());
+                policy.setMinHomeUplinkBandwidth(in.readLong());
+                policy.setMinRoamingDownlinkBandwidth(in.readLong());
+                policy.setMinRoamingUplinkBandwidth(in.readLong());
+                policy.setExcludedSsidList(in.createStringArray());
+                policy.setRequiredProtoPortMap(readProtoPortMap(in));
+                policy.setMaximumBssLoadValue(in.readInt());
+                policy.setPreferredRoamingPartnerList(readRoamingPartnerList(in));
+                policy.setPolicyUpdate(in.readParcelable(null));
+                return policy;
+            }
+
+            @Override
+            public Policy[] newArray(int size) {
+                return new Policy[size];
+            }
+
+            /**
+             * Helper function for reading IP Protocol to Port Number map from a Parcel.
+             *
+             * @param in The Parcel to read from
+             * @return Map of IP protocol to port number
+             */
+            private Map<Integer, String> readProtoPortMap(Parcel in) {
+                int size = in.readInt();
+                if (size == NULL_VALUE) {
+                    return null;
+                }
+                Map<Integer, String> protoPortMap = new HashMap<>(size);
+                for (int i = 0; i < size; i++) {
+                    int key = in.readInt();
+                    String value = in.readString();
+                    protoPortMap.put(key, value);
+                }
+                return protoPortMap;
+            }
+
+            /**
+             * Helper function for reading roaming partner list from a Parcel.
+             *
+             * @param in The Parcel to read from
+             * @return List of roaming partners
+             */
+            private List<RoamingPartner> readRoamingPartnerList(Parcel in) {
+                int size = in.readInt();
+                if (size == NULL_VALUE) {
+                    return null;
+                }
+                List<RoamingPartner> partnerList = new ArrayList<>();
+                for (int i = 0; i < size; i++) {
+                    partnerList.add(in.readParcelable(null));
+                }
+                return partnerList;
+            }
+
+        };
+
+    /**
+     * Helper function for writing IP Protocol to Port Number map to a Parcel.
+     *
+     * @param dest The Parcel to write to
+     * @param protoPortMap The map to write
+     */
+    private static void writeProtoPortMap(Parcel dest, Map<Integer, String> protoPortMap) {
+        if (protoPortMap == null) {
+            dest.writeInt(NULL_VALUE);
+            return;
+        }
+        dest.writeInt(protoPortMap.size());
+        for (Map.Entry<Integer, String> entry : protoPortMap.entrySet()) {
+            dest.writeInt(entry.getKey());
+            dest.writeString(entry.getValue());
+        }
+    }
+
+    /**
+     * Helper function for writing roaming partner list to a Parcel.
+     *
+     * @param dest The Parcel to write to
+     * @param flags The flag about how the object should be written
+     * @param partnerList The partner list to write
+     */
+    private static void writeRoamingPartnerList(Parcel dest, int flags,
+            List<RoamingPartner> partnerList) {
+        if (partnerList == null) {
+            dest.writeInt(NULL_VALUE);
+            return;
+        }
+        dest.writeInt(partnerList.size());
+        for (RoamingPartner partner : partnerList) {
+            dest.writeParcelable(partner, flags);
+        }
+    }
+}
diff --git a/android/net/wifi/hotspot2/pps/UpdateParameter.java b/android/net/wifi/hotspot2/pps/UpdateParameter.java
new file mode 100644
index 0000000..4a8aa36
--- /dev/null
+++ b/android/net/wifi/hotspot2/pps/UpdateParameter.java
@@ -0,0 +1,403 @@
+/**
+ * Copyright (c) 2017, The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.hotspot2.pps;
+
+import android.net.wifi.ParcelUtil;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.Base64;
+import android.util.Log;
+
+import java.nio.charset.StandardCharsets;
+import java.security.cert.X509Certificate;
+import java.util.Arrays;
+import java.util.Objects;
+
+/**
+ * Class representing configuration parameters for subscription or policy update in
+ * PerProviderSubscription (PPS) Management Object (MO) tree.  This is used by both
+ * PerProviderSubscription/Policy/PolicyUpdate and PerProviderSubscription/SubscriptionUpdate
+ * subtree.
+ *
+ * For more info, refer to Hotspot 2.0 PPS MO defined in section 9.1 of the Hotspot 2.0
+ * Release 2 Technical Specification.
+ *
+ * @hide
+ */
+public final class UpdateParameter implements Parcelable {
+    private static final String TAG = "UpdateParameter";
+
+    /**
+     * Value indicating policy update is not applicable.  Thus, never check with policy server
+     * for updates.
+     */
+    public static final long UPDATE_CHECK_INTERVAL_NEVER = 0xFFFFFFFFL;
+
+    /**
+     * Valid string for UpdateMethod.
+     */
+    public static final String UPDATE_METHOD_OMADM = "OMA-DM-ClientInitiated";
+    public static final String UPDATE_METHOD_SSP = "SSP-ClientInitiated";
+
+    /**
+     * Valid string for Restriction.
+     */
+    public static final String UPDATE_RESTRICTION_HOMESP = "HomeSP";
+    public static final String UPDATE_RESTRICTION_ROAMING_PARTNER = "RoamingPartner";
+    public static final String UPDATE_RESTRICTION_UNRESTRICTED = "Unrestricted";
+
+    /**
+     * Maximum bytes for URI string.
+     */
+    private static final int MAX_URI_BYTES = 1023;
+
+    /**
+     * Maximum bytes for URI string.
+     */
+    private static final int MAX_URL_BYTES = 1023;
+
+    /**
+     * Maximum bytes for username.
+     */
+    private static final int MAX_USERNAME_BYTES = 63;
+
+    /**
+     * Maximum bytes for password.
+     */
+    private static final int MAX_PASSWORD_BYTES = 255;
+
+    /**
+     * Number of bytes for certificate SHA-256 fingerprint byte array.
+     */
+    private static final int CERTIFICATE_SHA256_BYTES = 32;
+
+    /**
+     * This specifies how often the mobile device shall check with policy server for updates.
+     *
+     * Using Long.MIN_VALUE to indicate unset value.
+     */
+    private long mUpdateIntervalInMinutes = Long.MIN_VALUE;
+    public void setUpdateIntervalInMinutes(long updateIntervalInMinutes) {
+        mUpdateIntervalInMinutes = updateIntervalInMinutes;
+    }
+    public long getUpdateIntervalInMinutes() {
+        return mUpdateIntervalInMinutes;
+    }
+
+    /**
+     * The method used to update the policy.  Permitted values are "OMA-DM-ClientInitiated"
+     * and "SPP-ClientInitiated".
+     */
+    private String mUpdateMethod = null;
+    public void setUpdateMethod(String updateMethod) {
+        mUpdateMethod = updateMethod;
+    }
+    public String getUpdateMethod() {
+        return mUpdateMethod;
+    }
+
+    /**
+     * This specifies the hotspots at which the subscription update is permitted.  Permitted
+     * values are "HomeSP", "RoamingPartner", or "Unrestricted";
+     */
+    private String mRestriction = null;
+    public void setRestriction(String restriction) {
+        mRestriction = restriction;
+    }
+    public String getRestriction() {
+        return mRestriction;
+    }
+
+    /**
+     * The URI of the update server.
+     */
+    private String mServerUri = null;
+    public void setServerUri(String serverUri) {
+        mServerUri = serverUri;
+    }
+    public String getServerUri() {
+        return mServerUri;
+    }
+
+    /**
+     * Username used to authenticate with the policy server.
+     */
+    private String mUsername = null;
+    public void setUsername(String username) {
+        mUsername = username;
+    }
+    public String getUsername() {
+        return mUsername;
+    }
+
+    /**
+     * Base64 encoded password used to authenticate with the policy server.
+     */
+    private String mBase64EncodedPassword = null;
+    public void setBase64EncodedPassword(String password) {
+        mBase64EncodedPassword = password;
+    }
+    public String getBase64EncodedPassword() {
+        return mBase64EncodedPassword;
+    }
+
+    /**
+     * HTTPS URL for retrieving certificate for trust root.  The trust root is used to validate
+     * policy server's identity.
+     */
+    private String mTrustRootCertUrl = null;
+    public void setTrustRootCertUrl(String trustRootCertUrl) {
+        mTrustRootCertUrl = trustRootCertUrl;
+    }
+    public String getTrustRootCertUrl() {
+        return mTrustRootCertUrl;
+    }
+
+    /**
+     * SHA-256 fingerprint of the certificate located at {@code mTrustRootCertUrl}
+     */
+    private byte[] mTrustRootCertSha256Fingerprint = null;
+    public void setTrustRootCertSha256Fingerprint(byte[] fingerprint) {
+        mTrustRootCertSha256Fingerprint = fingerprint;
+    }
+    public byte[] getTrustRootCertSha256Fingerprint() {
+        return mTrustRootCertSha256Fingerprint;
+    }
+
+    /**
+     * CA (Certificate Authority) X509 certificates.
+     */
+    private X509Certificate mCaCertificate;
+
+    /**
+     * Set the CA (Certification Authority) certificate associated with Policy/Subscription update.
+     *
+     * @param caCertificate The CA certificate to set
+     * @hide
+     */
+    public void setCaCertificate(X509Certificate caCertificate) {
+        mCaCertificate = caCertificate;
+    }
+
+    /**
+     * Get the CA (Certification Authority) certificate associated with Policy/Subscription update.
+     *
+     * @return CA certificate associated and {@code null} if certificate is not set.
+     * @hide
+     */
+    public X509Certificate getCaCertificate() {
+        return mCaCertificate;
+    }
+
+    /**
+     * Constructor for creating Policy with default values.
+     */
+    public UpdateParameter() {}
+
+    /**
+     * Copy constructor.
+     *
+     * @param source The source to copy from
+     */
+    public UpdateParameter(UpdateParameter source) {
+        if (source == null) {
+            return;
+        }
+        mUpdateIntervalInMinutes = source.mUpdateIntervalInMinutes;
+        mUpdateMethod = source.mUpdateMethod;
+        mRestriction = source.mRestriction;
+        mServerUri = source.mServerUri;
+        mUsername = source.mUsername;
+        mBase64EncodedPassword = source.mBase64EncodedPassword;
+        mTrustRootCertUrl = source.mTrustRootCertUrl;
+        if (source.mTrustRootCertSha256Fingerprint != null) {
+            mTrustRootCertSha256Fingerprint = Arrays.copyOf(source.mTrustRootCertSha256Fingerprint,
+                    source.mTrustRootCertSha256Fingerprint.length);
+        }
+        mCaCertificate = source.mCaCertificate;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeLong(mUpdateIntervalInMinutes);
+        dest.writeString(mUpdateMethod);
+        dest.writeString(mRestriction);
+        dest.writeString(mServerUri);
+        dest.writeString(mUsername);
+        dest.writeString(mBase64EncodedPassword);
+        dest.writeString(mTrustRootCertUrl);
+        dest.writeByteArray(mTrustRootCertSha256Fingerprint);
+        ParcelUtil.writeCertificate(dest, mCaCertificate);
+    }
+
+    @Override
+    public boolean equals(Object thatObject) {
+        if (this == thatObject) {
+            return true;
+        }
+        if (!(thatObject instanceof UpdateParameter)) {
+            return false;
+        }
+        UpdateParameter that = (UpdateParameter) thatObject;
+
+        return mUpdateIntervalInMinutes == that.mUpdateIntervalInMinutes
+                && TextUtils.equals(mUpdateMethod, that.mUpdateMethod)
+                && TextUtils.equals(mRestriction, that.mRestriction)
+                && TextUtils.equals(mServerUri, that.mServerUri)
+                && TextUtils.equals(mUsername, that.mUsername)
+                && TextUtils.equals(mBase64EncodedPassword, that.mBase64EncodedPassword)
+                && TextUtils.equals(mTrustRootCertUrl, that.mTrustRootCertUrl)
+                && Arrays.equals(mTrustRootCertSha256Fingerprint,
+                that.mTrustRootCertSha256Fingerprint)
+                && Credential.isX509CertificateEquals(mCaCertificate, that.mCaCertificate);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mUpdateIntervalInMinutes, mUpdateMethod, mRestriction, mServerUri,
+                mUsername, mBase64EncodedPassword, mTrustRootCertUrl,
+                Arrays.hashCode(mTrustRootCertSha256Fingerprint), mCaCertificate);
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append("UpdateInterval: ").append(mUpdateIntervalInMinutes).append("\n");
+        builder.append("UpdateMethod: ").append(mUpdateMethod).append("\n");
+        builder.append("Restriction: ").append(mRestriction).append("\n");
+        builder.append("ServerURI: ").append(mServerUri).append("\n");
+        builder.append("Username: ").append(mUsername).append("\n");
+        builder.append("TrustRootCertURL: ").append(mTrustRootCertUrl).append("\n");
+        return builder.toString();
+    }
+
+    /**
+     * Validate UpdateParameter data.
+     *
+     * @return true on success
+     * @hide
+     */
+    public boolean validate() {
+        if (mUpdateIntervalInMinutes == Long.MIN_VALUE) {
+            Log.d(TAG, "Update interval not specified");
+            return false;
+        }
+        // Update not applicable.
+        if (mUpdateIntervalInMinutes == UPDATE_CHECK_INTERVAL_NEVER) {
+            return true;
+        }
+
+        if (!TextUtils.equals(mUpdateMethod, UPDATE_METHOD_OMADM)
+                && !TextUtils.equals(mUpdateMethod, UPDATE_METHOD_SSP)) {
+            Log.d(TAG, "Unknown update method: " + mUpdateMethod);
+            return false;
+        }
+
+        if (!TextUtils.equals(mRestriction, UPDATE_RESTRICTION_HOMESP)
+                && !TextUtils.equals(mRestriction, UPDATE_RESTRICTION_ROAMING_PARTNER)
+                && !TextUtils.equals(mRestriction, UPDATE_RESTRICTION_UNRESTRICTED)) {
+            Log.d(TAG, "Unknown restriction: " + mRestriction);
+            return false;
+        }
+
+        if (TextUtils.isEmpty(mServerUri)) {
+            Log.d(TAG, "Missing update server URI");
+            return false;
+        }
+        if (mServerUri.getBytes(StandardCharsets.UTF_8).length > MAX_URI_BYTES) {
+            Log.d(TAG, "URI bytes exceeded the max: "
+                    + mServerUri.getBytes(StandardCharsets.UTF_8).length);
+            return false;
+        }
+
+        if (TextUtils.isEmpty(mUsername)) {
+            Log.d(TAG, "Missing username");
+            return false;
+        }
+        if (mUsername.getBytes(StandardCharsets.UTF_8).length > MAX_USERNAME_BYTES) {
+            Log.d(TAG, "Username bytes exceeded the max: "
+                    + mUsername.getBytes(StandardCharsets.UTF_8).length);
+            return false;
+        }
+
+        if (TextUtils.isEmpty(mBase64EncodedPassword)) {
+            Log.d(TAG, "Missing username");
+            return false;
+        }
+        if (mBase64EncodedPassword.getBytes(StandardCharsets.UTF_8).length > MAX_PASSWORD_BYTES) {
+            Log.d(TAG, "Password bytes exceeded the max: "
+                    + mBase64EncodedPassword.getBytes(StandardCharsets.UTF_8).length);
+            return false;
+        }
+        try {
+            Base64.decode(mBase64EncodedPassword, Base64.DEFAULT);
+        } catch (IllegalArgumentException e) {
+            Log.d(TAG, "Invalid encoding for password: " + mBase64EncodedPassword);
+            return false;
+        }
+
+        if (TextUtils.isEmpty(mTrustRootCertUrl)) {
+            Log.d(TAG, "Missing trust root certificate URL");
+            return false;
+        }
+        if (mTrustRootCertUrl.getBytes(StandardCharsets.UTF_8).length > MAX_URL_BYTES) {
+            Log.d(TAG, "Trust root cert URL bytes exceeded the max: "
+                    + mTrustRootCertUrl.getBytes(StandardCharsets.UTF_8).length);
+            return false;
+        }
+
+        if (mTrustRootCertSha256Fingerprint == null) {
+            Log.d(TAG, "Missing trust root certificate SHA-256 fingerprint");
+            return false;
+        }
+        if (mTrustRootCertSha256Fingerprint.length != CERTIFICATE_SHA256_BYTES) {
+            Log.d(TAG, "Incorrect size of trust root certificate SHA-256 fingerprint: "
+                    + mTrustRootCertSha256Fingerprint.length);
+            return false;
+        }
+        return true;
+    }
+
+    public static final @android.annotation.NonNull Creator<UpdateParameter> CREATOR =
+        new Creator<UpdateParameter>() {
+            @Override
+            public UpdateParameter createFromParcel(Parcel in) {
+                UpdateParameter updateParam = new UpdateParameter();
+                updateParam.setUpdateIntervalInMinutes(in.readLong());
+                updateParam.setUpdateMethod(in.readString());
+                updateParam.setRestriction(in.readString());
+                updateParam.setServerUri(in.readString());
+                updateParam.setUsername(in.readString());
+                updateParam.setBase64EncodedPassword(in.readString());
+                updateParam.setTrustRootCertUrl(in.readString());
+                updateParam.setTrustRootCertSha256Fingerprint(in.createByteArray());
+                updateParam.setCaCertificate(ParcelUtil.readCertificate(in));
+                return updateParam;
+            }
+
+            @Override
+            public UpdateParameter[] newArray(int size) {
+                return new UpdateParameter[size];
+            }
+        };
+}
diff --git a/android/net/wifi/p2p/WifiP2pConfig.java b/android/net/wifi/p2p/WifiP2pConfig.java
new file mode 100644
index 0000000..c3cfb02
--- /dev/null
+++ b/android/net/wifi/p2p/WifiP2pConfig.java
@@ -0,0 +1,453 @@
+/*
+ * Copyright (C) 2011 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.wifi.p2p;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UnsupportedAppUsage;
+import android.net.MacAddress;
+import android.net.wifi.WpsInfo;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.regex.PatternSyntaxException;
+
+/**
+ * A class representing a Wi-Fi P2p configuration for setting up a connection
+ *
+ * {@see WifiP2pManager}
+ */
+public class WifiP2pConfig implements Parcelable {
+
+    /**
+     * The device MAC address uniquely identifies a Wi-Fi p2p device
+     */
+    public String deviceAddress = "";
+
+    /**
+     * Wi-Fi Protected Setup information
+     */
+    public WpsInfo wps;
+
+    /**
+     * The network name of a group, should be configured by helper method
+     */
+    /** @hide */
+    public String networkName = "";
+
+    /**
+     * The passphrase of a group, should be configured by helper method
+     */
+    /** @hide */
+    public String passphrase = "";
+
+    /**
+     * The required band for Group Owner
+     */
+    /** @hide */
+    public int groupOwnerBand = GROUP_OWNER_BAND_AUTO;
+
+    /** @hide */
+    public static final int MAX_GROUP_OWNER_INTENT   =   15;
+    /** @hide */
+    @UnsupportedAppUsage
+    public static final int MIN_GROUP_OWNER_INTENT   =   0;
+
+    /** @hide */
+    @IntDef(flag = false, prefix = { "GROUP_OWNER_BAND_" }, value = {
+        GROUP_OWNER_BAND_AUTO,
+        GROUP_OWNER_BAND_2GHZ,
+        GROUP_OWNER_BAND_5GHZ
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface GroupOperatingBandType {}
+
+    /**
+     * Allow the system to pick the operating frequency from all supported bands.
+     */
+    public static final int GROUP_OWNER_BAND_AUTO = 0;
+    /**
+     * Allow the system to pick the operating frequency from the 2.4 GHz band.
+     */
+    public static final int GROUP_OWNER_BAND_2GHZ = 1;
+    /**
+     * Allow the system to pick the operating frequency from the 5 GHz band.
+     */
+    public static final int GROUP_OWNER_BAND_5GHZ = 2;
+
+    /**
+     * This is an integer value between 0 and 15 where 0 indicates the least
+     * inclination to be a group owner and 15 indicates the highest inclination
+     * to be a group owner.
+     *
+     * A value of -1 indicates the system can choose an appropriate value.
+     */
+    public int groupOwnerIntent = -1;
+
+    /** @hide */
+    @UnsupportedAppUsage
+    public int netId = WifiP2pGroup.PERSISTENT_NET_ID;
+
+    public WifiP2pConfig() {
+        //set defaults
+        wps = new WpsInfo();
+        wps.setup = WpsInfo.PBC;
+    }
+
+    /** @hide */
+    public void invalidate() {
+        deviceAddress = "";
+    }
+
+    /** P2P-GO-NEG-REQUEST 42:fc:89:a8:96:09 dev_passwd_id=4 {@hide}*/
+    @UnsupportedAppUsage
+    public WifiP2pConfig(String supplicantEvent) throws IllegalArgumentException {
+        String[] tokens = supplicantEvent.split(" ");
+
+        if (tokens.length < 2 || !tokens[0].equals("P2P-GO-NEG-REQUEST")) {
+            throw new IllegalArgumentException("Malformed supplicant event");
+        }
+
+        deviceAddress = tokens[1];
+        wps = new WpsInfo();
+
+        if (tokens.length > 2) {
+            String[] nameVal = tokens[2].split("=");
+            int devPasswdId;
+            try {
+                devPasswdId = Integer.parseInt(nameVal[1]);
+            } catch (NumberFormatException e) {
+                devPasswdId = 0;
+            }
+            //Based on definitions in wps/wps_defs.h
+            switch (devPasswdId) {
+                //DEV_PW_USER_SPECIFIED = 0x0001,
+                case 0x01:
+                    wps.setup = WpsInfo.DISPLAY;
+                    break;
+                //DEV_PW_PUSHBUTTON = 0x0004,
+                case 0x04:
+                    wps.setup = WpsInfo.PBC;
+                    break;
+                //DEV_PW_REGISTRAR_SPECIFIED = 0x0005
+                case 0x05:
+                    wps.setup = WpsInfo.KEYPAD;
+                    break;
+                default:
+                    wps.setup = WpsInfo.PBC;
+                    break;
+            }
+        }
+    }
+
+    public String toString() {
+        StringBuffer sbuf = new StringBuffer();
+        sbuf.append("\n address: ").append(deviceAddress);
+        sbuf.append("\n wps: ").append(wps);
+        sbuf.append("\n groupOwnerIntent: ").append(groupOwnerIntent);
+        sbuf.append("\n persist: ").append(netId);
+        sbuf.append("\n networkName: ").append(networkName);
+        sbuf.append("\n passphrase: ").append(
+                TextUtils.isEmpty(passphrase) ? "<empty>" : "<non-empty>");
+        sbuf.append("\n groupOwnerBand: ").append(groupOwnerBand);
+        return sbuf.toString();
+    }
+
+    /** Implement the Parcelable interface */
+    public int describeContents() {
+        return 0;
+    }
+
+    /** copy constructor */
+    public WifiP2pConfig(WifiP2pConfig source) {
+        if (source != null) {
+            deviceAddress = source.deviceAddress;
+            wps = new WpsInfo(source.wps);
+            groupOwnerIntent = source.groupOwnerIntent;
+            netId = source.netId;
+            networkName = source.networkName;
+            passphrase = source.passphrase;
+            groupOwnerBand = source.groupOwnerBand;
+        }
+    }
+
+    /** Implement the Parcelable interface */
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeString(deviceAddress);
+        dest.writeParcelable(wps, flags);
+        dest.writeInt(groupOwnerIntent);
+        dest.writeInt(netId);
+        dest.writeString(networkName);
+        dest.writeString(passphrase);
+        dest.writeInt(groupOwnerBand);
+    }
+
+    /** Implement the Parcelable interface */
+    public static final @android.annotation.NonNull Creator<WifiP2pConfig> CREATOR =
+        new Creator<WifiP2pConfig>() {
+            public WifiP2pConfig createFromParcel(Parcel in) {
+                WifiP2pConfig config = new WifiP2pConfig();
+                config.deviceAddress = in.readString();
+                config.wps = (WpsInfo) in.readParcelable(null);
+                config.groupOwnerIntent = in.readInt();
+                config.netId = in.readInt();
+                config.networkName = in.readString();
+                config.passphrase = in.readString();
+                config.groupOwnerBand = in.readInt();
+                return config;
+            }
+
+            public WifiP2pConfig[] newArray(int size) {
+                return new WifiP2pConfig[size];
+            }
+        };
+
+    /**
+     * Builder used to build {@link WifiP2pConfig} objects for
+     * creating or joining a group.
+     */
+    public static final class Builder {
+
+        private static final MacAddress MAC_ANY_ADDRESS =
+                MacAddress.fromString("02:00:00:00:00:00");
+
+        private MacAddress mDeviceAddress = MAC_ANY_ADDRESS;
+        private String mNetworkName = "";
+        private String mPassphrase = "";
+        private int mGroupOperatingBand = GROUP_OWNER_BAND_AUTO;
+        private int mGroupOperatingFrequency = GROUP_OWNER_BAND_AUTO;
+        private int mNetId = WifiP2pGroup.TEMPORARY_NET_ID;
+
+        /**
+         * Specify the peer's MAC address. If not set, the device will
+         * try to find a peer whose SSID matches the network name as
+         * specified by {@link #setNetworkName(String)}. Specifying null will
+         * reset the peer's MAC address to "02:00:00:00:00:00".
+         * <p>
+         *     Optional. "02:00:00:00:00:00" by default.
+         *
+         * @param deviceAddress the peer's MAC address.
+         * @return The builder to facilitate chaining
+         *         {@code builder.setXXX(..).setXXX(..)}.
+         */
+        public @NonNull Builder setDeviceAddress(@Nullable MacAddress deviceAddress) {
+            if (deviceAddress == null) {
+                mDeviceAddress = MAC_ANY_ADDRESS;
+            } else {
+                mDeviceAddress = deviceAddress;
+            }
+            return this;
+        }
+
+        /**
+         * Specify the network name, a.k.a. group name,
+         * for creating or joining a group.
+         * <p>
+         * A network name shall begin with "DIRECT-xy". x and y are selected
+         * from the following character set: upper case letters, lower case
+         * letters and numbers. Any byte values allowed for an SSID according to
+         * IEEE802.11-2012 [1] may be included after the string "DIRECT-xy"
+         * (including none).
+         * <p>
+         *     Must be called - an empty network name or an network name
+         *     not conforming to the P2P Group ID naming rule is not valid.
+         *
+         * @param networkName network name of a group.
+         * @return The builder to facilitate chaining
+         *         {@code builder.setXXX(..).setXXX(..)}.
+         */
+        public @NonNull Builder setNetworkName(@NonNull String networkName) {
+            if (TextUtils.isEmpty(networkName)) {
+                throw new IllegalArgumentException(
+                        "network name must be non-empty.");
+            }
+            try {
+                if (!networkName.matches("^DIRECT-[a-zA-Z0-9]{2}.*")) {
+                    throw new IllegalArgumentException(
+                            "network name must starts with the prefix DIRECT-xy.");
+                }
+            } catch (PatternSyntaxException e) {
+                // can never happen (fixed pattern)
+            }
+            mNetworkName = networkName;
+            return this;
+        }
+
+        /**
+         * Specify the passphrase for creating or joining a group.
+         * <p>
+         * The passphrase must be an ASCII string whose length is between 8
+         * and 63.
+         * <p>
+         *     Must be called - an empty passphrase is not valid.
+         *
+         * @param passphrase the passphrase of a group.
+         * @return The builder to facilitate chaining
+         *         {@code builder.setXXX(..).setXXX(..)}.
+         */
+        public @NonNull Builder setPassphrase(@NonNull String passphrase) {
+            if (TextUtils.isEmpty(passphrase)) {
+                throw new IllegalArgumentException(
+                        "passphrase must be non-empty.");
+            }
+            if (passphrase.length() < 8 || passphrase.length() > 63) {
+                throw new IllegalArgumentException(
+                        "The length of a passphrase must be between 8 and 63.");
+            }
+            mPassphrase = passphrase;
+            return this;
+        }
+
+        /**
+         * Specify the band to use for creating the group or joining the group. The band should
+         * be {@link #GROUP_OWNER_BAND_2GHZ}, {@link #GROUP_OWNER_BAND_5GHZ} or
+         * {@link #GROUP_OWNER_BAND_AUTO}.
+         * <p>
+         * When creating a group as Group Owner using {@link
+         * WifiP2pManager#createGroup(WifiP2pManager.Channel,
+         * WifiP2pConfig, WifiP2pManager.ActionListener)},
+         * specifying {@link #GROUP_OWNER_BAND_AUTO} allows the system to pick the operating
+         * frequency from all supported bands.
+         * Specifying {@link #GROUP_OWNER_BAND_2GHZ} or {@link #GROUP_OWNER_BAND_5GHZ}
+         * only allows the system to pick the operating frequency in the specified band.
+         * If the Group Owner cannot create a group in the specified band, the operation will fail.
+         * <p>
+         * When joining a group as Group Client using {@link
+         * WifiP2pManager#connect(WifiP2pManager.Channel, WifiP2pConfig,
+         * WifiP2pManager.ActionListener)},
+         * specifying {@link #GROUP_OWNER_BAND_AUTO} allows the system to scan all supported
+         * frequencies to find the desired group. Specifying {@link #GROUP_OWNER_BAND_2GHZ} or
+         * {@link #GROUP_OWNER_BAND_5GHZ} only allows the system to scan the specified band.
+         * <p>
+         *     {@link #setGroupOperatingBand(int)} and {@link #setGroupOperatingFrequency(int)} are
+         *     mutually exclusive. Setting operating band and frequency both is invalid.
+         * <p>
+         *     Optional. {@link #GROUP_OWNER_BAND_AUTO} by default.
+         *
+         * @param band the operating band of the group.
+         *             This should be one of {@link #GROUP_OWNER_BAND_AUTO},
+         *             {@link #GROUP_OWNER_BAND_2GHZ}, {@link #GROUP_OWNER_BAND_5GHZ}.
+         * @return The builder to facilitate chaining
+         *         {@code builder.setXXX(..).setXXX(..)}.
+         */
+        public @NonNull Builder setGroupOperatingBand(@GroupOperatingBandType int band) {
+            switch (band) {
+                case GROUP_OWNER_BAND_AUTO:
+                case GROUP_OWNER_BAND_2GHZ:
+                case GROUP_OWNER_BAND_5GHZ:
+                    mGroupOperatingBand = band;
+                    break;
+                default:
+                    throw new IllegalArgumentException(
+                        "Invalid constant for the group operating band!");
+            }
+            return this;
+        }
+
+        /**
+         * Specify the frequency, in MHz, to use for creating the group or joining the group.
+         * <p>
+         * When creating a group as Group Owner using {@link WifiP2pManager#createGroup(
+         * WifiP2pManager.Channel, WifiP2pConfig, WifiP2pManager.ActionListener)},
+         * specifying a frequency only allows the system to pick the specified frequency.
+         * If the Group Owner cannot create a group at the specified frequency,
+         * the operation will fail.
+         * When not specifying a frequency, it allows the system to pick operating frequency
+         * from all supported bands.
+         * <p>
+         * When joining a group as Group Client using {@link WifiP2pManager#connect(
+         * WifiP2pManager.Channel, WifiP2pConfig, WifiP2pManager.ActionListener)},
+         * specifying a frequency only allows the system to scan the specified frequency.
+         * If the frequency is not supported or invalid, the operation will fail.
+         * When not specifying a frequency, it allows the system to scan all supported
+         * frequencies to find the desired group.
+         * <p>
+         *     {@link #setGroupOperatingBand(int)} and {@link #setGroupOperatingFrequency(int)} are
+         *     mutually exclusive. Setting operating band and frequency both is invalid.
+         * <p>
+         *     Optional. 0 by default.
+         *
+         * @param frequency the operating frequency of the group.
+         * @return The builder to facilitate chaining
+         *         {@code builder.setXXX(..).setXXX(..)}.
+         */
+        public @NonNull Builder setGroupOperatingFrequency(int frequency) {
+            if (frequency < 0) {
+                throw new IllegalArgumentException(
+                    "Invalid group operating frequency!");
+            }
+            mGroupOperatingFrequency = frequency;
+            return this;
+        }
+
+        /**
+         * Specify that the group configuration be persisted (i.e. saved).
+         * By default the group configuration will not be saved.
+         * <p>
+         *     Optional. false by default.
+         *
+         * @param persistent is this group persistent group.
+         * @return The builder to facilitate chaining
+         *         {@code builder.setXXX(..).setXXX(..)}.
+         */
+        public @NonNull Builder enablePersistentMode(boolean persistent) {
+            if (persistent) {
+                mNetId = WifiP2pGroup.PERSISTENT_NET_ID;
+            } else {
+                mNetId = WifiP2pGroup.TEMPORARY_NET_ID;
+            }
+            return this;
+        }
+
+        /**
+         * Build {@link WifiP2pConfig} given the current requests made on the builder.
+         * @return {@link WifiP2pConfig} constructed based on builder method calls.
+         */
+        public @NonNull WifiP2pConfig build() {
+            if (TextUtils.isEmpty(mNetworkName)) {
+                throw new IllegalStateException(
+                        "network name must be non-empty.");
+            }
+            if (TextUtils.isEmpty(mPassphrase)) {
+                throw new IllegalStateException(
+                        "passphrase must be non-empty.");
+            }
+
+            if (mGroupOperatingFrequency > 0 && mGroupOperatingBand > 0) {
+                throw new IllegalStateException(
+                        "Preferred frequency and band are mutually exclusive.");
+            }
+
+            WifiP2pConfig config = new WifiP2pConfig();
+            config.deviceAddress = mDeviceAddress.toString();
+            config.networkName = mNetworkName;
+            config.passphrase = mPassphrase;
+            config.groupOwnerBand = GROUP_OWNER_BAND_AUTO;
+            if (mGroupOperatingFrequency > 0) {
+                config.groupOwnerBand = mGroupOperatingFrequency;
+            } else if (mGroupOperatingBand > 0) {
+                config.groupOwnerBand = mGroupOperatingBand;
+            }
+            config.netId = mNetId;
+            return config;
+        }
+    }
+}
diff --git a/android/net/wifi/p2p/WifiP2pDevice.java b/android/net/wifi/p2p/WifiP2pDevice.java
new file mode 100644
index 0000000..c5318a9
--- /dev/null
+++ b/android/net/wifi/p2p/WifiP2pDevice.java
@@ -0,0 +1,427 @@
+/*
+ * Copyright (C) 2011 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.wifi.p2p;
+
+import android.annotation.UnsupportedAppUsage;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+
+import java.util.Objects;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A class representing a Wi-Fi p2p device
+ *
+ * Note that the operations are not thread safe
+ * {@see WifiP2pManager}
+ */
+public class WifiP2pDevice implements Parcelable {
+
+    private static final String TAG = "WifiP2pDevice";
+
+    /**
+     * The device name is a user friendly string to identify a Wi-Fi p2p device
+     */
+    public String deviceName = "";
+
+    /**
+     * The device MAC address uniquely identifies a Wi-Fi p2p device
+     */
+    public String deviceAddress = "";
+
+    /**
+     * Primary device type identifies the type of device. For example, an application
+     * could filter the devices discovered to only display printers if the purpose is to
+     * enable a printing action from the user. See the Wi-Fi Direct technical specification
+     * for the full list of standard device types supported.
+     */
+    public String primaryDeviceType;
+
+    /**
+     * Secondary device type is an optional attribute that can be provided by a device in
+     * addition to the primary device type.
+     */
+    public String secondaryDeviceType;
+
+
+    // These definitions match the ones in wpa_supplicant
+    /* WPS config methods supported */
+    private static final int WPS_CONFIG_DISPLAY         = 0x0008;
+    private static final int WPS_CONFIG_PUSHBUTTON      = 0x0080;
+    private static final int WPS_CONFIG_KEYPAD          = 0x0100;
+
+    /* Device Capability bitmap */
+    private static final int DEVICE_CAPAB_SERVICE_DISCOVERY         = 1;
+    @SuppressWarnings("unused")
+    private static final int DEVICE_CAPAB_CLIENT_DISCOVERABILITY    = 1<<1;
+    @SuppressWarnings("unused")
+    private static final int DEVICE_CAPAB_CONCURRENT_OPER           = 1<<2;
+    @SuppressWarnings("unused")
+    private static final int DEVICE_CAPAB_INFRA_MANAGED             = 1<<3;
+    @SuppressWarnings("unused")
+    private static final int DEVICE_CAPAB_DEVICE_LIMIT              = 1<<4;
+    private static final int DEVICE_CAPAB_INVITATION_PROCEDURE      = 1<<5;
+
+    /* Group Capability bitmap */
+    private static final int GROUP_CAPAB_GROUP_OWNER                = 1;
+    @SuppressWarnings("unused")
+    private static final int GROUP_CAPAB_PERSISTENT_GROUP           = 1<<1;
+    private static final int GROUP_CAPAB_GROUP_LIMIT                = 1<<2;
+    @SuppressWarnings("unused")
+    private static final int GROUP_CAPAB_INTRA_BSS_DIST             = 1<<3;
+    @SuppressWarnings("unused")
+    private static final int GROUP_CAPAB_CROSS_CONN                 = 1<<4;
+    @SuppressWarnings("unused")
+    private static final int GROUP_CAPAB_PERSISTENT_RECONN          = 1<<5;
+    @SuppressWarnings("unused")
+    private static final int GROUP_CAPAB_GROUP_FORMATION            = 1<<6;
+
+    /**
+     * WPS config methods supported
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public int wpsConfigMethodsSupported;
+
+    /**
+     * Device capability
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public int deviceCapability;
+
+    /**
+     * Group capability
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public int groupCapability;
+
+    public static final int CONNECTED   = 0;
+    public static final int INVITED     = 1;
+    public static final int FAILED      = 2;
+    public static final int AVAILABLE   = 3;
+    public static final int UNAVAILABLE = 4;
+
+    /** Device connection status */
+    public int status = UNAVAILABLE;
+
+    /** @hide */
+    @UnsupportedAppUsage
+    public WifiP2pWfdInfo wfdInfo;
+
+    /** Detailed device string pattern with WFD info
+     * Example:
+     *  P2P-DEVICE-FOUND 00:18:6b:de:a3:6e p2p_dev_addr=00:18:6b:de:a3:6e
+     *  pri_dev_type=1-0050F204-1 name='DWD-300-DEA36E' config_methods=0x188
+     *  dev_capab=0x21 group_capab=0x9
+     */
+    private static final Pattern detailedDevicePattern = Pattern.compile(
+        "((?:[0-9a-f]{2}:){5}[0-9a-f]{2}) " +
+        "(\\d+ )?" +
+        "p2p_dev_addr=((?:[0-9a-f]{2}:){5}[0-9a-f]{2}) " +
+        "pri_dev_type=(\\d+-[0-9a-fA-F]+-\\d+) " +
+        "name='(.*)' " +
+        "config_methods=(0x[0-9a-fA-F]+) " +
+        "dev_capab=(0x[0-9a-fA-F]+) " +
+        "group_capab=(0x[0-9a-fA-F]+)" +
+        "( wfd_dev_info=0x([0-9a-fA-F]{12}))?"
+    );
+
+    /** 2 token device address pattern
+     * Example:
+     *  P2P-DEVICE-LOST p2p_dev_addr=fa:7b:7a:42:02:13
+     *  AP-STA-DISCONNECTED 42:fc:89:a8:96:09
+     */
+    private static final Pattern twoTokenPattern = Pattern.compile(
+        "(p2p_dev_addr=)?((?:[0-9a-f]{2}:){5}[0-9a-f]{2})"
+    );
+
+    /** 3 token device address pattern
+     * Example:
+     *  AP-STA-CONNECTED 42:fc:89:a8:96:09 p2p_dev_addr=fa:7b:7a:42:02:13
+     *  AP-STA-DISCONNECTED 42:fc:89:a8:96:09 p2p_dev_addr=fa:7b:7a:42:02:13
+     */
+    private static final Pattern threeTokenPattern = Pattern.compile(
+        "(?:[0-9a-f]{2}:){5}[0-9a-f]{2} p2p_dev_addr=((?:[0-9a-f]{2}:){5}[0-9a-f]{2})"
+    );
+
+
+    public WifiP2pDevice() {
+    }
+
+    /**
+     * @param string formats supported include
+     *  P2P-DEVICE-FOUND fa:7b:7a:42:02:13 p2p_dev_addr=fa:7b:7a:42:02:13
+     *  pri_dev_type=1-0050F204-1 name='p2p-TEST1' config_methods=0x188 dev_capab=0x27
+     *  group_capab=0x0 wfd_dev_info=000006015d022a0032
+     *
+     *  P2P-DEVICE-LOST p2p_dev_addr=fa:7b:7a:42:02:13
+     *
+     *  AP-STA-CONNECTED 42:fc:89:a8:96:09 [p2p_dev_addr=02:90:4c:a0:92:54]
+     *
+     *  AP-STA-DISCONNECTED 42:fc:89:a8:96:09 [p2p_dev_addr=02:90:4c:a0:92:54]
+     *
+     *  fa:7b:7a:42:02:13
+     *
+     *  Note: The events formats can be looked up in the wpa_supplicant code
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public WifiP2pDevice(String string) throws IllegalArgumentException {
+        String[] tokens = string.split("[ \n]");
+        Matcher match;
+
+        if (tokens.length < 1) {
+            throw new IllegalArgumentException("Malformed supplicant event");
+        }
+
+        switch (tokens.length) {
+            case 1:
+                /* Just a device address */
+                deviceAddress = string;
+                return;
+            case 2:
+                match = twoTokenPattern.matcher(string);
+                if (!match.find()) {
+                    throw new IllegalArgumentException("Malformed supplicant event");
+                }
+                deviceAddress = match.group(2);
+                return;
+            case 3:
+                match = threeTokenPattern.matcher(string);
+                if (!match.find()) {
+                    throw new IllegalArgumentException("Malformed supplicant event");
+                }
+                deviceAddress = match.group(1);
+                return;
+            default:
+                match = detailedDevicePattern.matcher(string);
+                if (!match.find()) {
+                    throw new IllegalArgumentException("Malformed supplicant event");
+                }
+
+                deviceAddress = match.group(3);
+                primaryDeviceType = match.group(4);
+                deviceName = match.group(5);
+                wpsConfigMethodsSupported = parseHex(match.group(6));
+                deviceCapability = parseHex(match.group(7));
+                groupCapability = parseHex(match.group(8));
+                if (match.group(9) != null) {
+                    String str = match.group(10);
+                    wfdInfo = new WifiP2pWfdInfo(parseHex(str.substring(0,4)),
+                            parseHex(str.substring(4,8)),
+                            parseHex(str.substring(8,12)));
+                }
+                break;
+        }
+
+        if (tokens[0].startsWith("P2P-DEVICE-FOUND")) {
+            status = AVAILABLE;
+        }
+    }
+
+    /** Returns true if WPS push button configuration is supported */
+    public boolean wpsPbcSupported() {
+        return (wpsConfigMethodsSupported & WPS_CONFIG_PUSHBUTTON) != 0;
+    }
+
+    /** Returns true if WPS keypad configuration is supported */
+    public boolean wpsKeypadSupported() {
+        return (wpsConfigMethodsSupported & WPS_CONFIG_KEYPAD) != 0;
+    }
+
+    /** Returns true if WPS display configuration is supported */
+    public boolean wpsDisplaySupported() {
+        return (wpsConfigMethodsSupported & WPS_CONFIG_DISPLAY) != 0;
+    }
+
+    /** Returns true if the device is capable of service discovery */
+    public boolean isServiceDiscoveryCapable() {
+        return (deviceCapability & DEVICE_CAPAB_SERVICE_DISCOVERY) != 0;
+    }
+
+    /** Returns true if the device is capable of invitation {@hide}*/
+    public boolean isInvitationCapable() {
+        return (deviceCapability & DEVICE_CAPAB_INVITATION_PROCEDURE) != 0;
+    }
+
+    /** Returns true if the device reaches the limit. {@hide}*/
+    public boolean isDeviceLimit() {
+        return (deviceCapability & DEVICE_CAPAB_DEVICE_LIMIT) != 0;
+    }
+
+    /** Returns true if the device is a group owner */
+    public boolean isGroupOwner() {
+        return (groupCapability & GROUP_CAPAB_GROUP_OWNER) != 0;
+    }
+
+    /** Returns true if the group reaches the limit. {@hide}*/
+    public boolean isGroupLimit() {
+        return (groupCapability & GROUP_CAPAB_GROUP_LIMIT) != 0;
+    }
+
+    /**
+     * Update device details. This will be throw an exception if the device address
+     * does not match.
+     * @param device to be updated
+     * @throws IllegalArgumentException if the device is null or device address does not match
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public void update(WifiP2pDevice device) {
+        updateSupplicantDetails(device);
+        status = device.status;
+    }
+
+    /** Updates details obtained from supplicant @hide */
+    public void updateSupplicantDetails(WifiP2pDevice device) {
+        if (device == null) {
+            throw new IllegalArgumentException("device is null");
+        }
+        if (device.deviceAddress == null) {
+            throw new IllegalArgumentException("deviceAddress is null");
+        }
+        if (!deviceAddress.equals(device.deviceAddress)) {
+            throw new IllegalArgumentException("deviceAddress does not match");
+        }
+        deviceName = device.deviceName;
+        primaryDeviceType = device.primaryDeviceType;
+        secondaryDeviceType = device.secondaryDeviceType;
+        wpsConfigMethodsSupported = device.wpsConfigMethodsSupported;
+        deviceCapability = device.deviceCapability;
+        groupCapability = device.groupCapability;
+        wfdInfo = device.wfdInfo;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) return true;
+        if (!(obj instanceof WifiP2pDevice)) return false;
+
+        WifiP2pDevice other = (WifiP2pDevice) obj;
+        if (other == null || other.deviceAddress == null) {
+            return (deviceAddress == null);
+        }
+        return other.deviceAddress.equals(deviceAddress);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hashCode(deviceAddress);
+    }
+
+    @Override
+    public String toString() {
+        StringBuffer sbuf = new StringBuffer();
+        sbuf.append("Device: ").append(deviceName);
+        sbuf.append("\n deviceAddress: ").append(deviceAddress);
+        sbuf.append("\n primary type: ").append(primaryDeviceType);
+        sbuf.append("\n secondary type: ").append(secondaryDeviceType);
+        sbuf.append("\n wps: ").append(wpsConfigMethodsSupported);
+        sbuf.append("\n grpcapab: ").append(groupCapability);
+        sbuf.append("\n devcapab: ").append(deviceCapability);
+        sbuf.append("\n status: ").append(status);
+        sbuf.append("\n wfdInfo: ").append(wfdInfo);
+        return sbuf.toString();
+    }
+
+    /** Implement the Parcelable interface */
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    /** copy constructor */
+    public WifiP2pDevice(WifiP2pDevice source) {
+        if (source != null) {
+            deviceName = source.deviceName;
+            deviceAddress = source.deviceAddress;
+            primaryDeviceType = source.primaryDeviceType;
+            secondaryDeviceType = source.secondaryDeviceType;
+            wpsConfigMethodsSupported = source.wpsConfigMethodsSupported;
+            deviceCapability = source.deviceCapability;
+            groupCapability = source.groupCapability;
+            status = source.status;
+            if (source.wfdInfo != null) {
+                wfdInfo = new WifiP2pWfdInfo(source.wfdInfo);
+            }
+        }
+    }
+
+    /** Implement the Parcelable interface */
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeString(deviceName);
+        dest.writeString(deviceAddress);
+        dest.writeString(primaryDeviceType);
+        dest.writeString(secondaryDeviceType);
+        dest.writeInt(wpsConfigMethodsSupported);
+        dest.writeInt(deviceCapability);
+        dest.writeInt(groupCapability);
+        dest.writeInt(status);
+        if (wfdInfo != null) {
+            dest.writeInt(1);
+            wfdInfo.writeToParcel(dest, flags);
+        } else {
+            dest.writeInt(0);
+        }
+    }
+
+    /** Implement the Parcelable interface */
+    public static final @android.annotation.NonNull Creator<WifiP2pDevice> CREATOR =
+        new Creator<WifiP2pDevice>() {
+            @Override
+            public WifiP2pDevice createFromParcel(Parcel in) {
+                WifiP2pDevice device = new WifiP2pDevice();
+                device.deviceName = in.readString();
+                device.deviceAddress = in.readString();
+                device.primaryDeviceType = in.readString();
+                device.secondaryDeviceType = in.readString();
+                device.wpsConfigMethodsSupported = in.readInt();
+                device.deviceCapability = in.readInt();
+                device.groupCapability = in.readInt();
+                device.status = in.readInt();
+                if (in.readInt() == 1) {
+                    device.wfdInfo = WifiP2pWfdInfo.CREATOR.createFromParcel(in);
+                }
+                return device;
+            }
+
+            @Override
+            public WifiP2pDevice[] newArray(int size) {
+                return new WifiP2pDevice[size];
+            }
+        };
+
+    //supported formats: 0x1abc, 0X1abc, 1abc
+    private int parseHex(String hexString) {
+        int num = 0;
+        if (hexString.startsWith("0x") || hexString.startsWith("0X")) {
+            hexString = hexString.substring(2);
+        }
+
+        try {
+            num = Integer.parseInt(hexString, 16);
+        } catch(NumberFormatException e) {
+            Log.e(TAG, "Failed to parse hex string " + hexString);
+        }
+        return num;
+    }
+}
diff --git a/android/net/wifi/p2p/WifiP2pDeviceList.java b/android/net/wifi/p2p/WifiP2pDeviceList.java
new file mode 100644
index 0000000..acf06fb
--- /dev/null
+++ b/android/net/wifi/p2p/WifiP2pDeviceList.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2011 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.wifi.p2p;
+
+import android.annotation.UnsupportedAppUsage;
+import android.os.Parcelable;
+import android.os.Parcel;
+import android.net.wifi.p2p.WifiP2pDevice;
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+
+/**
+ * A class representing a Wi-Fi P2p device list.
+ *
+ * Note that the operations are not thread safe.
+ * {@see WifiP2pManager}
+ */
+public class WifiP2pDeviceList implements Parcelable {
+
+    private final HashMap<String, WifiP2pDevice> mDevices = new HashMap<String, WifiP2pDevice>();
+
+    public WifiP2pDeviceList() {
+    }
+
+    /** copy constructor */
+    public WifiP2pDeviceList(WifiP2pDeviceList source) {
+        if (source != null) {
+            for (WifiP2pDevice d : source.getDeviceList()) {
+                mDevices.put(d.deviceAddress, new WifiP2pDevice(d));
+            }
+        }
+    }
+
+    /** @hide */
+    public WifiP2pDeviceList(ArrayList<WifiP2pDevice> devices) {
+        for (WifiP2pDevice device : devices) {
+            if (device.deviceAddress != null) {
+                mDevices.put(device.deviceAddress, new WifiP2pDevice(device));
+            }
+        }
+    }
+
+    private void validateDevice(WifiP2pDevice device) {
+        if (device == null) throw new IllegalArgumentException("Null device");
+        if (TextUtils.isEmpty(device.deviceAddress)) {
+            throw new IllegalArgumentException("Empty deviceAddress");
+        }
+    }
+
+    private void validateDeviceAddress(String deviceAddress) {
+        if (TextUtils.isEmpty(deviceAddress)) {
+            throw new IllegalArgumentException("Empty deviceAddress");
+        }
+    }
+
+    /** Clear the list @hide */
+    public boolean clear() {
+        if (mDevices.isEmpty()) return false;
+        mDevices.clear();
+        return true;
+    }
+
+    /**
+     * Add/update a device to the list. If the device is not found, a new device entry
+     * is created. If the device is already found, the device details are updated
+     * @param device to be updated
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public void update(WifiP2pDevice device) {
+        updateSupplicantDetails(device);
+        mDevices.get(device.deviceAddress).status = device.status;
+    }
+
+    /** Only updates details fetched from the supplicant @hide */
+    public void updateSupplicantDetails(WifiP2pDevice device) {
+        validateDevice(device);
+        WifiP2pDevice d = mDevices.get(device.deviceAddress);
+        if (d != null) {
+            d.deviceName = device.deviceName;
+            d.primaryDeviceType = device.primaryDeviceType;
+            d.secondaryDeviceType = device.secondaryDeviceType;
+            d.wpsConfigMethodsSupported = device.wpsConfigMethodsSupported;
+            d.deviceCapability = device.deviceCapability;
+            d.groupCapability = device.groupCapability;
+            d.wfdInfo = device.wfdInfo;
+            return;
+        }
+        //Not found, add a new one
+        mDevices.put(device.deviceAddress, device);
+    }
+
+    /** @hide */
+    public void updateGroupCapability(String deviceAddress, int groupCapab) {
+        validateDeviceAddress(deviceAddress);
+        WifiP2pDevice d = mDevices.get(deviceAddress);
+        if (d != null) {
+            d.groupCapability = groupCapab;
+        }
+    }
+
+    /** @hide */
+    public void updateStatus(String deviceAddress, int status) {
+        validateDeviceAddress(deviceAddress);
+        WifiP2pDevice d = mDevices.get(deviceAddress);
+        if (d != null) {
+            d.status = status;
+        }
+    }
+
+    /**
+     * Fetch a device from the list
+     * @param deviceAddress is the address of the device
+     * @return WifiP2pDevice device found, or null if none found
+     */
+    public WifiP2pDevice get(String deviceAddress) {
+        validateDeviceAddress(deviceAddress);
+        return mDevices.get(deviceAddress);
+    }
+
+    /** @hide */
+    public boolean remove(WifiP2pDevice device) {
+        validateDevice(device);
+        return mDevices.remove(device.deviceAddress) != null;
+    }
+
+    /**
+     * Remove a device from the list
+     * @param deviceAddress is the address of the device
+     * @return WifiP2pDevice device removed, or null if none removed
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public WifiP2pDevice remove(String deviceAddress) {
+        validateDeviceAddress(deviceAddress);
+        return mDevices.remove(deviceAddress);
+    }
+
+    /** Returns true if any device the list was removed @hide */
+    public boolean remove(WifiP2pDeviceList list) {
+        boolean ret = false;
+        for (WifiP2pDevice d : list.mDevices.values()) {
+            if (remove(d)) ret = true;
+        }
+        return ret;
+    }
+
+    /** Get the list of devices */
+    public Collection<WifiP2pDevice> getDeviceList() {
+        return Collections.unmodifiableCollection(mDevices.values());
+    }
+
+    /** @hide */
+    public boolean isGroupOwner(String deviceAddress) {
+        validateDeviceAddress(deviceAddress);
+        WifiP2pDevice device = mDevices.get(deviceAddress);
+        if (device == null) {
+            throw new IllegalArgumentException("Device not found " + deviceAddress);
+        }
+        return device.isGroupOwner();
+    }
+
+    public String toString() {
+        StringBuffer sbuf = new StringBuffer();
+        for (WifiP2pDevice device : mDevices.values()) {
+            sbuf.append("\n").append(device);
+        }
+        return sbuf.toString();
+    }
+
+    /** Implement the Parcelable interface */
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Implement the Parcelable interface */
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(mDevices.size());
+        for(WifiP2pDevice device : mDevices.values()) {
+            dest.writeParcelable(device, flags);
+        }
+    }
+
+    /** Implement the Parcelable interface */
+    public static final @android.annotation.NonNull Creator<WifiP2pDeviceList> CREATOR =
+        new Creator<WifiP2pDeviceList>() {
+            public WifiP2pDeviceList createFromParcel(Parcel in) {
+                WifiP2pDeviceList deviceList = new WifiP2pDeviceList();
+
+                int deviceCount = in.readInt();
+                for (int i = 0; i < deviceCount; i++) {
+                    deviceList.update((WifiP2pDevice)in.readParcelable(null));
+                }
+                return deviceList;
+            }
+
+            public WifiP2pDeviceList[] newArray(int size) {
+                return new WifiP2pDeviceList[size];
+            }
+        };
+}
diff --git a/android/net/wifi/p2p/WifiP2pGroup.java b/android/net/wifi/p2p/WifiP2pGroup.java
new file mode 100644
index 0000000..4866bd4
--- /dev/null
+++ b/android/net/wifi/p2p/WifiP2pGroup.java
@@ -0,0 +1,357 @@
+/*
+ * Copyright (C) 2011 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.wifi.p2p;
+
+import android.annotation.UnsupportedAppUsage;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * A class representing a Wi-Fi P2p group. A p2p group consists of a single group
+ * owner and one or more clients. In the case of a group with only two devices, one
+ * will be the group owner and the other will be a group client.
+ *
+ * {@see WifiP2pManager}
+ */
+public class WifiP2pGroup implements Parcelable {
+
+    /** The temporary network id.
+     * {@hide} */
+    @UnsupportedAppUsage
+    public static final int TEMPORARY_NET_ID = -1;
+
+    /** The persistent network id.
+     * If a matching persistent profile is found, use it.
+     * Otherwise, create a new persistent profile.
+     * {@hide} */
+    public static final int PERSISTENT_NET_ID = -2;
+
+    /** The network name */
+    private String mNetworkName;
+
+    /** Group owner */
+    private WifiP2pDevice mOwner;
+
+    /** Device is group owner */
+    private boolean mIsGroupOwner;
+
+    /** Group clients */
+    private List<WifiP2pDevice> mClients = new ArrayList<WifiP2pDevice>();
+
+    /** The passphrase used for WPA2-PSK */
+    private String mPassphrase;
+
+    private String mInterface;
+
+    /** The network id in the wpa_supplicant */
+    private int mNetId;
+
+    /** The frequency (in MHz) used by this group */
+    private int mFrequency;
+
+    /** P2P group started string pattern */
+    private static final Pattern groupStartedPattern = Pattern.compile(
+        "ssid=\"(.+)\" " +
+        "freq=(\\d+) " +
+        "(?:psk=)?([0-9a-fA-F]{64})?" +
+        "(?:passphrase=)?(?:\"(.{0,63})\")? " +
+        "go_dev_addr=((?:[0-9a-f]{2}:){5}[0-9a-f]{2})" +
+        " ?(\\[PERSISTENT\\])?"
+    );
+
+    public WifiP2pGroup() {
+    }
+
+    /**
+     * @param supplicantEvent formats supported include
+     *
+     *  P2P-GROUP-STARTED p2p-wlan0-0 [client|GO] ssid="DIRECT-W8" freq=2437
+     *  [psk=2182b2e50e53f260d04f3c7b25ef33c965a3291b9b36b455a82d77fd82ca15bc|
+     *  passphrase="fKG4jMe3"] go_dev_addr=fa:7b:7a:42:02:13 [PERSISTENT]
+     *
+     *  P2P-GROUP-REMOVED p2p-wlan0-0 [client|GO] reason=REQUESTED
+     *
+     *  P2P-INVITATION-RECEIVED sa=fa:7b:7a:42:02:13 go_dev_addr=f8:7b:7a:42:02:13
+     *  bssid=fa:7b:7a:42:82:13 unknown-network
+     *
+     *  P2P-INVITATION-RECEIVED sa=b8:f9:34:2a:c7:9d persistent=0
+     *
+     *  Note: The events formats can be looked up in the wpa_supplicant code
+     *  @hide
+     */
+    @UnsupportedAppUsage
+    public WifiP2pGroup(String supplicantEvent) throws IllegalArgumentException {
+
+        String[] tokens = supplicantEvent.split(" ");
+
+        if (tokens.length < 3) {
+            throw new IllegalArgumentException("Malformed supplicant event");
+        }
+
+        if (tokens[0].startsWith("P2P-GROUP")) {
+            mInterface = tokens[1];
+            mIsGroupOwner = tokens[2].equals("GO");
+
+            Matcher match = groupStartedPattern.matcher(supplicantEvent);
+            if (!match.find()) {
+                return;
+            }
+
+            mNetworkName = match.group(1);
+            // It throws NumberFormatException if the string cannot be parsed as an integer.
+            mFrequency = Integer.parseInt(match.group(2));
+            // psk is unused right now
+            //String psk = match.group(3);
+            mPassphrase = match.group(4);
+            mOwner = new WifiP2pDevice(match.group(5));
+            if (match.group(6) != null) {
+                mNetId = PERSISTENT_NET_ID;
+            } else {
+                mNetId = TEMPORARY_NET_ID;
+            }
+        } else if (tokens[0].equals("P2P-INVITATION-RECEIVED")) {
+            String sa = null;
+            mNetId = PERSISTENT_NET_ID;
+            for (String token : tokens) {
+                String[] nameValue = token.split("=");
+                if (nameValue.length != 2) continue;
+
+                if (nameValue[0].equals("sa")) {
+                    sa = nameValue[1];
+
+                    // set source address into the client list.
+                    WifiP2pDevice dev = new WifiP2pDevice();
+                    dev.deviceAddress = nameValue[1];
+                    mClients.add(dev);
+                    continue;
+                }
+
+                if (nameValue[0].equals("go_dev_addr")) {
+                    mOwner = new WifiP2pDevice(nameValue[1]);
+                    continue;
+                }
+
+                if (nameValue[0].equals("persistent")) {
+                    mNetId = Integer.parseInt(nameValue[1]);
+                    continue;
+                }
+            }
+        } else {
+            throw new IllegalArgumentException("Malformed supplicant event");
+        }
+    }
+
+    /** @hide */
+    public void setNetworkName(String networkName) {
+        mNetworkName = networkName;
+    }
+
+    /**
+     * Get the network name (SSID) of the group. Legacy Wi-Fi clients will discover
+     * the p2p group using the network name.
+     */
+    public String getNetworkName() {
+        return mNetworkName;
+    }
+
+    /** @hide */
+    @UnsupportedAppUsage
+    public void setIsGroupOwner(boolean isGo) {
+        mIsGroupOwner = isGo;
+    }
+
+    /** Check whether this device is the group owner of the created p2p group */
+    public boolean isGroupOwner() {
+        return mIsGroupOwner;
+    }
+
+    /** @hide */
+    public void setOwner(WifiP2pDevice device) {
+        mOwner = device;
+    }
+
+    /** Get the details of the group owner as a {@link WifiP2pDevice} object */
+    public WifiP2pDevice getOwner() {
+        return mOwner;
+    }
+
+    /** @hide */
+    public void addClient(String address) {
+        addClient(new WifiP2pDevice(address));
+    }
+
+    /** @hide */
+    public void addClient(WifiP2pDevice device) {
+        for (WifiP2pDevice client : mClients) {
+            if (client.equals(device)) return;
+        }
+        mClients.add(device);
+    }
+
+    /** @hide */
+    public boolean removeClient(String address) {
+        return mClients.remove(new WifiP2pDevice(address));
+    }
+
+    /** @hide */
+    public boolean removeClient(WifiP2pDevice device) {
+        return mClients.remove(device);
+    }
+
+    /** @hide */
+    @UnsupportedAppUsage
+    public boolean isClientListEmpty() {
+        return mClients.size() == 0;
+    }
+
+    /** @hide Returns {@code true} if the device is part of the group */
+    public boolean contains(WifiP2pDevice device) {
+        if (mOwner.equals(device) || mClients.contains(device)) return true;
+        return false;
+    }
+
+    /** Get the list of clients currently part of the p2p group */
+    public Collection<WifiP2pDevice> getClientList() {
+        return Collections.unmodifiableCollection(mClients);
+    }
+
+    /** @hide */
+    public void setPassphrase(String passphrase) {
+        mPassphrase = passphrase;
+    }
+
+    /**
+     * Get the passphrase of the group. This function will return a valid passphrase only
+     * at the group owner. Legacy Wi-Fi clients will need this passphrase alongside
+     * network name obtained from {@link #getNetworkName()} to join the group
+     */
+    public String getPassphrase() {
+        return mPassphrase;
+    }
+
+    /** @hide */
+    @UnsupportedAppUsage
+    public void setInterface(String intf) {
+        mInterface = intf;
+    }
+
+    /** Get the interface name on which the group is created */
+    public String getInterface() {
+        return mInterface;
+    }
+
+    /** @hide */
+    @UnsupportedAppUsage
+    public int getNetworkId() {
+        return mNetId;
+    }
+
+    /** @hide */
+    @UnsupportedAppUsage
+    public void setNetworkId(int netId) {
+        this.mNetId = netId;
+    }
+
+    /** Get the operating frequency (in MHz) of the p2p group */
+    public int getFrequency() {
+        return mFrequency;
+    }
+
+    /** @hide */
+    public void setFrequency(int freq) {
+        this.mFrequency = freq;
+    }
+
+    public String toString() {
+        StringBuffer sbuf = new StringBuffer();
+        sbuf.append("network: ").append(mNetworkName);
+        sbuf.append("\n isGO: ").append(mIsGroupOwner);
+        sbuf.append("\n GO: ").append(mOwner);
+        for (WifiP2pDevice client : mClients) {
+            sbuf.append("\n Client: ").append(client);
+        }
+        sbuf.append("\n interface: ").append(mInterface);
+        sbuf.append("\n networkId: ").append(mNetId);
+        sbuf.append("\n frequency: ").append(mFrequency);
+        return sbuf.toString();
+    }
+
+    /** Implement the Parcelable interface */
+    public int describeContents() {
+        return 0;
+    }
+
+    /** copy constructor */
+    public WifiP2pGroup(WifiP2pGroup source) {
+        if (source != null) {
+            mNetworkName = source.getNetworkName();
+            mOwner = new WifiP2pDevice(source.getOwner());
+            mIsGroupOwner = source.mIsGroupOwner;
+            for (WifiP2pDevice d : source.getClientList()) mClients.add(d);
+            mPassphrase = source.getPassphrase();
+            mInterface = source.getInterface();
+            mNetId = source.getNetworkId();
+            mFrequency = source.getFrequency();
+        }
+    }
+
+    /** Implement the Parcelable interface */
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeString(mNetworkName);
+        dest.writeParcelable(mOwner, flags);
+        dest.writeByte(mIsGroupOwner ? (byte) 1: (byte) 0);
+        dest.writeInt(mClients.size());
+        for (WifiP2pDevice client : mClients) {
+            dest.writeParcelable(client, flags);
+        }
+        dest.writeString(mPassphrase);
+        dest.writeString(mInterface);
+        dest.writeInt(mNetId);
+        dest.writeInt(mFrequency);
+    }
+
+    /** Implement the Parcelable interface */
+    public static final @android.annotation.NonNull Creator<WifiP2pGroup> CREATOR =
+        new Creator<WifiP2pGroup>() {
+            public WifiP2pGroup createFromParcel(Parcel in) {
+                WifiP2pGroup group = new WifiP2pGroup();
+                group.setNetworkName(in.readString());
+                group.setOwner((WifiP2pDevice)in.readParcelable(null));
+                group.setIsGroupOwner(in.readByte() == (byte)1);
+                int clientCount = in.readInt();
+                for (int i=0; i<clientCount; i++) {
+                    group.addClient((WifiP2pDevice) in.readParcelable(null));
+                }
+                group.setPassphrase(in.readString());
+                group.setInterface(in.readString());
+                group.setNetworkId(in.readInt());
+                group.setFrequency(in.readInt());
+                return group;
+            }
+
+            public WifiP2pGroup[] newArray(int size) {
+                return new WifiP2pGroup[size];
+            }
+        };
+}
diff --git a/android/net/wifi/p2p/WifiP2pGroupList.java b/android/net/wifi/p2p/WifiP2pGroupList.java
new file mode 100644
index 0000000..62524d9
--- /dev/null
+++ b/android/net/wifi/p2p/WifiP2pGroupList.java
@@ -0,0 +1,250 @@
+/*
+ * Copyright (C) 2012 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.wifi.p2p;
+
+import java.util.Collection;
+import java.util.Map;
+
+import android.annotation.UnsupportedAppUsage;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.LruCache;
+
+
+/**
+ * A class representing a Wi-Fi P2p group list
+ *
+ * {@see WifiP2pManager}
+ * @hide
+ */
+public class WifiP2pGroupList implements Parcelable {
+
+    private static final int CREDENTIAL_MAX_NUM             =   32;
+
+    @UnsupportedAppUsage
+    private final LruCache<Integer, WifiP2pGroup> mGroups;
+    private final GroupDeleteListener mListener;
+
+    private boolean isClearCalled = false;
+
+    public interface GroupDeleteListener {
+        public void onDeleteGroup(int netId);
+    }
+
+    /** @hide */
+    public WifiP2pGroupList() {
+        this(null, null);
+    }
+
+    /** @hide */
+    @UnsupportedAppUsage
+    public WifiP2pGroupList(WifiP2pGroupList source, GroupDeleteListener listener) {
+        mListener = listener;
+        mGroups = new LruCache<Integer, WifiP2pGroup>(CREDENTIAL_MAX_NUM) {
+            @Override
+            protected void entryRemoved(boolean evicted, Integer netId,
+                    WifiP2pGroup oldValue, WifiP2pGroup newValue) {
+                if (mListener != null && !isClearCalled) {
+                    mListener.onDeleteGroup(oldValue.getNetworkId());
+                }
+            }
+        };
+
+        if (source != null) {
+            for (Map.Entry<Integer, WifiP2pGroup> item : source.mGroups.snapshot().entrySet()) {
+                mGroups.put(item.getKey(), item.getValue());
+            }
+        }
+    }
+
+    /**
+     * Return the list of p2p group.
+     *
+     * @return the list of p2p group.
+     */
+    @UnsupportedAppUsage
+    public Collection<WifiP2pGroup> getGroupList() {
+        return mGroups.snapshot().values();
+    }
+
+    /**
+     * Add the specified group to this group list.
+     *
+     * @param group
+     * @hide
+     */
+    public void add(WifiP2pGroup group) {
+        mGroups.put(group.getNetworkId(), group);
+    }
+
+    /**
+     * Remove the group with the specified network id from this group list.
+     *
+     * @param netId
+     * @hide
+     */
+    public void remove(int netId) {
+        mGroups.remove(netId);
+    }
+
+    /**
+     * Remove the group with the specified device address from this group list.
+     *
+     * @param deviceAddress
+     */
+    void remove(String deviceAddress) {
+        remove(getNetworkId(deviceAddress));
+    }
+
+    /**
+     * Clear the group.
+     * @hide
+     */
+    public boolean clear() {
+        if (mGroups.size() == 0) return false;
+        isClearCalled = true;
+        mGroups.evictAll();
+        isClearCalled = false;
+        return true;
+    }
+
+    /**
+     * Return the network id of the group owner profile with the specified p2p device
+     * address.
+     * If more than one persistent group of the same address is present in the list,
+     * return the first one.
+     *
+     * @param deviceAddress p2p device address.
+     * @return the network id. if not found, return -1.
+     * @hide
+     */
+    public int getNetworkId(String deviceAddress) {
+        if (deviceAddress == null) return -1;
+
+        final Collection<WifiP2pGroup> groups = mGroups.snapshot().values();
+        for (WifiP2pGroup grp: groups) {
+            if (deviceAddress.equalsIgnoreCase(grp.getOwner().deviceAddress)) {
+                // update cache ordered.
+                mGroups.get(grp.getNetworkId());
+                return grp.getNetworkId();
+            }
+        }
+        return -1;
+    }
+
+    /**
+     * Return the network id of the group with the specified p2p device address
+     * and the ssid.
+     *
+     * @param deviceAddress p2p device address.
+     * @param ssid ssid.
+     * @return the network id. if not found, return -1.
+     * @hide
+     */
+    public int getNetworkId(String deviceAddress, String ssid) {
+        if (deviceAddress == null || ssid == null) {
+            return -1;
+        }
+
+        final Collection<WifiP2pGroup> groups = mGroups.snapshot().values();
+        for (WifiP2pGroup grp: groups) {
+            if (deviceAddress.equalsIgnoreCase(grp.getOwner().deviceAddress) &&
+                    ssid.equals(grp.getNetworkName())) {
+                // update cache ordered.
+                mGroups.get(grp.getNetworkId());
+                return grp.getNetworkId();
+            }
+        }
+
+        return -1;
+    }
+
+    /**
+     * Return the group owner address of the group with the specified network id
+     *
+     * @param netId network id.
+     * @return the address. if not found, return null.
+     * @hide
+     */
+    public String getOwnerAddr(int netId) {
+        WifiP2pGroup grp = mGroups.get(netId);
+        if (grp != null) {
+            return grp.getOwner().deviceAddress;
+        }
+        return null;
+    }
+
+    /**
+     * Return true if this group list contains the specified network id.
+     * This function does NOT update LRU information.
+     * It means the internal queue is NOT reordered.
+     *
+     * @param netId network id.
+     * @return true if the specified network id is present in this group list.
+     * @hide
+     */
+    public boolean contains(int netId) {
+        final Collection<WifiP2pGroup> groups = mGroups.snapshot().values();
+        for (WifiP2pGroup grp: groups) {
+            if (netId == grp.getNetworkId()) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    public String toString() {
+        StringBuffer sbuf = new StringBuffer();
+
+        final Collection<WifiP2pGroup> groups = mGroups.snapshot().values();
+        for (WifiP2pGroup grp: groups) {
+            sbuf.append(grp).append("\n");
+        }
+        return sbuf.toString();
+    }
+
+    /** Implement the Parcelable interface */
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Implement the Parcelable interface */
+    public void writeToParcel(Parcel dest, int flags) {
+        final Collection<WifiP2pGroup> groups = mGroups.snapshot().values();
+        dest.writeInt(groups.size());
+        for(WifiP2pGroup group : groups) {
+            dest.writeParcelable(group, flags);
+        }
+    }
+
+    /** Implement the Parcelable interface */
+    public static final @android.annotation.NonNull Creator<WifiP2pGroupList> CREATOR =
+        new Creator<WifiP2pGroupList>() {
+            public WifiP2pGroupList createFromParcel(Parcel in) {
+                WifiP2pGroupList grpList = new WifiP2pGroupList();
+
+                int deviceCount = in.readInt();
+                for (int i = 0; i < deviceCount; i++) {
+                    grpList.add((WifiP2pGroup)in.readParcelable(null));
+                }
+                return grpList;
+            }
+
+            public WifiP2pGroupList[] newArray(int size) {
+                return new WifiP2pGroupList[size];
+            }
+        };
+}
diff --git a/android/net/wifi/p2p/WifiP2pInfo.java b/android/net/wifi/p2p/WifiP2pInfo.java
new file mode 100644
index 0000000..33bc37e
--- /dev/null
+++ b/android/net/wifi/p2p/WifiP2pInfo.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2011 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.wifi.p2p;
+
+import android.os.Parcelable;
+import android.os.Parcel;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+
+/**
+ * A class representing connection information about a Wi-Fi p2p group
+ *
+ * {@see WifiP2pManager}
+ */
+public class WifiP2pInfo implements Parcelable {
+
+    /** Indicates if a p2p group has been successfully formed */
+    public boolean groupFormed;
+
+    /** Indicates if the current device is the group owner */
+    public boolean isGroupOwner;
+
+    /** Group owner address */
+    public InetAddress groupOwnerAddress;
+
+    public WifiP2pInfo() {
+    }
+
+    public String toString() {
+        StringBuffer sbuf = new StringBuffer();
+        sbuf.append("groupFormed: ").append(groupFormed)
+            .append(" isGroupOwner: ").append(isGroupOwner)
+            .append(" groupOwnerAddress: ").append(groupOwnerAddress);
+        return sbuf.toString();
+    }
+
+    /** Implement the Parcelable interface */
+    public int describeContents() {
+        return 0;
+    }
+
+    /** copy constructor */
+    public WifiP2pInfo(WifiP2pInfo source) {
+        if (source != null) {
+            groupFormed = source.groupFormed;
+            isGroupOwner = source.isGroupOwner;
+            groupOwnerAddress = source.groupOwnerAddress;
+       }
+    }
+
+    /** Implement the Parcelable interface */
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeByte(groupFormed ? (byte)1 : (byte)0);
+        dest.writeByte(isGroupOwner ? (byte)1 : (byte)0);
+
+        if (groupOwnerAddress != null) {
+            dest.writeByte((byte)1);
+            dest.writeByteArray(groupOwnerAddress.getAddress());
+        } else {
+            dest.writeByte((byte)0);
+        }
+    }
+
+    /** Implement the Parcelable interface */
+    public static final @android.annotation.NonNull Creator<WifiP2pInfo> CREATOR =
+        new Creator<WifiP2pInfo>() {
+            public WifiP2pInfo createFromParcel(Parcel in) {
+                WifiP2pInfo info = new WifiP2pInfo();
+                info.groupFormed = (in.readByte() == 1);
+                info.isGroupOwner = (in.readByte() == 1);
+                if (in.readByte() == 1) {
+                    try {
+                        info.groupOwnerAddress = InetAddress.getByAddress(in.createByteArray());
+                    } catch (UnknownHostException e) {}
+                }
+                return info;
+            }
+
+            public WifiP2pInfo[] newArray(int size) {
+                return new WifiP2pInfo[size];
+            }
+        };
+}
diff --git a/android/net/wifi/p2p/WifiP2pManager.java b/android/net/wifi/p2p/WifiP2pManager.java
new file mode 100644
index 0000000..cd659e2
--- /dev/null
+++ b/android/net/wifi/p2p/WifiP2pManager.java
@@ -0,0 +1,1907 @@
+/*
+ * Copyright (C) 2011 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.wifi.p2p;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SdkConstant;
+import android.annotation.SdkConstant.SdkConstantType;
+import android.annotation.SystemService;
+import android.annotation.UnsupportedAppUsage;
+import android.content.Context;
+import android.net.NetworkInfo;
+import android.net.wifi.WpsInfo;
+import android.net.wifi.p2p.nsd.WifiP2pDnsSdServiceInfo;
+import android.net.wifi.p2p.nsd.WifiP2pDnsSdServiceResponse;
+import android.net.wifi.p2p.nsd.WifiP2pServiceInfo;
+import android.net.wifi.p2p.nsd.WifiP2pServiceRequest;
+import android.net.wifi.p2p.nsd.WifiP2pServiceResponse;
+import android.net.wifi.p2p.nsd.WifiP2pUpnpServiceInfo;
+import android.net.wifi.p2p.nsd.WifiP2pUpnpServiceResponse;
+import android.os.Binder;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.internal.util.AsyncChannel;
+import com.android.internal.util.Protocol;
+
+import dalvik.system.CloseGuard;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * This class provides the API for managing Wi-Fi peer-to-peer connectivity. This lets an
+ * application discover available peers, setup connection to peers and query for the list of peers.
+ * When a p2p connection is formed over wifi, the device continues to maintain the uplink
+ * connection over mobile or any other available network for internet connectivity on the device.
+ *
+ * <p> The API is asynchronous and responses to requests from an application are on listener
+ * callbacks provided by the application. The application needs to do an initialization with
+ * {@link #initialize} before doing any p2p operation.
+ *
+ * <p> Most application calls need a {@link ActionListener} instance for receiving callbacks
+ * {@link ActionListener#onSuccess} or {@link ActionListener#onFailure}. Action callbacks
+ * indicate whether the initiation of the action was a success or a failure.
+ * Upon failure, the reason of failure can be one of {@link #ERROR}, {@link #P2P_UNSUPPORTED}
+ * or {@link #BUSY}.
+ *
+ * <p> An application can initiate discovery of peers with {@link #discoverPeers}. An initiated
+ * discovery request from an application stays active until the device starts connecting to a peer
+ * ,forms a p2p group or there is an explicit {@link #stopPeerDiscovery}.
+ * Applications can listen to {@link #WIFI_P2P_DISCOVERY_CHANGED_ACTION} to know if a peer-to-peer
+ * discovery is running or stopped. Additionally, {@link #WIFI_P2P_PEERS_CHANGED_ACTION} indicates
+ * if the peer list has changed.
+ *
+ * <p> When an application needs to fetch the current list of peers, it can request the list
+ * of peers with {@link #requestPeers}. When the peer list is available
+ * {@link PeerListListener#onPeersAvailable} is called with the device list.
+ *
+ * <p> An application can initiate a connection request to a peer through {@link #connect}. See
+ * {@link WifiP2pConfig} for details on setting up the configuration. For communication with legacy
+ * Wi-Fi devices that do not support p2p, an app can create a group using {@link #createGroup}
+ * which creates an access point whose details can be fetched with {@link #requestGroupInfo}.
+ *
+ * <p> After a successful group formation through {@link #createGroup} or through {@link #connect},
+ * use {@link #requestConnectionInfo} to fetch the connection details. The connection info
+ * {@link WifiP2pInfo} contains the address of the group owner
+ * {@link WifiP2pInfo#groupOwnerAddress} and a flag {@link WifiP2pInfo#isGroupOwner} to indicate
+ * if the current device is a p2p group owner. A p2p client can thus communicate with
+ * the p2p group owner through a socket connection. If the current device is the p2p group owner,
+ * {@link WifiP2pInfo#groupOwnerAddress} is anonymized unless the caller holds the
+ * {@code android.Manifest.permission#LOCAL_MAC_ADDRESS} permission.
+ *
+ * <p> With peer discovery using {@link  #discoverPeers}, an application discovers the neighboring
+ * peers, but has no good way to figure out which peer to establish a connection with. For example,
+ * if a game application is interested in finding all the neighboring peers that are also running
+ * the same game, it has no way to find out until after the connection is setup. Pre-association
+ * service discovery is meant to address this issue of filtering the peers based on the running
+ * services.
+ *
+ * <p>With pre-association service discovery, an application can advertise a service for a
+ * application on a peer device prior to a connection setup between the devices.
+ * Currently, DNS based service discovery (Bonjour) and Upnp are the higher layer protocols
+ * supported. Get Bonjour resources at dns-sd.org and Upnp resources at upnp.org
+ * As an example, a video application can discover a Upnp capable media renderer
+ * prior to setting up a Wi-fi p2p connection with the device.
+ *
+ * <p> An application can advertise a Upnp or a Bonjour service with a call to
+ * {@link #addLocalService}. After a local service is added,
+ * the framework automatically responds to a peer application discovering the service prior
+ * to establishing a p2p connection. A call to {@link #removeLocalService} removes a local
+ * service and {@link #clearLocalServices} can be used to clear all local services.
+ *
+ * <p> An application that is looking for peer devices that support certain services
+ * can do so with a call to  {@link #discoverServices}. Prior to initiating the discovery,
+ * application can add service discovery request with a call to {@link #addServiceRequest},
+ * remove a service discovery request with a call to {@link #removeServiceRequest} or clear
+ * all requests with a call to {@link #clearServiceRequests}. When no service requests remain,
+ * a previously running service discovery will stop.
+ *
+ * The application is notified of a result of service discovery request through listener callbacks
+ * set through {@link #setDnsSdResponseListeners} for Bonjour or
+ * {@link #setUpnpServiceResponseListener} for Upnp.
+ *
+ * <p class="note"><strong>Note:</strong>
+ * Registering an application handler with {@link #initialize} requires the permissions
+ * {@link android.Manifest.permission#ACCESS_WIFI_STATE} and
+ * {@link android.Manifest.permission#CHANGE_WIFI_STATE} to perform any further peer-to-peer
+ * operations.
+ *
+ * {@see WifiP2pConfig}
+ * {@see WifiP2pInfo}
+ * {@see WifiP2pGroup}
+ * {@see WifiP2pDevice}
+ * {@see WifiP2pDeviceList}
+ * {@see android.net.wifi.WpsInfo}
+ */
+@SystemService(Context.WIFI_P2P_SERVICE)
+public class WifiP2pManager {
+    private static final String TAG = "WifiP2pManager";
+    /**
+     * Broadcast intent action to indicate whether Wi-Fi p2p is enabled or disabled. An
+     * extra {@link #EXTRA_WIFI_STATE} provides the state information as int.
+     *
+     * @see #EXTRA_WIFI_STATE
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String WIFI_P2P_STATE_CHANGED_ACTION =
+        "android.net.wifi.p2p.STATE_CHANGED";
+
+    /**
+     * The lookup key for an int that indicates whether Wi-Fi p2p is enabled or disabled.
+     * Retrieve it with {@link android.content.Intent#getIntExtra(String,int)}.
+     *
+     * @see #WIFI_P2P_STATE_DISABLED
+     * @see #WIFI_P2P_STATE_ENABLED
+     */
+    public static final String EXTRA_WIFI_STATE = "wifi_p2p_state";
+
+    /** @hide */
+    @IntDef({
+            WIFI_P2P_STATE_DISABLED,
+            WIFI_P2P_STATE_ENABLED})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface WifiP2pState {
+    }
+
+    /**
+     * Wi-Fi p2p is disabled.
+     *
+     * @see #WIFI_P2P_STATE_CHANGED_ACTION
+     */
+    public static final int WIFI_P2P_STATE_DISABLED = 1;
+
+    /**
+     * Wi-Fi p2p is enabled.
+     *
+     * @see #WIFI_P2P_STATE_CHANGED_ACTION
+     */
+    public static final int WIFI_P2P_STATE_ENABLED = 2;
+
+    /**
+     * Broadcast intent action indicating that the state of Wi-Fi p2p connectivity
+     * has changed. One extra {@link #EXTRA_WIFI_P2P_INFO} provides the p2p connection info in
+     * the form of a {@link WifiP2pInfo} object. Another extra {@link #EXTRA_NETWORK_INFO} provides
+     * the network info in the form of a {@link android.net.NetworkInfo}. A third extra provides
+     * the details of the group.
+     *
+     * All of these permissions are required to receive this broadcast:
+     * {@link android.Manifest.permission#ACCESS_FINE_LOCATION} and
+     * {@link android.Manifest.permission#ACCESS_WIFI_STATE}
+     *
+     * @see #EXTRA_WIFI_P2P_INFO
+     * @see #EXTRA_NETWORK_INFO
+     * @see #EXTRA_WIFI_P2P_GROUP
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String WIFI_P2P_CONNECTION_CHANGED_ACTION =
+        "android.net.wifi.p2p.CONNECTION_STATE_CHANGE";
+
+    /**
+     * The lookup key for a {@link android.net.wifi.p2p.WifiP2pInfo} object
+     * Retrieve with {@link android.content.Intent#getParcelableExtra(String)}.
+     */
+    public static final String EXTRA_WIFI_P2P_INFO = "wifiP2pInfo";
+
+    /**
+     * The lookup key for a {@link android.net.NetworkInfo} object associated with the
+     * p2p network. Retrieve with
+     * {@link android.content.Intent#getParcelableExtra(String)}.
+     */
+    public static final String EXTRA_NETWORK_INFO = "networkInfo";
+
+    /**
+     * The lookup key for a {@link android.net.wifi.p2p.WifiP2pGroup} object
+     * associated with the p2p network. Retrieve with
+     * {@link android.content.Intent#getParcelableExtra(String)}.
+     */
+    public static final String EXTRA_WIFI_P2P_GROUP = "p2pGroupInfo";
+
+    /**
+     * Broadcast intent action indicating that the available peer list has changed. This
+     * can be sent as a result of peers being found, lost or updated.
+     *
+     * All of these permissions are required to receive this broadcast:
+     * {@link android.Manifest.permission#ACCESS_FINE_LOCATION} and
+     * {@link android.Manifest.permission#ACCESS_WIFI_STATE}
+     *
+     * <p> An extra {@link #EXTRA_P2P_DEVICE_LIST} provides the full list of
+     * current peers. The full list of peers can also be obtained any time with
+     * {@link #requestPeers}.
+     *
+     * @see #EXTRA_P2P_DEVICE_LIST
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String WIFI_P2P_PEERS_CHANGED_ACTION =
+        "android.net.wifi.p2p.PEERS_CHANGED";
+
+     /**
+      * The lookup key for a {@link android.net.wifi.p2p.WifiP2pDeviceList} object representing
+      * the new peer list when {@link #WIFI_P2P_PEERS_CHANGED_ACTION} broadcast is sent.
+      *
+      * <p>Retrieve with {@link android.content.Intent#getParcelableExtra(String)}.
+      */
+    public static final String EXTRA_P2P_DEVICE_LIST = "wifiP2pDeviceList";
+
+    /**
+     * Broadcast intent action indicating that peer discovery has either started or stopped.
+     * One extra {@link #EXTRA_DISCOVERY_STATE} indicates whether discovery has started
+     * or stopped.
+     *
+     * <p>Note that discovery will be stopped during a connection setup. If the application tries
+     * to re-initiate discovery during this time, it can fail.
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String WIFI_P2P_DISCOVERY_CHANGED_ACTION =
+        "android.net.wifi.p2p.DISCOVERY_STATE_CHANGE";
+
+    /**
+     * The lookup key for an int that indicates whether p2p discovery has started or stopped.
+     * Retrieve it with {@link android.content.Intent#getIntExtra(String,int)}.
+     *
+     * @see #WIFI_P2P_DISCOVERY_STARTED
+     * @see #WIFI_P2P_DISCOVERY_STOPPED
+     */
+    public static final String EXTRA_DISCOVERY_STATE = "discoveryState";
+
+    /** @hide */
+    @IntDef({
+            WIFI_P2P_DISCOVERY_STOPPED,
+            WIFI_P2P_DISCOVERY_STARTED})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface WifiP2pDiscoveryState {
+    }
+
+    /**
+     * p2p discovery has stopped
+     *
+     * @see #WIFI_P2P_DISCOVERY_CHANGED_ACTION
+     */
+    public static final int WIFI_P2P_DISCOVERY_STOPPED = 1;
+
+    /**
+     * p2p discovery has started
+     *
+     * @see #WIFI_P2P_DISCOVERY_CHANGED_ACTION
+     */
+    public static final int WIFI_P2P_DISCOVERY_STARTED = 2;
+
+    /**
+     * Broadcast intent action indicating that this device details have changed.
+     *
+     * <p> An extra {@link #EXTRA_WIFI_P2P_DEVICE} provides this device details.
+     * The valid device details can also be obtained with
+     * {@link #requestDeviceInfo(Channel, DeviceInfoListener)} when p2p is enabled.
+     * To get information notifications on P2P getting enabled refers
+     * {@link #WIFI_P2P_STATE_ENABLED}.
+     *
+     * <p> The {@link #EXTRA_WIFI_P2P_DEVICE} extra contains an anonymized version of the device's
+     * MAC address. Callers holding the {@code android.Manifest.permission#LOCAL_MAC_ADDRESS}
+     * permission can use {@link #requestDeviceInfo} to obtain the actual MAC address of this
+     * device.
+     *
+     * All of these permissions are required to receive this broadcast:
+     * {@link android.Manifest.permission#ACCESS_FINE_LOCATION} and
+     * {@link android.Manifest.permission#ACCESS_WIFI_STATE}
+     *
+     * @see #EXTRA_WIFI_P2P_DEVICE
+     */
+    @SdkConstant(SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String WIFI_P2P_THIS_DEVICE_CHANGED_ACTION =
+        "android.net.wifi.p2p.THIS_DEVICE_CHANGED";
+
+    /**
+     * The lookup key for a {@link android.net.wifi.p2p.WifiP2pDevice} object
+     * Retrieve with {@link android.content.Intent#getParcelableExtra(String)}.
+     */
+    public static final String EXTRA_WIFI_P2P_DEVICE = "wifiP2pDevice";
+
+    /**
+     * Broadcast intent action indicating that remembered persistent groups have changed.
+     * @hide
+     */
+    public static final String WIFI_P2P_PERSISTENT_GROUPS_CHANGED_ACTION =
+        "android.net.wifi.p2p.PERSISTENT_GROUPS_CHANGED";
+
+    /**
+     * The lookup key for a handover message returned by the WifiP2pService.
+     * @hide
+     */
+    public static final String EXTRA_HANDOVER_MESSAGE =
+            "android.net.wifi.p2p.EXTRA_HANDOVER_MESSAGE";
+
+    /**
+     * The lookup key for a calling package name from WifiP2pManager
+     * @hide
+     */
+    public static final String CALLING_PACKAGE =
+            "android.net.wifi.p2p.CALLING_PACKAGE";
+
+    /**
+     * The lookup key for a calling package binder from WifiP2pManager
+     * @hide
+     */
+    public static final String CALLING_BINDER =
+            "android.net.wifi.p2p.CALLING_BINDER";
+
+    IWifiP2pManager mService;
+
+    private static final int BASE = Protocol.BASE_WIFI_P2P_MANAGER;
+
+    /** @hide */
+    public static final int DISCOVER_PEERS                          = BASE + 1;
+    /** @hide */
+    public static final int DISCOVER_PEERS_FAILED                   = BASE + 2;
+    /** @hide */
+    public static final int DISCOVER_PEERS_SUCCEEDED                = BASE + 3;
+
+    /** @hide */
+    public static final int STOP_DISCOVERY                          = BASE + 4;
+    /** @hide */
+    public static final int STOP_DISCOVERY_FAILED                   = BASE + 5;
+    /** @hide */
+    public static final int STOP_DISCOVERY_SUCCEEDED                = BASE + 6;
+
+    /** @hide */
+    public static final int CONNECT                                 = BASE + 7;
+    /** @hide */
+    public static final int CONNECT_FAILED                          = BASE + 8;
+    /** @hide */
+    public static final int CONNECT_SUCCEEDED                       = BASE + 9;
+
+    /** @hide */
+    public static final int CANCEL_CONNECT                          = BASE + 10;
+    /** @hide */
+    public static final int CANCEL_CONNECT_FAILED                   = BASE + 11;
+    /** @hide */
+    public static final int CANCEL_CONNECT_SUCCEEDED                = BASE + 12;
+
+    /** @hide */
+    @UnsupportedAppUsage
+    public static final int CREATE_GROUP                            = BASE + 13;
+    /** @hide */
+    public static final int CREATE_GROUP_FAILED                     = BASE + 14;
+    /** @hide */
+    public static final int CREATE_GROUP_SUCCEEDED                  = BASE + 15;
+
+    /** @hide */
+    public static final int REMOVE_GROUP                            = BASE + 16;
+    /** @hide */
+    public static final int REMOVE_GROUP_FAILED                     = BASE + 17;
+    /** @hide */
+    public static final int REMOVE_GROUP_SUCCEEDED                  = BASE + 18;
+
+    /** @hide */
+    public static final int REQUEST_PEERS                           = BASE + 19;
+    /** @hide */
+    public static final int RESPONSE_PEERS                          = BASE + 20;
+
+    /** @hide */
+    public static final int REQUEST_CONNECTION_INFO                 = BASE + 21;
+    /** @hide */
+    public static final int RESPONSE_CONNECTION_INFO                = BASE + 22;
+
+    /** @hide */
+    public static final int REQUEST_GROUP_INFO                      = BASE + 23;
+    /** @hide */
+    public static final int RESPONSE_GROUP_INFO                     = BASE + 24;
+
+    /** @hide */
+    public static final int ADD_LOCAL_SERVICE                       = BASE + 28;
+    /** @hide */
+    public static final int ADD_LOCAL_SERVICE_FAILED                = BASE + 29;
+    /** @hide */
+    public static final int ADD_LOCAL_SERVICE_SUCCEEDED             = BASE + 30;
+
+    /** @hide */
+    public static final int REMOVE_LOCAL_SERVICE                    = BASE + 31;
+    /** @hide */
+    public static final int REMOVE_LOCAL_SERVICE_FAILED             = BASE + 32;
+    /** @hide */
+    public static final int REMOVE_LOCAL_SERVICE_SUCCEEDED          = BASE + 33;
+
+    /** @hide */
+    public static final int CLEAR_LOCAL_SERVICES                    = BASE + 34;
+    /** @hide */
+    public static final int CLEAR_LOCAL_SERVICES_FAILED             = BASE + 35;
+    /** @hide */
+    public static final int CLEAR_LOCAL_SERVICES_SUCCEEDED          = BASE + 36;
+
+    /** @hide */
+    public static final int ADD_SERVICE_REQUEST                     = BASE + 37;
+    /** @hide */
+    public static final int ADD_SERVICE_REQUEST_FAILED              = BASE + 38;
+    /** @hide */
+    public static final int ADD_SERVICE_REQUEST_SUCCEEDED           = BASE + 39;
+
+    /** @hide */
+    public static final int REMOVE_SERVICE_REQUEST                  = BASE + 40;
+    /** @hide */
+    public static final int REMOVE_SERVICE_REQUEST_FAILED           = BASE + 41;
+    /** @hide */
+    public static final int REMOVE_SERVICE_REQUEST_SUCCEEDED        = BASE + 42;
+
+    /** @hide */
+    public static final int CLEAR_SERVICE_REQUESTS                  = BASE + 43;
+    /** @hide */
+    public static final int CLEAR_SERVICE_REQUESTS_FAILED           = BASE + 44;
+    /** @hide */
+    public static final int CLEAR_SERVICE_REQUESTS_SUCCEEDED        = BASE + 45;
+
+    /** @hide */
+    public static final int DISCOVER_SERVICES                       = BASE + 46;
+    /** @hide */
+    public static final int DISCOVER_SERVICES_FAILED                = BASE + 47;
+    /** @hide */
+    public static final int DISCOVER_SERVICES_SUCCEEDED             = BASE + 48;
+
+    /** @hide */
+    public static final int PING                                    = BASE + 49;
+
+    /** @hide */
+    public static final int RESPONSE_SERVICE                        = BASE + 50;
+
+    /** @hide */
+    public static final int SET_DEVICE_NAME                         = BASE + 51;
+    /** @hide */
+    public static final int SET_DEVICE_NAME_FAILED                  = BASE + 52;
+    /** @hide */
+    public static final int SET_DEVICE_NAME_SUCCEEDED               = BASE + 53;
+
+    /** @hide */
+    public static final int DELETE_PERSISTENT_GROUP                 = BASE + 54;
+    /** @hide */
+    public static final int DELETE_PERSISTENT_GROUP_FAILED          = BASE + 55;
+    /** @hide */
+    public static final int DELETE_PERSISTENT_GROUP_SUCCEEDED       = BASE + 56;
+
+    /** @hide */
+    public static final int REQUEST_PERSISTENT_GROUP_INFO           = BASE + 57;
+    /** @hide */
+    public static final int RESPONSE_PERSISTENT_GROUP_INFO          = BASE + 58;
+
+    /** @hide */
+    public static final int SET_WFD_INFO                            = BASE + 59;
+    /** @hide */
+    public static final int SET_WFD_INFO_FAILED                     = BASE + 60;
+    /** @hide */
+    public static final int SET_WFD_INFO_SUCCEEDED                  = BASE + 61;
+
+    /** @hide */
+    public static final int START_WPS                               = BASE + 62;
+    /** @hide */
+    public static final int START_WPS_FAILED                        = BASE + 63;
+    /** @hide */
+    public static final int START_WPS_SUCCEEDED                     = BASE + 64;
+
+    /** @hide */
+    public static final int START_LISTEN                            = BASE + 65;
+    /** @hide */
+    public static final int START_LISTEN_FAILED                     = BASE + 66;
+    /** @hide */
+    public static final int START_LISTEN_SUCCEEDED                  = BASE + 67;
+
+    /** @hide */
+    public static final int STOP_LISTEN                             = BASE + 68;
+    /** @hide */
+    public static final int STOP_LISTEN_FAILED                      = BASE + 69;
+    /** @hide */
+    public static final int STOP_LISTEN_SUCCEEDED                   = BASE + 70;
+
+    /** @hide */
+    public static final int SET_CHANNEL                             = BASE + 71;
+    /** @hide */
+    public static final int SET_CHANNEL_FAILED                      = BASE + 72;
+    /** @hide */
+    public static final int SET_CHANNEL_SUCCEEDED                   = BASE + 73;
+
+    /** @hide */
+    public static final int GET_HANDOVER_REQUEST                    = BASE + 75;
+    /** @hide */
+    public static final int GET_HANDOVER_SELECT                     = BASE + 76;
+    /** @hide */
+    public static final int RESPONSE_GET_HANDOVER_MESSAGE           = BASE + 77;
+    /** @hide */
+    public static final int INITIATOR_REPORT_NFC_HANDOVER           = BASE + 78;
+    /** @hide */
+    public static final int RESPONDER_REPORT_NFC_HANDOVER           = BASE + 79;
+    /** @hide */
+    public static final int REPORT_NFC_HANDOVER_SUCCEEDED           = BASE + 80;
+    /** @hide */
+    public static final int REPORT_NFC_HANDOVER_FAILED              = BASE + 81;
+
+    /** @hide */
+    public static final int FACTORY_RESET                           = BASE + 82;
+    /** @hide */
+    public static final int FACTORY_RESET_FAILED                    = BASE + 83;
+    /** @hide */
+    public static final int FACTORY_RESET_SUCCEEDED                 = BASE + 84;
+
+    /** @hide */
+    public static final int REQUEST_ONGOING_PEER_CONFIG             = BASE + 85;
+    /** @hide */
+    public static final int RESPONSE_ONGOING_PEER_CONFIG            = BASE + 86;
+    /** @hide */
+    public static final int SET_ONGOING_PEER_CONFIG                 = BASE + 87;
+    /** @hide */
+    public static final int SET_ONGOING_PEER_CONFIG_FAILED          = BASE + 88;
+    /** @hide */
+    public static final int SET_ONGOING_PEER_CONFIG_SUCCEEDED       = BASE + 89;
+
+    /** @hide */
+    public static final int REQUEST_P2P_STATE                       = BASE + 90;
+    /** @hide */
+    public static final int RESPONSE_P2P_STATE                      = BASE + 91;
+
+    /** @hide */
+    public static final int REQUEST_DISCOVERY_STATE                 = BASE + 92;
+    /** @hide */
+    public static final int RESPONSE_DISCOVERY_STATE                = BASE + 93;
+
+    /** @hide */
+    public static final int REQUEST_NETWORK_INFO                    = BASE + 94;
+    /** @hide */
+    public static final int RESPONSE_NETWORK_INFO                   = BASE + 95;
+
+    /** @hide */
+    public static final int UPDATE_CHANNEL_INFO                     = BASE + 96;
+
+    /** @hide */
+    public static final int REQUEST_DEVICE_INFO                     = BASE + 97;
+    /** @hide */
+    public static final int RESPONSE_DEVICE_INFO                    = BASE + 98;
+
+    /**
+     * Create a new WifiP2pManager instance. Applications use
+     * {@link android.content.Context#getSystemService Context.getSystemService()} to retrieve
+     * the standard {@link android.content.Context#WIFI_P2P_SERVICE Context.WIFI_P2P_SERVICE}.
+     * @param service the Binder interface
+     * @hide - hide this because it takes in a parameter of type IWifiP2pManager, which
+     * is a system private class.
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    public WifiP2pManager(IWifiP2pManager service) {
+        mService = service;
+    }
+
+    /**
+     * Passed with {@link ActionListener#onFailure}.
+     * Indicates that the operation failed due to an internal error.
+     */
+    public static final int ERROR               = 0;
+
+    /**
+     * Passed with {@link ActionListener#onFailure}.
+     * Indicates that the operation failed because p2p is unsupported on the device.
+     */
+    public static final int P2P_UNSUPPORTED     = 1;
+
+    /**
+     * Passed with {@link ActionListener#onFailure}.
+     * Indicates that the operation failed because the framework is busy and
+     * unable to service the request
+     */
+    public static final int BUSY                = 2;
+
+    /**
+     * Passed with {@link ActionListener#onFailure}.
+     * Indicates that the {@link #discoverServices} failed because no service
+     * requests are added. Use {@link #addServiceRequest} to add a service
+     * request.
+     */
+    public static final int NO_SERVICE_REQUESTS = 3;
+
+    /** Interface for callback invocation when framework channel is lost */
+    public interface ChannelListener {
+        /**
+         * The channel to the framework has been disconnected.
+         * Application could try re-initializing using {@link #initialize}
+         */
+        public void onChannelDisconnected();
+    }
+
+    /** Interface for callback invocation on an application action */
+    public interface ActionListener {
+        /** The operation succeeded */
+        public void onSuccess();
+        /**
+         * The operation failed
+         * @param reason The reason for failure could be one of {@link #P2P_UNSUPPORTED},
+         * {@link #ERROR} or {@link #BUSY}
+         */
+        public void onFailure(int reason);
+    }
+
+    /** Interface for callback invocation when peer list is available */
+    public interface PeerListListener {
+        /**
+         * The requested peer list is available
+         * @param peers List of available peers
+         */
+        public void onPeersAvailable(WifiP2pDeviceList peers);
+    }
+
+    /** Interface for callback invocation when connection info is available */
+    public interface ConnectionInfoListener {
+        /**
+         * The requested connection info is available
+         * @param info Wi-Fi p2p connection info
+         */
+        public void onConnectionInfoAvailable(WifiP2pInfo info);
+    }
+
+    /** Interface for callback invocation when group info is available */
+    public interface GroupInfoListener {
+        /**
+         * The requested p2p group info is available
+         * @param group Wi-Fi p2p group info
+         */
+        public void onGroupInfoAvailable(WifiP2pGroup group);
+    }
+
+   /**
+    * Interface for callback invocation when service discovery response other than
+    * Upnp or Bonjour is received
+    */
+    public interface ServiceResponseListener {
+
+        /**
+         * The requested service response is available.
+         *
+         * @param protocolType protocol type. currently only
+         * {@link WifiP2pServiceInfo#SERVICE_TYPE_VENDOR_SPECIFIC}.
+         * @param responseData service discovery response data based on the requested
+         *  service protocol type. The format depends on the service type.
+         * @param srcDevice source device.
+         */
+        public void onServiceAvailable(int protocolType,
+                byte[] responseData, WifiP2pDevice srcDevice);
+    }
+
+    /**
+     * Interface for callback invocation when Bonjour service discovery response
+     * is received
+     */
+    public interface DnsSdServiceResponseListener {
+
+        /**
+         * The requested Bonjour service response is available.
+         *
+         * <p>This function is invoked when the device with the specified Bonjour
+         * registration type returned the instance name.
+         * @param instanceName instance name.<br>
+         *  e.g) "MyPrinter".
+         * @param registrationType <br>
+         * e.g) "_ipp._tcp.local."
+         * @param srcDevice source device.
+         */
+        public void onDnsSdServiceAvailable(String instanceName,
+                String registrationType, WifiP2pDevice srcDevice);
+
+   }
+
+    /**
+     * Interface for callback invocation when Bonjour TXT record is available
+     * for a service
+     */
+   public interface DnsSdTxtRecordListener {
+        /**
+         * The requested Bonjour service response is available.
+         *
+         * <p>This function is invoked when the device with the specified full
+         * service domain service returned TXT record.
+         *
+         * @param fullDomainName full domain name. <br>
+         * e.g) "MyPrinter._ipp._tcp.local.".
+         * @param txtRecordMap TXT record data as a map of key/value pairs
+         * @param srcDevice source device.
+         */
+        public void onDnsSdTxtRecordAvailable(String fullDomainName,
+                Map<String, String> txtRecordMap,
+                WifiP2pDevice srcDevice);
+   }
+
+    /**
+     * Interface for callback invocation when upnp service discovery response
+     * is received
+     * */
+    public interface UpnpServiceResponseListener {
+
+        /**
+         * The requested upnp service response is available.
+         *
+         * <p>This function is invoked when the specified device or service is found.
+         *
+         * @param uniqueServiceNames The list of unique service names.<br>
+         * e.g) uuid:6859dede-8574-59ab-9332-123456789012::urn:schemas-upnp-org:device:
+         * MediaServer:1
+         * @param srcDevice source device.
+         */
+        public void onUpnpServiceAvailable(List<String> uniqueServiceNames,
+                WifiP2pDevice srcDevice);
+    }
+
+
+    /** Interface for callback invocation when stored group info list is available {@hide}*/
+    public interface PersistentGroupInfoListener {
+        /**
+         * The requested stored p2p group info list is available
+         * @param groups Wi-Fi p2p group info list
+         */
+        public void onPersistentGroupInfoAvailable(WifiP2pGroupList groups);
+    }
+
+    /**
+     * Interface for callback invocation when Handover Request or Select Message is available
+     * @hide
+     */
+    public interface HandoverMessageListener {
+        public void onHandoverMessageAvailable(String handoverMessage);
+    }
+
+    /** Interface for callback invocation when p2p state is available
+     *  in response to {@link #requestP2pState}.
+     */
+    public interface P2pStateListener {
+        /**
+         * The requested p2p state is available.
+         * @param state Wi-Fi p2p state
+         *        @see #WIFI_P2P_STATE_DISABLED
+         *        @see #WIFI_P2P_STATE_ENABLED
+         */
+        void onP2pStateAvailable(@WifiP2pState int state);
+    }
+
+    /** Interface for callback invocation when p2p state is available
+     *  in response to {@link #requestDiscoveryState}.
+     */
+    public interface DiscoveryStateListener {
+        /**
+         * The requested p2p discovery state is available.
+         * @param state Wi-Fi p2p discovery state
+         *        @see #WIFI_P2P_DISCOVERY_STARTED
+         *        @see #WIFI_P2P_DISCOVERY_STOPPED
+         */
+        void onDiscoveryStateAvailable(@WifiP2pDiscoveryState int state);
+    }
+
+    /** Interface for callback invocation when {@link android.net.NetworkInfo} is available
+     *  in response to {@link #requestNetworkInfo}.
+     */
+    public interface NetworkInfoListener {
+        /**
+         * The requested {@link android.net.NetworkInfo} is available
+         * @param networkInfo Wi-Fi p2p {@link android.net.NetworkInfo}
+         */
+        void onNetworkInfoAvailable(@NonNull NetworkInfo networkInfo);
+    }
+
+    /**
+     * Interface for callback invocation when ongoing peer info is available
+     * @hide
+     */
+    public interface OngoingPeerInfoListener {
+        /**
+         * The requested ongoing WifiP2pConfig is available
+         * @param peerConfig WifiP2pConfig for current connecting session
+         */
+        void onOngoingPeerAvailable(WifiP2pConfig peerConfig);
+    }
+
+    /** Interface for callback invocation when {@link android.net.wifi.p2p.WifiP2pDevice}
+     *  is available in response to {@link #requestDeviceInfo(Channel, DeviceInfoListener)}.
+     */
+    public interface DeviceInfoListener {
+        /**
+         * The requested {@link android.net.wifi.p2p.WifiP2pDevice} is available.
+         * @param wifiP2pDevice Wi-Fi p2p {@link android.net.wifi.p2p.WifiP2pDevice}
+         */
+        void onDeviceInfoAvailable(@Nullable WifiP2pDevice wifiP2pDevice);
+    }
+
+    /**
+     * A channel that connects the application to the Wifi p2p framework.
+     * Most p2p operations require a Channel as an argument. An instance of Channel is obtained
+     * by doing a call on {@link #initialize}
+     */
+    public static class Channel implements AutoCloseable {
+        /** @hide */
+        public Channel(Context context, Looper looper, ChannelListener l, Binder binder,
+                WifiP2pManager p2pManager) {
+            mAsyncChannel = new AsyncChannel();
+            mHandler = new P2pHandler(looper);
+            mChannelListener = l;
+            mContext = context;
+            mBinder = binder;
+            mP2pManager = p2pManager;
+
+            mCloseGuard.open("close");
+        }
+        private final static int INVALID_LISTENER_KEY = 0;
+        private final WifiP2pManager mP2pManager;
+        private ChannelListener mChannelListener;
+        private ServiceResponseListener mServRspListener;
+        private DnsSdServiceResponseListener mDnsSdServRspListener;
+        private DnsSdTxtRecordListener mDnsSdTxtListener;
+        private UpnpServiceResponseListener mUpnpServRspListener;
+        private HashMap<Integer, Object> mListenerMap = new HashMap<Integer, Object>();
+        private final Object mListenerMapLock = new Object();
+        private int mListenerKey = 0;
+
+        private final CloseGuard mCloseGuard = CloseGuard.get();
+
+        /**
+         * Close the current P2P connection and indicate to the P2P service that connections
+         * created by the app can be removed.
+         */
+        public void close() {
+            if (mP2pManager == null) {
+                Log.w(TAG, "Channel.close(): Null mP2pManager!?");
+            } else {
+                try {
+                    mP2pManager.mService.close(mBinder);
+                } catch (RemoteException e) {
+                    throw e.rethrowFromSystemServer();
+                }
+            }
+
+            mAsyncChannel.disconnect();
+            mCloseGuard.close();
+        }
+
+        /** @hide */
+        @Override
+        protected void finalize() throws Throwable {
+            try {
+                if (mCloseGuard != null) {
+                    mCloseGuard.warnIfOpen();
+                }
+
+                close();
+            } finally {
+                super.finalize();
+            }
+        }
+
+        /* package */ final Binder mBinder;
+
+        @UnsupportedAppUsage
+        private AsyncChannel mAsyncChannel;
+        private P2pHandler mHandler;
+        Context mContext;
+        class P2pHandler extends Handler {
+            P2pHandler(Looper looper) {
+                super(looper);
+            }
+
+            @Override
+            public void handleMessage(Message message) {
+                Object listener = getListener(message.arg2);
+                switch (message.what) {
+                    case AsyncChannel.CMD_CHANNEL_DISCONNECTED:
+                        if (mChannelListener != null) {
+                            mChannelListener.onChannelDisconnected();
+                            mChannelListener = null;
+                        }
+                        break;
+                    /* ActionListeners grouped together */
+                    case DISCOVER_PEERS_FAILED:
+                    case STOP_DISCOVERY_FAILED:
+                    case DISCOVER_SERVICES_FAILED:
+                    case CONNECT_FAILED:
+                    case CANCEL_CONNECT_FAILED:
+                    case CREATE_GROUP_FAILED:
+                    case REMOVE_GROUP_FAILED:
+                    case ADD_LOCAL_SERVICE_FAILED:
+                    case REMOVE_LOCAL_SERVICE_FAILED:
+                    case CLEAR_LOCAL_SERVICES_FAILED:
+                    case ADD_SERVICE_REQUEST_FAILED:
+                    case REMOVE_SERVICE_REQUEST_FAILED:
+                    case CLEAR_SERVICE_REQUESTS_FAILED:
+                    case SET_DEVICE_NAME_FAILED:
+                    case DELETE_PERSISTENT_GROUP_FAILED:
+                    case SET_WFD_INFO_FAILED:
+                    case START_WPS_FAILED:
+                    case START_LISTEN_FAILED:
+                    case STOP_LISTEN_FAILED:
+                    case SET_CHANNEL_FAILED:
+                    case REPORT_NFC_HANDOVER_FAILED:
+                    case FACTORY_RESET_FAILED:
+                    case SET_ONGOING_PEER_CONFIG_FAILED:
+                        if (listener != null) {
+                            ((ActionListener) listener).onFailure(message.arg1);
+                        }
+                        break;
+                    /* ActionListeners grouped together */
+                    case DISCOVER_PEERS_SUCCEEDED:
+                    case STOP_DISCOVERY_SUCCEEDED:
+                    case DISCOVER_SERVICES_SUCCEEDED:
+                    case CONNECT_SUCCEEDED:
+                    case CANCEL_CONNECT_SUCCEEDED:
+                    case CREATE_GROUP_SUCCEEDED:
+                    case REMOVE_GROUP_SUCCEEDED:
+                    case ADD_LOCAL_SERVICE_SUCCEEDED:
+                    case REMOVE_LOCAL_SERVICE_SUCCEEDED:
+                    case CLEAR_LOCAL_SERVICES_SUCCEEDED:
+                    case ADD_SERVICE_REQUEST_SUCCEEDED:
+                    case REMOVE_SERVICE_REQUEST_SUCCEEDED:
+                    case CLEAR_SERVICE_REQUESTS_SUCCEEDED:
+                    case SET_DEVICE_NAME_SUCCEEDED:
+                    case DELETE_PERSISTENT_GROUP_SUCCEEDED:
+                    case SET_WFD_INFO_SUCCEEDED:
+                    case START_WPS_SUCCEEDED:
+                    case START_LISTEN_SUCCEEDED:
+                    case STOP_LISTEN_SUCCEEDED:
+                    case SET_CHANNEL_SUCCEEDED:
+                    case REPORT_NFC_HANDOVER_SUCCEEDED:
+                    case FACTORY_RESET_SUCCEEDED:
+                    case SET_ONGOING_PEER_CONFIG_SUCCEEDED:
+                        if (listener != null) {
+                            ((ActionListener) listener).onSuccess();
+                        }
+                        break;
+                    case RESPONSE_PEERS:
+                        WifiP2pDeviceList peers = (WifiP2pDeviceList) message.obj;
+                        if (listener != null) {
+                            ((PeerListListener) listener).onPeersAvailable(peers);
+                        }
+                        break;
+                    case RESPONSE_CONNECTION_INFO:
+                        WifiP2pInfo wifiP2pInfo = (WifiP2pInfo) message.obj;
+                        if (listener != null) {
+                            ((ConnectionInfoListener) listener).onConnectionInfoAvailable(wifiP2pInfo);
+                        }
+                        break;
+                    case RESPONSE_GROUP_INFO:
+                        WifiP2pGroup group = (WifiP2pGroup) message.obj;
+                        if (listener != null) {
+                            ((GroupInfoListener) listener).onGroupInfoAvailable(group);
+                        }
+                        break;
+                    case RESPONSE_SERVICE:
+                        WifiP2pServiceResponse resp = (WifiP2pServiceResponse) message.obj;
+                        handleServiceResponse(resp);
+                        break;
+                    case RESPONSE_PERSISTENT_GROUP_INFO:
+                        WifiP2pGroupList groups = (WifiP2pGroupList) message.obj;
+                        if (listener != null) {
+                            ((PersistentGroupInfoListener) listener).
+                                onPersistentGroupInfoAvailable(groups);
+                        }
+                        break;
+                    case RESPONSE_GET_HANDOVER_MESSAGE:
+                        Bundle handoverBundle = (Bundle) message.obj;
+                        if (listener != null) {
+                            String handoverMessage = handoverBundle != null
+                                    ? handoverBundle.getString(EXTRA_HANDOVER_MESSAGE)
+                                    : null;
+                            ((HandoverMessageListener) listener)
+                                    .onHandoverMessageAvailable(handoverMessage);
+                        }
+                        break;
+                    case RESPONSE_ONGOING_PEER_CONFIG:
+                        WifiP2pConfig peerConfig = (WifiP2pConfig) message.obj;
+                        if (listener != null) {
+                            ((OngoingPeerInfoListener) listener)
+                                    .onOngoingPeerAvailable(peerConfig);
+                        }
+                        break;
+                    case RESPONSE_P2P_STATE:
+                        if (listener != null) {
+                            ((P2pStateListener) listener)
+                                    .onP2pStateAvailable(message.arg1);
+                        }
+                        break;
+                    case RESPONSE_DISCOVERY_STATE:
+                        if (listener != null) {
+                            ((DiscoveryStateListener) listener)
+                                    .onDiscoveryStateAvailable(message.arg1);
+                        }
+                        break;
+                    case RESPONSE_NETWORK_INFO:
+                        if (listener != null) {
+                            ((NetworkInfoListener) listener)
+                                    .onNetworkInfoAvailable((NetworkInfo) message.obj);
+                        }
+                        break;
+                    case RESPONSE_DEVICE_INFO:
+                        if (listener != null) {
+                            ((DeviceInfoListener) listener)
+                                    .onDeviceInfoAvailable((WifiP2pDevice) message.obj);
+                        }
+                        break;
+                    default:
+                        Log.d(TAG, "Ignored " + message);
+                        break;
+                }
+            }
+        }
+
+        private void handleServiceResponse(WifiP2pServiceResponse resp) {
+            if (resp instanceof WifiP2pDnsSdServiceResponse) {
+                handleDnsSdServiceResponse((WifiP2pDnsSdServiceResponse)resp);
+            } else if (resp instanceof WifiP2pUpnpServiceResponse) {
+                if (mUpnpServRspListener != null) {
+                    handleUpnpServiceResponse((WifiP2pUpnpServiceResponse)resp);
+                }
+            } else {
+                if (mServRspListener != null) {
+                    mServRspListener.onServiceAvailable(resp.getServiceType(),
+                            resp.getRawData(), resp.getSrcDevice());
+                }
+            }
+        }
+
+        private void handleUpnpServiceResponse(WifiP2pUpnpServiceResponse resp) {
+            mUpnpServRspListener.onUpnpServiceAvailable(resp.getUniqueServiceNames(),
+                    resp.getSrcDevice());
+        }
+
+        private void handleDnsSdServiceResponse(WifiP2pDnsSdServiceResponse resp) {
+            if (resp.getDnsType() == WifiP2pDnsSdServiceInfo.DNS_TYPE_PTR) {
+                if (mDnsSdServRspListener != null) {
+                    mDnsSdServRspListener.onDnsSdServiceAvailable(
+                            resp.getInstanceName(),
+                            resp.getDnsQueryName(),
+                            resp.getSrcDevice());
+                }
+            } else if (resp.getDnsType() == WifiP2pDnsSdServiceInfo.DNS_TYPE_TXT) {
+                if (mDnsSdTxtListener != null) {
+                    mDnsSdTxtListener.onDnsSdTxtRecordAvailable(
+                            resp.getDnsQueryName(),
+                            resp.getTxtRecord(),
+                            resp.getSrcDevice());
+                }
+            } else {
+                Log.e(TAG, "Unhandled resp " + resp);
+            }
+        }
+
+        @UnsupportedAppUsage
+        private int putListener(Object listener) {
+            if (listener == null) return INVALID_LISTENER_KEY;
+            int key;
+            synchronized (mListenerMapLock) {
+                do {
+                    key = mListenerKey++;
+                } while (key == INVALID_LISTENER_KEY);
+                mListenerMap.put(key, listener);
+            }
+            return key;
+        }
+
+        private Object getListener(int key) {
+            if (key == INVALID_LISTENER_KEY) return null;
+            synchronized (mListenerMapLock) {
+                return mListenerMap.remove(key);
+            }
+        }
+    }
+
+    private static void checkChannel(Channel c) {
+        if (c == null) throw new IllegalArgumentException("Channel needs to be initialized");
+    }
+
+    private static void checkServiceInfo(WifiP2pServiceInfo info) {
+        if (info == null) throw new IllegalArgumentException("service info is null");
+    }
+
+    private static void checkServiceRequest(WifiP2pServiceRequest req) {
+        if (req == null) throw new IllegalArgumentException("service request is null");
+    }
+
+    private static void checkP2pConfig(WifiP2pConfig c) {
+        if (c == null) throw new IllegalArgumentException("config cannot be null");
+        if (TextUtils.isEmpty(c.deviceAddress)) {
+            throw new IllegalArgumentException("deviceAddress cannot be empty");
+        }
+    }
+
+    /**
+     * Registers the application with the Wi-Fi framework. This function
+     * must be the first to be called before any p2p operations are performed.
+     *
+     * @param srcContext is the context of the source
+     * @param srcLooper is the Looper on which the callbacks are receivied
+     * @param listener for callback at loss of framework communication. Can be null.
+     * @return Channel instance that is necessary for performing any further p2p operations
+     */
+    public Channel initialize(Context srcContext, Looper srcLooper, ChannelListener listener) {
+        Binder binder = new Binder();
+        Channel channel = initalizeChannel(srcContext, srcLooper, listener, getMessenger(binder),
+                binder);
+        return channel;
+    }
+
+    /**
+     * Registers the application with the Wi-Fi framework. Enables system-only functionality.
+     * @hide
+     */
+    public Channel initializeInternal(Context srcContext, Looper srcLooper,
+                                      ChannelListener listener) {
+        return initalizeChannel(srcContext, srcLooper, listener, getP2pStateMachineMessenger(),
+                null);
+    }
+
+    private Channel initalizeChannel(Context srcContext, Looper srcLooper, ChannelListener listener,
+                                     Messenger messenger, Binder binder) {
+        if (messenger == null) return null;
+
+        Channel c = new Channel(srcContext, srcLooper, listener, binder, this);
+        if (c.mAsyncChannel.connectSync(srcContext, c.mHandler, messenger)
+                == AsyncChannel.STATUS_SUCCESSFUL) {
+            Bundle bundle = new Bundle();
+            bundle.putString(CALLING_PACKAGE, c.mContext.getOpPackageName());
+            bundle.putBinder(CALLING_BINDER, binder);
+            c.mAsyncChannel.sendMessage(UPDATE_CHANNEL_INFO, 0,
+                    c.putListener(null), bundle);
+            return c;
+        } else {
+            c.close();
+            return null;
+        }
+    }
+
+    /**
+     * Initiate peer discovery. A discovery process involves scanning for available Wi-Fi peers
+     * for the purpose of establishing a connection.
+     *
+     * <p> The function call immediately returns after sending a discovery request
+     * to the framework. The application is notified of a success or failure to initiate
+     * discovery through listener callbacks {@link ActionListener#onSuccess} or
+     * {@link ActionListener#onFailure}.
+     *
+     * <p> The discovery remains active until a connection is initiated or
+     * a p2p group is formed. Register for {@link #WIFI_P2P_PEERS_CHANGED_ACTION} intent to
+     * determine when the framework notifies of a change as peers are discovered.
+     *
+     * <p> Upon receiving a {@link #WIFI_P2P_PEERS_CHANGED_ACTION} intent, an application
+     * can request for the list of peers using {@link #requestPeers}.
+     *
+     * @param c is the channel created at {@link #initialize}
+     * @param listener for callbacks on success or failure. Can be null.
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_FINE_LOCATION)
+    public void discoverPeers(Channel c, ActionListener listener) {
+        checkChannel(c);
+        c.mAsyncChannel.sendMessage(DISCOVER_PEERS, 0, c.putListener(listener));
+    }
+
+   /**
+     * Stop an ongoing peer discovery
+     *
+     * <p> The function call immediately returns after sending a stop request
+     * to the framework. The application is notified of a success or failure to initiate
+     * stop through listener callbacks {@link ActionListener#onSuccess} or
+     * {@link ActionListener#onFailure}.
+     *
+     * @param c is the channel created at {@link #initialize}
+     * @param listener for callbacks on success or failure. Can be null.
+     */
+    public void stopPeerDiscovery(Channel c, ActionListener listener) {
+        checkChannel(c);
+        c.mAsyncChannel.sendMessage(STOP_DISCOVERY, 0, c.putListener(listener));
+    }
+
+    /**
+     * Start a p2p connection to a device with the specified configuration.
+     *
+     * <p> The function call immediately returns after sending a connection request
+     * to the framework. The application is notified of a success or failure to initiate
+     * connect through listener callbacks {@link ActionListener#onSuccess} or
+     * {@link ActionListener#onFailure}.
+     *
+     * <p> Register for {@link #WIFI_P2P_CONNECTION_CHANGED_ACTION} intent to
+     * determine when the framework notifies of a change in connectivity.
+     *
+     * <p> If the current device is not part of a p2p group, a connect request initiates
+     * a group negotiation with the peer.
+     *
+     * <p> If the current device is part of an existing p2p group or has created
+     * a p2p group with {@link #createGroup}, an invitation to join the group is sent to
+     * the peer device.
+     *
+     * @param c is the channel created at {@link #initialize}
+     * @param config options as described in {@link WifiP2pConfig} class
+     * @param listener for callbacks on success or failure. Can be null.
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_FINE_LOCATION)
+    public void connect(Channel c, WifiP2pConfig config, ActionListener listener) {
+        checkChannel(c);
+        checkP2pConfig(config);
+        c.mAsyncChannel.sendMessage(CONNECT, 0, c.putListener(listener), config);
+    }
+
+    /**
+     * Cancel any ongoing p2p group negotiation
+     *
+     * <p> The function call immediately returns after sending a connection cancellation request
+     * to the framework. The application is notified of a success or failure to initiate
+     * cancellation through listener callbacks {@link ActionListener#onSuccess} or
+     * {@link ActionListener#onFailure}.
+     *
+     * @param c is the channel created at {@link #initialize}
+     * @param listener for callbacks on success or failure. Can be null.
+     */
+    public void cancelConnect(Channel c, ActionListener listener) {
+        checkChannel(c);
+        c.mAsyncChannel.sendMessage(CANCEL_CONNECT, 0, c.putListener(listener));
+    }
+
+    /**
+     * Create a p2p group with the current device as the group owner. This essentially creates
+     * an access point that can accept connections from legacy clients as well as other p2p
+     * devices.
+     *
+     * <p class="note"><strong>Note:</strong>
+     * This function would normally not be used unless the current device needs
+     * to form a p2p connection with a legacy client
+     *
+     * <p> The function call immediately returns after sending a group creation request
+     * to the framework. The application is notified of a success or failure to initiate
+     * group creation through listener callbacks {@link ActionListener#onSuccess} or
+     * {@link ActionListener#onFailure}.
+     *
+     * <p> Application can request for the group details with {@link #requestGroupInfo}.
+     *
+     * @param c is the channel created at {@link #initialize}
+     * @param listener for callbacks on success or failure. Can be null.
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_FINE_LOCATION)
+    public void createGroup(Channel c, ActionListener listener) {
+        checkChannel(c);
+        c.mAsyncChannel.sendMessage(CREATE_GROUP, WifiP2pGroup.PERSISTENT_NET_ID,
+                c.putListener(listener));
+    }
+
+    /**
+     * Create a p2p group with the current device as the group owner. This essentially creates
+     * an access point that can accept connections from legacy clients as well as other p2p
+     * devices.
+     *
+     * <p> An app should use {@link WifiP2pConfig.Builder} to build the configuration
+     * for a group.
+     *
+     * <p class="note"><strong>Note:</strong>
+     * This function would normally not be used unless the current device needs
+     * to form a p2p group as a Group Owner and allow peers to join it as either
+     * Group Clients or legacy Wi-Fi STAs.
+     *
+     * <p> The function call immediately returns after sending a group creation request
+     * to the framework. The application is notified of a success or failure to initiate
+     * group creation through listener callbacks {@link ActionListener#onSuccess} or
+     * {@link ActionListener#onFailure}.
+     *
+     * <p> Application can request for the group details with {@link #requestGroupInfo}.
+     *
+     * @param c is the channel created at {@link #initialize}.
+     * @param config the configuration of a p2p group.
+     * @param listener for callbacks on success or failure. Can be null.
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_FINE_LOCATION)
+    public void createGroup(@NonNull Channel c,
+            @Nullable WifiP2pConfig config,
+            @Nullable ActionListener listener) {
+        checkChannel(c);
+        c.mAsyncChannel.sendMessage(CREATE_GROUP, 0,
+                c.putListener(listener), config);
+    }
+
+    /**
+     * Remove the current p2p group.
+     *
+     * <p> The function call immediately returns after sending a group removal request
+     * to the framework. The application is notified of a success or failure to initiate
+     * group removal through listener callbacks {@link ActionListener#onSuccess} or
+     * {@link ActionListener#onFailure}.
+     *
+     * @param c is the channel created at {@link #initialize}
+     * @param listener for callbacks on success or failure. Can be null.
+     */
+    public void removeGroup(Channel c, ActionListener listener) {
+        checkChannel(c);
+        c.mAsyncChannel.sendMessage(REMOVE_GROUP, 0, c.putListener(listener));
+    }
+
+    /**
+     * Force p2p to enter or exit listen state
+     *
+     * @param c is the channel created at {@link #initialize(Context, Looper, ChannelListener)}
+     * @param enable enables or disables listening
+     * @param listener for callbacks on success or failure. Can be null.
+     *
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.NETWORK_SETTINGS)
+    public void listen(Channel c, boolean enable, ActionListener listener) {
+        checkChannel(c);
+        c.mAsyncChannel.sendMessage(enable ? START_LISTEN : STOP_LISTEN,
+                0, c.putListener(listener));
+    }
+
+    /** @hide */
+    @UnsupportedAppUsage
+    public void setWifiP2pChannels(Channel c, int lc, int oc, ActionListener listener) {
+        checkChannel(c);
+        Bundle p2pChannels = new Bundle();
+        p2pChannels.putInt("lc", lc);
+        p2pChannels.putInt("oc", oc);
+        c.mAsyncChannel.sendMessage(SET_CHANNEL, 0, c.putListener(listener), p2pChannels);
+    }
+
+    /**
+     * Start a Wi-Fi Protected Setup (WPS) session.
+     *
+     * <p> The function call immediately returns after sending a request to start a
+     * WPS session. Currently, this is only valid if the current device is running
+     * as a group owner to allow any new clients to join the group. The application
+     * is notified of a success or failure to initiate WPS through listener callbacks
+     * {@link ActionListener#onSuccess} or {@link ActionListener#onFailure}.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public void startWps(Channel c, WpsInfo wps, ActionListener listener) {
+        checkChannel(c);
+        c.mAsyncChannel.sendMessage(START_WPS, 0, c.putListener(listener), wps);
+    }
+
+    /**
+     * Register a local service for service discovery. If a local service is registered,
+     * the framework automatically responds to a service discovery request from a peer.
+     *
+     * <p> The function call immediately returns after sending a request to add a local
+     * service to the framework. The application is notified of a success or failure to
+     * add service through listener callbacks {@link ActionListener#onSuccess} or
+     * {@link ActionListener#onFailure}.
+     *
+     * <p>The service information is set through {@link WifiP2pServiceInfo}.<br>
+     * or its subclass calls  {@link WifiP2pUpnpServiceInfo#newInstance} or
+     *  {@link WifiP2pDnsSdServiceInfo#newInstance} for a Upnp or Bonjour service
+     * respectively
+     *
+     * <p>The service information can be cleared with calls to
+     *  {@link #removeLocalService} or {@link #clearLocalServices}.
+     *
+     * @param c is the channel created at {@link #initialize}
+     * @param servInfo is a local service information.
+     * @param listener for callbacks on success or failure. Can be null.
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_FINE_LOCATION)
+    public void addLocalService(Channel c, WifiP2pServiceInfo servInfo, ActionListener listener) {
+        checkChannel(c);
+        checkServiceInfo(servInfo);
+        c.mAsyncChannel.sendMessage(ADD_LOCAL_SERVICE, 0, c.putListener(listener), servInfo);
+    }
+
+    /**
+     * Remove a registered local service added with {@link #addLocalService}
+     *
+     * <p> The function call immediately returns after sending a request to remove a
+     * local service to the framework. The application is notified of a success or failure to
+     * add service through listener callbacks {@link ActionListener#onSuccess} or
+     * {@link ActionListener#onFailure}.
+     *
+     * @param c is the channel created at {@link #initialize}
+     * @param servInfo is the local service information.
+     * @param listener for callbacks on success or failure. Can be null.
+     */
+    public void removeLocalService(Channel c, WifiP2pServiceInfo servInfo,
+            ActionListener listener) {
+        checkChannel(c);
+        checkServiceInfo(servInfo);
+        c.mAsyncChannel.sendMessage(REMOVE_LOCAL_SERVICE, 0, c.putListener(listener), servInfo);
+    }
+
+    /**
+     * Clear all registered local services of service discovery.
+     *
+     * <p> The function call immediately returns after sending a request to clear all
+     * local services to the framework. The application is notified of a success or failure to
+     * add service through listener callbacks {@link ActionListener#onSuccess} or
+     * {@link ActionListener#onFailure}.
+     *
+     * @param c is the channel created at {@link #initialize}
+     * @param listener for callbacks on success or failure. Can be null.
+     */
+    public void clearLocalServices(Channel c, ActionListener listener) {
+        checkChannel(c);
+        c.mAsyncChannel.sendMessage(CLEAR_LOCAL_SERVICES, 0, c.putListener(listener));
+    }
+
+    /**
+     * Register a callback to be invoked on receiving service discovery response.
+     * Used only for vendor specific protocol right now. For Bonjour or Upnp, use
+     * {@link #setDnsSdResponseListeners} or {@link #setUpnpServiceResponseListener}
+     * respectively.
+     *
+     * <p> see {@link #discoverServices} for the detail.
+     *
+     * @param c is the channel created at {@link #initialize}
+     * @param listener for callbacks on receiving service discovery response.
+     */
+    public void setServiceResponseListener(Channel c,
+            ServiceResponseListener listener) {
+        checkChannel(c);
+        c.mServRspListener = listener;
+    }
+
+    /**
+     * Register a callback to be invoked on receiving Bonjour service discovery
+     * response.
+     *
+     * <p> see {@link #discoverServices} for the detail.
+     *
+     * @param c
+     * @param servListener is for listening to a Bonjour service response
+     * @param txtListener is for listening to a Bonjour TXT record response
+     */
+    public void setDnsSdResponseListeners(Channel c,
+            DnsSdServiceResponseListener servListener, DnsSdTxtRecordListener txtListener) {
+        checkChannel(c);
+        c.mDnsSdServRspListener = servListener;
+        c.mDnsSdTxtListener = txtListener;
+    }
+
+    /**
+     * Register a callback to be invoked on receiving upnp service discovery
+     * response.
+     *
+     * <p> see {@link #discoverServices} for the detail.
+     *
+     * @param c is the channel created at {@link #initialize}
+     * @param listener for callbacks on receiving service discovery response.
+     */
+    public void setUpnpServiceResponseListener(Channel c,
+            UpnpServiceResponseListener listener) {
+        checkChannel(c);
+        c.mUpnpServRspListener = listener;
+    }
+
+    /**
+     * Initiate service discovery. A discovery process involves scanning for
+     * requested services for the purpose of establishing a connection to a peer
+     * that supports an available service.
+     *
+     * <p> The function call immediately returns after sending a request to start service
+     * discovery to the framework. The application is notified of a success or failure to initiate
+     * discovery through listener callbacks {@link ActionListener#onSuccess} or
+     * {@link ActionListener#onFailure}.
+     *
+     * <p> The services to be discovered are specified with calls to {@link #addServiceRequest}.
+     *
+     * <p>The application is notified of the response against the service discovery request
+     * through listener callbacks registered by {@link #setServiceResponseListener} or
+     * {@link #setDnsSdResponseListeners}, or {@link #setUpnpServiceResponseListener}.
+     *
+     * @param c is the channel created at {@link #initialize}
+     * @param listener for callbacks on success or failure. Can be null.
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_FINE_LOCATION)
+    public void discoverServices(Channel c, ActionListener listener) {
+        checkChannel(c);
+        c.mAsyncChannel.sendMessage(DISCOVER_SERVICES, 0, c.putListener(listener));
+    }
+
+    /**
+     * Add a service discovery request.
+     *
+     * <p> The function call immediately returns after sending a request to add service
+     * discovery request to the framework. The application is notified of a success or failure to
+     * add service through listener callbacks {@link ActionListener#onSuccess} or
+     * {@link ActionListener#onFailure}.
+     *
+     * <p>After service discovery request is added, you can initiate service discovery by
+     * {@link #discoverServices}.
+     *
+     * <p>The added service requests can be cleared with calls to
+     * {@link #removeServiceRequest(Channel, WifiP2pServiceRequest, ActionListener)} or
+     * {@link #clearServiceRequests(Channel, ActionListener)}.
+     *
+     * @param c is the channel created at {@link #initialize}
+     * @param req is the service discovery request.
+     * @param listener for callbacks on success or failure. Can be null.
+     */
+    public void addServiceRequest(Channel c,
+            WifiP2pServiceRequest req, ActionListener listener) {
+        checkChannel(c);
+        checkServiceRequest(req);
+        c.mAsyncChannel.sendMessage(ADD_SERVICE_REQUEST, 0,
+                c.putListener(listener), req);
+    }
+
+    /**
+     * Remove a specified service discovery request added with {@link #addServiceRequest}
+     *
+     * <p> The function call immediately returns after sending a request to remove service
+     * discovery request to the framework. The application is notified of a success or failure to
+     * add service through listener callbacks {@link ActionListener#onSuccess} or
+     * {@link ActionListener#onFailure}.
+     *
+     * @param c is the channel created at {@link #initialize}
+     * @param req is the service discovery request.
+     * @param listener for callbacks on success or failure. Can be null.
+     */
+    public void removeServiceRequest(Channel c, WifiP2pServiceRequest req,
+            ActionListener listener) {
+        checkChannel(c);
+        checkServiceRequest(req);
+        c.mAsyncChannel.sendMessage(REMOVE_SERVICE_REQUEST, 0,
+                c.putListener(listener), req);
+    }
+
+    /**
+     * Clear all registered service discovery requests.
+     *
+     * <p> The function call immediately returns after sending a request to clear all
+     * service discovery requests to the framework. The application is notified of a success
+     * or failure to add service through listener callbacks {@link ActionListener#onSuccess} or
+     * {@link ActionListener#onFailure}.
+     *
+     * @param c is the channel created at {@link #initialize}
+     * @param listener for callbacks on success or failure. Can be null.
+     */
+    public void clearServiceRequests(Channel c, ActionListener listener) {
+        checkChannel(c);
+        c.mAsyncChannel.sendMessage(CLEAR_SERVICE_REQUESTS,
+                0, c.putListener(listener));
+    }
+
+    /**
+     * Request the current list of peers.
+     *
+     * @param c is the channel created at {@link #initialize}
+     * @param listener for callback when peer list is available. Can be null.
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_FINE_LOCATION)
+    public void requestPeers(Channel c, PeerListListener listener) {
+        checkChannel(c);
+        c.mAsyncChannel.sendMessage(REQUEST_PEERS, 0, c.putListener(listener));
+    }
+
+    /**
+     * Request device connection info.
+     *
+     * @param c is the channel created at {@link #initialize}
+     * @param listener for callback when connection info is available. Can be null.
+     */
+    public void requestConnectionInfo(Channel c, ConnectionInfoListener listener) {
+        checkChannel(c);
+        c.mAsyncChannel.sendMessage(REQUEST_CONNECTION_INFO, 0, c.putListener(listener));
+    }
+
+    /**
+     * Request p2p group info.
+     *
+     * @param c is the channel created at {@link #initialize}
+     * @param listener for callback when group info is available. Can be null.
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_FINE_LOCATION)
+    public void requestGroupInfo(Channel c, GroupInfoListener listener) {
+        checkChannel(c);
+        c.mAsyncChannel.sendMessage(REQUEST_GROUP_INFO, 0, c.putListener(listener));
+    }
+
+    /**
+     * Set p2p device name.
+     * @hide
+     * @param c is the channel created at {@link #initialize}
+     * @param listener for callback when group info is available. Can be null.
+     */
+    @UnsupportedAppUsage
+    public void setDeviceName(Channel c, String devName, ActionListener listener) {
+        checkChannel(c);
+        WifiP2pDevice d = new WifiP2pDevice();
+        d.deviceName = devName;
+        c.mAsyncChannel.sendMessage(SET_DEVICE_NAME, 0, c.putListener(listener), d);
+    }
+
+    /** @hide */
+    @UnsupportedAppUsage
+    public void setWFDInfo(
+            Channel c, WifiP2pWfdInfo wfdInfo,
+            ActionListener listener) {
+        checkChannel(c);
+        try {
+            mService.checkConfigureWifiDisplayPermission();
+        } catch (RemoteException e) {
+            e.rethrowFromSystemServer();
+        }
+        c.mAsyncChannel.sendMessage(SET_WFD_INFO, 0, c.putListener(listener), wfdInfo);
+    }
+
+
+    /**
+     * Delete a stored persistent group from the system settings.
+     *
+     * <p> The function call immediately returns after sending a persistent group removal request
+     * to the framework. The application is notified of a success or failure to initiate
+     * group removal through listener callbacks {@link ActionListener#onSuccess} or
+     * {@link ActionListener#onFailure}.
+     *
+     * <p>The persistent p2p group list stored in the system can be obtained by
+     * {@link #requestPersistentGroupInfo(Channel, PersistentGroupInfoListener)} and
+     *  a network id can be obtained by {@link WifiP2pGroup#getNetworkId()}.
+     *
+     * @param c is the channel created at {@link #initialize}
+     * @param netId he network id of the p2p group.
+     * @param listener for callbacks on success or failure. Can be null.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public void deletePersistentGroup(Channel c, int netId, ActionListener listener) {
+        checkChannel(c);
+        c.mAsyncChannel.sendMessage(DELETE_PERSISTENT_GROUP, netId, c.putListener(listener));
+    }
+
+    /**
+     * Request a list of all the persistent p2p groups stored in system.
+     *
+     * @param c is the channel created at {@link #initialize}
+     * @param listener for callback when persistent group info list is available. Can be null.
+     * @hide
+     */
+    @UnsupportedAppUsage
+    public void requestPersistentGroupInfo(Channel c, PersistentGroupInfoListener listener) {
+        checkChannel(c);
+        c.mAsyncChannel.sendMessage(REQUEST_PERSISTENT_GROUP_INFO, 0, c.putListener(listener));
+    }
+
+    /** @hide */
+    public static final int MIRACAST_DISABLED = 0;
+    /** @hide */
+    public static final int MIRACAST_SOURCE   = 1;
+    /** @hide */
+    public static final int MIRACAST_SINK     = 2;
+    /** Internal use only @hide */
+    @UnsupportedAppUsage
+    public void setMiracastMode(int mode) {
+        try {
+            mService.setMiracastMode(mode);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Get a reference to WifiP2pService handler. This is used to establish
+     * an AsyncChannel communication with WifiService
+     *
+     * @param binder A binder for the service to associate with this client.
+     *
+     * @return Messenger pointing to the WifiP2pService handler
+     * @hide
+     */
+    public Messenger getMessenger(Binder binder) {
+        try {
+            return mService.getMessenger(binder);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Get a reference to P2pStateMachine handler. This is used to establish
+     * a priveleged AsyncChannel communication with WifiP2pService.
+     *
+     * @return Messenger pointing to the WifiP2pService handler
+     * @hide
+     */
+    public Messenger getP2pStateMachineMessenger() {
+        try {
+            return mService.getP2pStateMachineMessenger();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Get a handover request message for use in WFA NFC Handover transfer.
+     * @hide
+     */
+    public void getNfcHandoverRequest(Channel c, HandoverMessageListener listener) {
+        checkChannel(c);
+        c.mAsyncChannel.sendMessage(GET_HANDOVER_REQUEST, 0, c.putListener(listener));
+    }
+
+
+    /**
+     * Get a handover select message for use in WFA NFC Handover transfer.
+     * @hide
+     */
+    public void getNfcHandoverSelect(Channel c, HandoverMessageListener listener) {
+        checkChannel(c);
+        c.mAsyncChannel.sendMessage(GET_HANDOVER_SELECT, 0, c.putListener(listener));
+    }
+
+    /**
+     * @hide
+     */
+    public void initiatorReportNfcHandover(Channel c, String handoverSelect,
+                                              ActionListener listener) {
+        checkChannel(c);
+        Bundle bundle = new Bundle();
+        bundle.putString(EXTRA_HANDOVER_MESSAGE, handoverSelect);
+        c.mAsyncChannel.sendMessage(INITIATOR_REPORT_NFC_HANDOVER, 0,
+                c.putListener(listener), bundle);
+    }
+
+
+    /**
+     * @hide
+     */
+    public void responderReportNfcHandover(Channel c, String handoverRequest,
+                                              ActionListener listener) {
+        checkChannel(c);
+        Bundle bundle = new Bundle();
+        bundle.putString(EXTRA_HANDOVER_MESSAGE, handoverRequest);
+        c.mAsyncChannel.sendMessage(RESPONDER_REPORT_NFC_HANDOVER, 0,
+                c.putListener(listener), bundle);
+    }
+
+    /**
+     * Removes all saved p2p groups.
+     *
+     * @param c is the channel created at {@link #initialize}.
+     * @param listener for callback on success or failure. Can be null.
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.NETWORK_SETTINGS)
+    public void factoryReset(@NonNull Channel c, @Nullable ActionListener listener) {
+        checkChannel(c);
+        c.mAsyncChannel.sendMessage(FACTORY_RESET, 0, c.putListener(listener));
+    }
+
+    /**
+     * Request saved WifiP2pConfig which used for an ongoing peer connection
+     *
+     * @param c is the channel created at {@link #initialize}
+     * @param listener for callback when ongoing peer config updated. Can't be null.
+     *
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.NETWORK_STACK)
+    public void requestOngoingPeerConfig(@NonNull Channel c,
+            @NonNull OngoingPeerInfoListener listener) {
+        checkChannel(c);
+        c.mAsyncChannel.sendMessage(REQUEST_ONGOING_PEER_CONFIG,
+                Binder.getCallingUid(), c.putListener(listener));
+    }
+
+     /**
+     * Set saved WifiP2pConfig which used for an ongoing peer connection
+     *
+     * @param c is the channel created at {@link #initialize}
+     * @param config used for change an ongoing peer connection
+     * @param listener for callback when ongoing peer config updated. Can be null.
+     *
+     * @hide
+     */
+    @RequiresPermission(android.Manifest.permission.NETWORK_STACK)
+    public void setOngoingPeerConfig(@NonNull Channel c, @NonNull WifiP2pConfig config,
+            @Nullable ActionListener listener) {
+        checkChannel(c);
+        checkP2pConfig(config);
+        c.mAsyncChannel.sendMessage(SET_ONGOING_PEER_CONFIG, 0,
+                c.putListener(listener), config);
+    }
+
+    /**
+     * Request p2p enabled state.
+     *
+     * <p> This state indicates whether Wi-Fi p2p is enabled or disabled.
+     * The valid value is one of {@link #WIFI_P2P_STATE_DISABLED} or
+     * {@link #WIFI_P2P_STATE_ENABLED}. The state is returned using the
+     * {@link P2pStateListener} listener.
+     *
+     * <p> This state is also included in the {@link #WIFI_P2P_STATE_CHANGED_ACTION}
+     * broadcast event with extra {@link #EXTRA_WIFI_STATE}.
+     *
+     * @param c is the channel created at {@link #initialize}.
+     * @param listener for callback when p2p state is available..
+     */
+    public void requestP2pState(@NonNull Channel c,
+            @NonNull P2pStateListener listener) {
+        checkChannel(c);
+        if (listener == null) throw new IllegalArgumentException("This listener cannot be null.");
+        c.mAsyncChannel.sendMessage(REQUEST_P2P_STATE, 0, c.putListener(listener));
+    }
+
+    /**
+     * Request p2p discovery state.
+     *
+     * <p> This state indicates whether p2p discovery has started or stopped.
+     * The valid value is one of {@link #WIFI_P2P_DISCOVERY_STARTED} or
+     * {@link #WIFI_P2P_DISCOVERY_STOPPED}. The state is returned using the
+     * {@link DiscoveryStateListener} listener.
+     *
+     * <p> This state is also included in the {@link #WIFI_P2P_DISCOVERY_CHANGED_ACTION}
+     * broadcast event with extra {@link #EXTRA_DISCOVERY_STATE}.
+     *
+     * @param c is the channel created at {@link #initialize}.
+     * @param listener for callback when discovery state is available..
+     */
+    public void requestDiscoveryState(@NonNull Channel c,
+            @NonNull DiscoveryStateListener listener) {
+        checkChannel(c);
+        if (listener == null) throw new IllegalArgumentException("This listener cannot be null.");
+        c.mAsyncChannel.sendMessage(REQUEST_DISCOVERY_STATE, 0, c.putListener(listener));
+    }
+
+    /**
+     * Request network info.
+     *
+     * <p> This method provides the network info in the form of a {@link android.net.NetworkInfo}.
+     * {@link android.net.NetworkInfo#isAvailable()} indicates the p2p availability and
+     * {@link android.net.NetworkInfo#getDetailedState()} reports the current fine-grained state
+     * of the network. This {@link android.net.NetworkInfo} is returned using the
+     * {@link NetworkInfoListener} listener.
+     *
+     * <p> This information is also included in the {@link #WIFI_P2P_CONNECTION_CHANGED_ACTION}
+     * broadcast event with extra {@link #EXTRA_NETWORK_INFO}.
+     *
+     * @param c is the channel created at {@link #initialize}.
+     * @param listener for callback when network info is available..
+     */
+    public void requestNetworkInfo(@NonNull Channel c,
+            @NonNull NetworkInfoListener listener) {
+        checkChannel(c);
+        if (listener == null) throw new IllegalArgumentException("This listener cannot be null.");
+        c.mAsyncChannel.sendMessage(REQUEST_NETWORK_INFO, 0, c.putListener(listener));
+    }
+
+     /**
+     * Request Device Info
+     *
+     * <p> This method provides the device info
+     * in the form of a {@link android.net.wifi.p2p.WifiP2pDevice}.
+     * Valid {@link android.net.wifi.p2p.WifiP2pDevice} is returned when p2p is enabled.
+     * To get information notifications on P2P getting enabled refers
+     * {@link #WIFI_P2P_STATE_ENABLED}.
+     *
+     * <p> This {@link android.net.wifi.p2p.WifiP2pDevice} is returned using the
+     * {@link DeviceInfoListener} listener.
+     *
+     * <p> {@link android.net.wifi.p2p.WifiP2pDevice#deviceAddress} is only available if the caller
+     * holds the {@code android.Manifest.permission#LOCAL_MAC_ADDRESS} permission, and holds the
+     * anonymized MAC address (02:00:00:00:00:00) otherwise.
+     *
+     * <p> This information is also included in the {@link #WIFI_P2P_THIS_DEVICE_CHANGED_ACTION}
+     * broadcast event with extra {@link #EXTRA_WIFI_P2P_DEVICE}.
+     *
+     * @param c is the channel created at {@link #initialize(Context, Looper, ChannelListener)}.
+     * @param listener for callback when network info is available.
+     */
+    @RequiresPermission(android.Manifest.permission.ACCESS_FINE_LOCATION)
+    public void requestDeviceInfo(@NonNull Channel c, @NonNull DeviceInfoListener listener) {
+        checkChannel(c);
+        if (listener == null) throw new IllegalArgumentException("This listener cannot be null.");
+        c.mAsyncChannel.sendMessage(REQUEST_DEVICE_INFO, 0, c.putListener(listener));
+    }
+}
diff --git a/android/net/wifi/p2p/WifiP2pProvDiscEvent.java b/android/net/wifi/p2p/WifiP2pProvDiscEvent.java
new file mode 100644
index 0000000..153e03c
--- /dev/null
+++ b/android/net/wifi/p2p/WifiP2pProvDiscEvent.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2011 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.wifi.p2p;
+
+import android.annotation.UnsupportedAppUsage;
+
+/**
+ * A class representing a Wi-Fi p2p provisional discovery request/response
+ * See {@link #WifiP2pProvDiscEvent} for supported types
+ *
+ * @hide
+ */
+public class WifiP2pProvDiscEvent {
+
+    private static final String TAG = "WifiP2pProvDiscEvent";
+
+    public static final int PBC_REQ     = 1;
+    public static final int PBC_RSP     = 2;
+    public static final int ENTER_PIN   = 3;
+    public static final int SHOW_PIN    = 4;
+
+    /* One of PBC_REQ, PBC_RSP, ENTER_PIN or SHOW_PIN */
+    @UnsupportedAppUsage
+    public int event;
+
+    @UnsupportedAppUsage
+    public WifiP2pDevice device;
+
+    /* Valid when event = SHOW_PIN */
+    @UnsupportedAppUsage
+    public String pin;
+
+    @UnsupportedAppUsage
+    public WifiP2pProvDiscEvent() {
+        device = new WifiP2pDevice();
+    }
+
+    /**
+     * @param string formats supported include
+     *
+     *  P2P-PROV-DISC-PBC-REQ 42:fc:89:e1:e2:27
+     *  P2P-PROV-DISC-PBC-RESP 02:12:47:f2:5a:36
+     *  P2P-PROV-DISC-ENTER-PIN 42:fc:89:e1:e2:27
+     *  P2P-PROV-DISC-SHOW-PIN 42:fc:89:e1:e2:27 44490607
+     *
+     *  Note: The events formats can be looked up in the wpa_supplicant code
+     * @hide
+     */
+    public WifiP2pProvDiscEvent(String string) throws IllegalArgumentException {
+        String[] tokens = string.split(" ");
+
+        if (tokens.length < 2) {
+            throw new IllegalArgumentException("Malformed event " + string);
+        }
+
+        if (tokens[0].endsWith("PBC-REQ")) event = PBC_REQ;
+        else if (tokens[0].endsWith("PBC-RESP")) event = PBC_RSP;
+        else if (tokens[0].endsWith("ENTER-PIN")) event = ENTER_PIN;
+        else if (tokens[0].endsWith("SHOW-PIN")) event = SHOW_PIN;
+        else throw new IllegalArgumentException("Malformed event " + string);
+
+
+        device = new WifiP2pDevice();
+        device.deviceAddress = tokens[1];
+
+        if (event == SHOW_PIN) {
+            pin = tokens[2];
+        }
+    }
+
+    public String toString() {
+        StringBuffer sbuf = new StringBuffer();
+        sbuf.append(device);
+        sbuf.append("\n event: ").append(event);
+        sbuf.append("\n pin: ").append(pin);
+        return sbuf.toString();
+    }
+}
diff --git a/android/net/wifi/p2p/WifiP2pWfdInfo.java b/android/net/wifi/p2p/WifiP2pWfdInfo.java
new file mode 100644
index 0000000..3caa280
--- /dev/null
+++ b/android/net/wifi/p2p/WifiP2pWfdInfo.java
@@ -0,0 +1,210 @@
+/*
+ * Copyright (C) 2012 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.wifi.p2p;
+
+import android.annotation.UnsupportedAppUsage;
+import android.os.Build;
+import android.os.Parcelable;
+import android.os.Parcel;
+
+import java.util.Locale;
+
+/**
+ * A class representing Wifi Display information for a device
+ * @hide
+ */
+public class WifiP2pWfdInfo implements Parcelable {
+
+    private static final String TAG = "WifiP2pWfdInfo";
+
+    private boolean mWfdEnabled;
+
+    private int mDeviceInfo;
+
+    public static final int WFD_SOURCE              = 0;
+    public static final int PRIMARY_SINK            = 1;
+    public static final int SECONDARY_SINK          = 2;
+    public static final int SOURCE_OR_PRIMARY_SINK  = 3;
+
+    /* Device information bitmap */
+    /** One of {@link #WFD_SOURCE}, {@link #PRIMARY_SINK}, {@link #SECONDARY_SINK}
+     * or {@link #SOURCE_OR_PRIMARY_SINK}
+     */
+    private static final int DEVICE_TYPE                            = 0x3;
+    private static final int COUPLED_SINK_SUPPORT_AT_SOURCE         = 0x4;
+    private static final int COUPLED_SINK_SUPPORT_AT_SINK           = 0x8;
+    private static final int SESSION_AVAILABLE                      = 0x30;
+    private static final int SESSION_AVAILABLE_BIT1                 = 0x10;
+    private static final int SESSION_AVAILABLE_BIT2                 = 0x20;
+
+    private int mCtrlPort;
+
+    private int mMaxThroughput;
+
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    public WifiP2pWfdInfo() {
+    }
+
+    @UnsupportedAppUsage
+    public WifiP2pWfdInfo(int devInfo, int ctrlPort, int maxTput) {
+        mWfdEnabled = true;
+        mDeviceInfo = devInfo;
+        mCtrlPort = ctrlPort;
+        mMaxThroughput = maxTput;
+    }
+
+    @UnsupportedAppUsage
+    public boolean isWfdEnabled() {
+        return mWfdEnabled;
+    }
+
+    @UnsupportedAppUsage
+    public void setWfdEnabled(boolean enabled) {
+        mWfdEnabled = enabled;
+    }
+
+    @UnsupportedAppUsage
+    public int getDeviceType() {
+        return (mDeviceInfo & DEVICE_TYPE);
+    }
+
+    @UnsupportedAppUsage
+    public boolean setDeviceType(int deviceType) {
+        if (deviceType >= WFD_SOURCE && deviceType <= SOURCE_OR_PRIMARY_SINK) {
+            mDeviceInfo &= ~DEVICE_TYPE;
+            mDeviceInfo |= deviceType;
+            return true;
+        }
+        return false;
+    }
+
+    public boolean isCoupledSinkSupportedAtSource() {
+        return (mDeviceInfo & COUPLED_SINK_SUPPORT_AT_SINK) != 0;
+    }
+
+    public void setCoupledSinkSupportAtSource(boolean enabled) {
+        if (enabled ) {
+            mDeviceInfo |= COUPLED_SINK_SUPPORT_AT_SINK;
+        } else {
+            mDeviceInfo &= ~COUPLED_SINK_SUPPORT_AT_SINK;
+        }
+    }
+
+    public boolean isCoupledSinkSupportedAtSink() {
+        return (mDeviceInfo & COUPLED_SINK_SUPPORT_AT_SINK) != 0;
+    }
+
+    public void setCoupledSinkSupportAtSink(boolean enabled) {
+        if (enabled ) {
+            mDeviceInfo |= COUPLED_SINK_SUPPORT_AT_SINK;
+        } else {
+            mDeviceInfo &= ~COUPLED_SINK_SUPPORT_AT_SINK;
+        }
+    }
+
+    public boolean isSessionAvailable() {
+        return (mDeviceInfo & SESSION_AVAILABLE) != 0;
+    }
+
+    @UnsupportedAppUsage
+    public void setSessionAvailable(boolean enabled) {
+        if (enabled) {
+            mDeviceInfo |= SESSION_AVAILABLE_BIT1;
+            mDeviceInfo &= ~SESSION_AVAILABLE_BIT2;
+        } else {
+            mDeviceInfo &= ~SESSION_AVAILABLE;
+        }
+    }
+
+    public int getControlPort() {
+        return mCtrlPort;
+    }
+
+    @UnsupportedAppUsage
+    public void setControlPort(int port) {
+        mCtrlPort = port;
+    }
+
+    @UnsupportedAppUsage
+    public void setMaxThroughput(int maxThroughput) {
+        mMaxThroughput = maxThroughput;
+    }
+
+    public int getMaxThroughput() {
+        return mMaxThroughput;
+    }
+
+    public String getDeviceInfoHex() {
+        return String.format(
+                Locale.US, "%04x%04x%04x", mDeviceInfo, mCtrlPort, mMaxThroughput);
+    }
+
+    public String toString() {
+        StringBuffer sbuf = new StringBuffer();
+        sbuf.append("WFD enabled: ").append(mWfdEnabled);
+        sbuf.append("WFD DeviceInfo: ").append(mDeviceInfo);
+        sbuf.append("\n WFD CtrlPort: ").append(mCtrlPort);
+        sbuf.append("\n WFD MaxThroughput: ").append(mMaxThroughput);
+        return sbuf.toString();
+    }
+
+    /** Implement the Parcelable interface */
+    public int describeContents() {
+        return 0;
+    }
+
+    /** copy constructor */
+    @UnsupportedAppUsage
+    public WifiP2pWfdInfo(WifiP2pWfdInfo source) {
+        if (source != null) {
+            mWfdEnabled = source.mWfdEnabled;
+            mDeviceInfo = source.mDeviceInfo;
+            mCtrlPort = source.mCtrlPort;
+            mMaxThroughput = source.mMaxThroughput;
+        }
+    }
+
+    /** Implement the Parcelable interface */
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(mWfdEnabled ? 1 : 0);
+        dest.writeInt(mDeviceInfo);
+        dest.writeInt(mCtrlPort);
+        dest.writeInt(mMaxThroughput);
+    }
+
+    public void readFromParcel(Parcel in) {
+        mWfdEnabled = (in.readInt() == 1);
+        mDeviceInfo = in.readInt();
+        mCtrlPort = in.readInt();
+        mMaxThroughput = in.readInt();
+    }
+
+    /** Implement the Parcelable interface */
+    @UnsupportedAppUsage
+    public static final @android.annotation.NonNull Creator<WifiP2pWfdInfo> CREATOR =
+        new Creator<WifiP2pWfdInfo>() {
+            public WifiP2pWfdInfo createFromParcel(Parcel in) {
+                WifiP2pWfdInfo device = new WifiP2pWfdInfo();
+                device.readFromParcel(in);
+                return device;
+            }
+
+            public WifiP2pWfdInfo[] newArray(int size) {
+                return new WifiP2pWfdInfo[size];
+            }
+        };
+}
diff --git a/android/net/wifi/p2p/nsd/WifiP2pDnsSdServiceInfo.java b/android/net/wifi/p2p/nsd/WifiP2pDnsSdServiceInfo.java
new file mode 100644
index 0000000..e32c8e8
--- /dev/null
+++ b/android/net/wifi/p2p/nsd/WifiP2pDnsSdServiceInfo.java
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2012 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.wifi.p2p.nsd;
+
+import android.annotation.UnsupportedAppUsage;
+import android.net.nsd.DnsSdTxtRecord;
+import android.os.Build;
+import android.text.TextUtils;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * A class for storing Bonjour service information that is advertised
+ * over a Wi-Fi peer-to-peer setup.
+ *
+ * {@see android.net.wifi.p2p.WifiP2pManager#addLocalService}
+ * {@see android.net.wifi.p2p.WifiP2pManager#removeLocalService}
+ * {@see WifiP2pServiceInfo}
+ * {@see WifiP2pUpnpServiceInfo}
+ */
+public class WifiP2pDnsSdServiceInfo extends WifiP2pServiceInfo {
+
+    /**
+     * Bonjour version 1.
+     * @hide
+     */
+    public static final int VERSION_1 = 0x01;
+
+    /**
+     * Pointer record.
+     * @hide
+     */
+    public static final int DNS_TYPE_PTR = 12;
+
+    /**
+     * Text record.
+     * @hide
+     */
+    public static final int DNS_TYPE_TXT = 16;
+
+    /**
+     * virtual memory packet.
+     * see E.3 of the Wi-Fi Direct technical specification for the detail.<br>
+     * Key: domain name Value: pointer address.<br>
+     */
+    private final static Map<String, String> sVmPacket;
+
+    static {
+        sVmPacket = new HashMap<String, String>();
+        sVmPacket.put("_tcp.local.", "c00c");
+        sVmPacket.put("local.", "c011");
+        sVmPacket.put("_udp.local.", "c01c");
+    }
+
+    /**
+     * This constructor is only used in newInstance().
+     *
+     * @param queryList
+     */
+    private WifiP2pDnsSdServiceInfo(List<String> queryList) {
+        super(queryList);
+    }
+
+    /**
+     * Create a Bonjour service information object.
+     *
+     * @param instanceName instance name.<br>
+     *  e.g) "MyPrinter"
+     * @param serviceType service type.<br>
+     *  e.g) "_ipp._tcp"
+     * @param txtMap TXT record with key/value pair in a map confirming to format defined at
+     * http://files.dns-sd.org/draft-cheshire-dnsext-dns-sd.txt
+     * @return Bonjour service information object
+     */
+    public static WifiP2pDnsSdServiceInfo newInstance(String instanceName,
+            String serviceType, Map<String, String> txtMap) {
+        if (TextUtils.isEmpty(instanceName) || TextUtils.isEmpty(serviceType)) {
+            throw new IllegalArgumentException(
+                    "instance name or service type cannot be empty");
+        }
+
+        DnsSdTxtRecord txtRecord = new DnsSdTxtRecord();
+        if (txtMap != null) {
+            for (String key : txtMap.keySet()) {
+                txtRecord.set(key, txtMap.get(key));
+            }
+        }
+
+        ArrayList<String> queries = new ArrayList<String>();
+        queries.add(createPtrServiceQuery(instanceName, serviceType));
+        queries.add(createTxtServiceQuery(instanceName, serviceType, txtRecord));
+
+        return new WifiP2pDnsSdServiceInfo(queries);
+    }
+
+    /**
+     * Create wpa_supplicant service query for PTR record.
+     *
+     * @param instanceName instance name.<br>
+     *  e.g) "MyPrinter"
+     * @param serviceType service type.<br>
+     *  e.g) "_ipp._tcp"
+     * @return wpa_supplicant service query.
+     */
+    private static String createPtrServiceQuery(String instanceName,
+            String serviceType) {
+
+        StringBuffer sb = new StringBuffer();
+        sb.append("bonjour ");
+        sb.append(createRequest(serviceType + ".local.", DNS_TYPE_PTR, VERSION_1));
+        sb.append(" ");
+
+        byte[] data = instanceName.getBytes();
+        sb.append(String.format(Locale.US, "%02x", data.length));
+        sb.append(WifiP2pServiceInfo.bin2HexStr(data));
+        // This is the start point of this response.
+        // Therefore, it indicates the request domain name.
+        sb.append("c027");
+        return sb.toString();
+    }
+
+    /**
+     * Create wpa_supplicant service query for TXT record.
+     *
+     * @param instanceName instance name.<br>
+     *  e.g) "MyPrinter"
+     * @param serviceType service type.<br>
+     *  e.g) "_ipp._tcp"
+     * @param txtRecord TXT record.<br>
+     * @return wpa_supplicant service query.
+     */
+    private static String createTxtServiceQuery(String instanceName,
+            String serviceType,
+            DnsSdTxtRecord txtRecord) {
+
+
+        StringBuffer sb = new StringBuffer();
+        sb.append("bonjour ");
+
+        sb.append(createRequest((instanceName + "." + serviceType + ".local."),
+                DNS_TYPE_TXT, VERSION_1));
+        sb.append(" ");
+        byte[] rawData = txtRecord.getRawData();
+        if (rawData.length == 0) {
+            sb.append("00");
+        } else {
+            sb.append(bin2HexStr(rawData));
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Create bonjour service discovery request.
+     *
+     * @param dnsName dns name
+     * @param dnsType dns type
+     * @param version version number
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    static String createRequest(String dnsName, int dnsType, int version) {
+        StringBuffer sb = new StringBuffer();
+
+        /*
+         * The request format is as follows.
+         * ________________________________________________
+         * |  Encoded and Compressed dns name (variable)  |
+         * ________________________________________________
+         * |   Type (2)           | Version (1) |
+         */
+        if (dnsType == WifiP2pDnsSdServiceInfo.DNS_TYPE_TXT) {
+            dnsName = dnsName.toLowerCase(Locale.ROOT); // TODO: is this right?
+        }
+        sb.append(compressDnsName(dnsName));
+        sb.append(String.format(Locale.US, "%04x", dnsType));
+        sb.append(String.format(Locale.US, "%02x", version));
+
+        return sb.toString();
+    }
+
+    /**
+     * Compress DNS data.
+     *
+     * see E.3 of the Wi-Fi Direct technical specification for the detail.
+     *
+     * @param dnsName dns name
+     * @return compressed dns name
+     */
+    private static String compressDnsName(String dnsName) {
+        StringBuffer sb = new StringBuffer();
+
+        // The domain name is replaced with a pointer to a prior
+        // occurrence of the same name in virtual memory packet.
+        while (true) {
+            String data = sVmPacket.get(dnsName);
+            if (data != null) {
+                sb.append(data);
+                break;
+            }
+            int i = dnsName.indexOf('.');
+            if (i == -1) {
+                if (dnsName.length() > 0) {
+                    sb.append(String.format(Locale.US, "%02x", dnsName.length()));
+                    sb.append(WifiP2pServiceInfo.bin2HexStr(dnsName.getBytes()));
+                }
+                // for a sequence of labels ending in a zero octet
+                sb.append("00");
+                break;
+            }
+
+            String name = dnsName.substring(0, i);
+            dnsName = dnsName.substring(i + 1);
+            sb.append(String.format(Locale.US, "%02x", name.length()));
+            sb.append(WifiP2pServiceInfo.bin2HexStr(name.getBytes()));
+        }
+        return sb.toString();
+    }
+}
diff --git a/android/net/wifi/p2p/nsd/WifiP2pDnsSdServiceRequest.java b/android/net/wifi/p2p/nsd/WifiP2pDnsSdServiceRequest.java
new file mode 100644
index 0000000..d5415e0
--- /dev/null
+++ b/android/net/wifi/p2p/nsd/WifiP2pDnsSdServiceRequest.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2012 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.wifi.p2p.nsd;
+
+import android.net.wifi.p2p.WifiP2pManager;
+
+/**
+ * A class for creating a Bonjour service discovery request for use with
+ * {@link WifiP2pManager#addServiceRequest} and {@link WifiP2pManager#removeServiceRequest}
+ *
+ * {@see WifiP2pManager}
+ * {@see WifiP2pServiceRequest}
+ * {@see WifiP2pUpnpServiceRequest}
+ */
+public class WifiP2pDnsSdServiceRequest extends WifiP2pServiceRequest {
+
+    /**
+     * This constructor is only used in newInstance().
+     *
+     * @param query The part of service specific query.
+     * @hide
+     */
+    private WifiP2pDnsSdServiceRequest(String query) {
+        super(WifiP2pServiceInfo.SERVICE_TYPE_BONJOUR, query);
+    }
+
+    /**
+     * This constructor is only used in newInstance().
+     * @hide
+     */
+    private WifiP2pDnsSdServiceRequest() {
+        super(WifiP2pServiceInfo.SERVICE_TYPE_BONJOUR, null);
+    }
+
+    private WifiP2pDnsSdServiceRequest(String dnsQuery, int dnsType, int version) {
+        super(WifiP2pServiceInfo.SERVICE_TYPE_BONJOUR, WifiP2pDnsSdServiceInfo.createRequest(
+                dnsQuery,
+                dnsType,
+                version));
+    }
+
+    /**
+     * Create a service discovery request to search all Bonjour services.
+     *
+     * @return service request for Bonjour.
+     */
+    public static WifiP2pDnsSdServiceRequest newInstance() {
+        return new WifiP2pDnsSdServiceRequest();
+    }
+
+    /**
+     * Create a service discovery to search for Bonjour services with the specified
+     * service type.
+     *
+     * @param serviceType service type. Cannot be null <br>
+     *  "_afpovertcp._tcp."(Apple File Sharing over TCP)<br>
+     *  "_ipp._tcp" (IP Printing over TCP)<br>
+     *  "_http._tcp" (http service)
+     * @return service request for DnsSd.
+     */
+    public static WifiP2pDnsSdServiceRequest newInstance(String serviceType) {
+        if (serviceType == null) {
+            throw new IllegalArgumentException("service type cannot be null");
+        }
+        return new WifiP2pDnsSdServiceRequest(serviceType + ".local.",
+                WifiP2pDnsSdServiceInfo.DNS_TYPE_PTR,
+                WifiP2pDnsSdServiceInfo.VERSION_1);
+    }
+
+    /**
+     * Create a service discovery request to get the TXT data from the specified
+     * Bonjour service.
+     *
+     * @param instanceName instance name. Cannot be null. <br>
+     *  "MyPrinter"
+     * @param serviceType service type. Cannot be null. <br>
+     * e.g) <br>
+     *  "_afpovertcp._tcp"(Apple File Sharing over TCP)<br>
+     *  "_ipp._tcp" (IP Printing over TCP)<br>
+     * @return service request for Bonjour.
+     */
+    public static WifiP2pDnsSdServiceRequest newInstance(String instanceName,
+            String serviceType) {
+        if (instanceName == null || serviceType == null) {
+            throw new IllegalArgumentException(
+                    "instance name or service type cannot be null");
+        }
+        String fullDomainName = instanceName + "." + serviceType + ".local.";
+        return new WifiP2pDnsSdServiceRequest(fullDomainName,
+                WifiP2pDnsSdServiceInfo.DNS_TYPE_TXT,
+                WifiP2pDnsSdServiceInfo.VERSION_1);
+    }
+}
diff --git a/android/net/wifi/p2p/nsd/WifiP2pDnsSdServiceResponse.java b/android/net/wifi/p2p/nsd/WifiP2pDnsSdServiceResponse.java
new file mode 100644
index 0000000..ed84a1a
--- /dev/null
+++ b/android/net/wifi/p2p/nsd/WifiP2pDnsSdServiceResponse.java
@@ -0,0 +1,309 @@
+/*
+ * Copyright (C) 2012 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.wifi.p2p.nsd;
+
+import android.net.wifi.p2p.WifiP2pDevice;
+
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * A class for a response of bonjour service discovery.
+ *
+ * @hide
+ */
+public class WifiP2pDnsSdServiceResponse extends WifiP2pServiceResponse {
+
+    /**
+     * DNS query name.
+     * e.g)
+     * for PTR
+     * "_ipp._tcp.local."
+     * for TXT
+     * "MyPrinter._ipp._tcp.local."
+     */
+    private String mDnsQueryName;
+
+    /**
+     * Service instance name.
+     * e.g) "MyPrinter"
+     * This field is only used when the dns type equals to
+     * {@link WifiP2pDnsSdServiceInfo#DNS_TYPE_PTR}.
+     */
+    private String mInstanceName;
+
+    /**
+     * DNS Type.
+     * Should be {@link WifiP2pDnsSdServiceInfo#DNS_TYPE_PTR} or
+     * {@link WifiP2pDnsSdServiceInfo#DNS_TYPE_TXT}.
+     */
+    private int mDnsType;
+
+    /**
+     * DnsSd version number.
+     * Should be {@link WifiP2pDnsSdServiceInfo#VERSION_1}.
+     */
+    private int mVersion;
+
+    /**
+     * Txt record.
+     * This field is only used when the dns type equals to
+     * {@link WifiP2pDnsSdServiceInfo#DNS_TYPE_TXT}.
+     */
+    private final HashMap<String, String> mTxtRecord = new HashMap<String, String>();
+
+    /**
+     * Virtual memory packet.
+     * see E.3 of the Wi-Fi Direct technical specification for the detail.<br>
+     * The spec can be obtained from wi-fi.org
+     * Key: pointer Value: domain name.<br>
+     */
+    private final static Map<Integer, String> sVmpack;
+
+    static {
+        sVmpack = new HashMap<Integer, String>();
+        sVmpack.put(0x0c, "_tcp.local.");
+        sVmpack.put(0x11, "local.");
+        sVmpack.put(0x1c, "_udp.local.");
+    }
+
+    /**
+     * Returns query DNS name.
+     * @return DNS name.
+     */
+    public String getDnsQueryName() {
+        return mDnsQueryName;
+    }
+
+    /**
+     * Return query DNS type.
+     * @return DNS type.
+     */
+    public int getDnsType() {
+        return mDnsType;
+    }
+
+    /**
+     * Return bonjour version number.
+     * @return version number.
+     */
+    public int getVersion() {
+        return mVersion;
+    }
+
+    /**
+     * Return instance name.
+     * @return
+     */
+    public String getInstanceName() {
+        return mInstanceName;
+    }
+
+    /**
+     * Return TXT record data.
+     * @return TXT record data.
+     */
+    public Map<String, String> getTxtRecord() {
+        return mTxtRecord;
+    }
+
+    @Override
+    public String toString() {
+        StringBuffer sbuf = new StringBuffer();
+        sbuf.append("serviceType:DnsSd(").append(mServiceType).append(")");
+        sbuf.append(" status:").append(Status.toString(mStatus));
+        sbuf.append(" srcAddr:").append(mDevice.deviceAddress);
+        sbuf.append(" version:").append(String.format("%02x", mVersion));
+        sbuf.append(" dnsName:").append(mDnsQueryName);
+        sbuf.append(" TxtRecord:");
+        for (String key : mTxtRecord.keySet()) {
+            sbuf.append(" key:").append(key).append(" value:").append(mTxtRecord.get(key));
+        }
+        if (mInstanceName != null) {
+            sbuf.append(" InsName:").append(mInstanceName);
+        }
+        return sbuf.toString();
+    }
+
+    /**
+     * This is only used in framework.
+     * @param status status code.
+     * @param dev source device.
+     * @param data RDATA.
+     * @hide
+     */
+    protected WifiP2pDnsSdServiceResponse(int status,
+            int tranId, WifiP2pDevice dev, byte[] data) {
+        super(WifiP2pServiceInfo.SERVICE_TYPE_BONJOUR,
+                status, tranId, dev, data);
+        if (!parse()) {
+            throw new IllegalArgumentException("Malformed bonjour service response");
+        }
+    }
+
+    /**
+     * Parse DnsSd service discovery response.
+     *
+     * @return {@code true} if the operation succeeded
+     */
+    private boolean parse() {
+        /*
+         * The data format from Wi-Fi Direct spec is as follows.
+         * ________________________________________________
+         * |  encoded and compressed dns name (variable)  |
+         * ________________________________________________
+         * |       dnstype(2byte)      |  version(1byte)  |
+         * ________________________________________________
+         * |              RDATA (variable)                |
+         */
+        if (mData == null) {
+            // the empty is OK.
+            return true;
+        }
+
+        DataInputStream dis = new DataInputStream(new ByteArrayInputStream(mData));
+
+        mDnsQueryName = readDnsName(dis);
+        if (mDnsQueryName == null) {
+            return false;
+        }
+
+        try {
+            mDnsType = dis.readUnsignedShort();
+            mVersion = dis.readUnsignedByte();
+        } catch (IOException e) {
+            e.printStackTrace();
+            return false;
+        }
+
+        if (mDnsType == WifiP2pDnsSdServiceInfo.DNS_TYPE_PTR) {
+            String rData = readDnsName(dis);
+            if (rData == null) {
+                return false;
+            }
+            if (rData.length() <= mDnsQueryName.length()) {
+                return false;
+            }
+
+            mInstanceName = rData.substring(0,
+                    rData.length() - mDnsQueryName.length() -1);
+        } else if (mDnsType == WifiP2pDnsSdServiceInfo.DNS_TYPE_TXT) {
+            return readTxtData(dis);
+        } else {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * Read dns name.
+     *
+     * @param dis data input stream.
+     * @return dns name
+     */
+    private String readDnsName(DataInputStream dis) {
+        StringBuffer sb = new StringBuffer();
+
+        // copy virtual memory packet.
+        HashMap<Integer, String> vmpack = new HashMap<Integer, String>(sVmpack);
+        if (mDnsQueryName != null) {
+            vmpack.put(0x27, mDnsQueryName);
+        }
+        try {
+            while (true) {
+                int i = dis.readUnsignedByte();
+                if (i == 0x00) {
+                    return sb.toString();
+                } else if (i == 0xc0) {
+                    // refer to pointer.
+                    String ref = vmpack.get(dis.readUnsignedByte());
+                    if (ref == null) {
+                        //invalid.
+                        return null;
+                    }
+                    sb.append(ref);
+                    return sb.toString();
+                } else {
+                    byte[] data = new byte[i];
+                    dis.readFully(data);
+                    sb.append(new String(data));
+                    sb.append(".");
+                }
+            }
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    /**
+     * Read TXT record data.
+     *
+     * @param dis
+     * @return true if TXT data is valid
+     */
+    private boolean readTxtData(DataInputStream dis) {
+        try {
+            while (dis.available() > 0) {
+                int len = dis.readUnsignedByte();
+                if (len == 0) {
+                    break;
+                }
+                byte[] data = new byte[len];
+                dis.readFully(data);
+                String[] keyVal = new String(data).split("=");
+                if (keyVal.length != 2) {
+                    return false;
+                }
+                mTxtRecord.put(keyVal[0], keyVal[1]);
+            }
+            return true;
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+        return false;
+    }
+
+    /**
+     * Creates DnsSd service response.
+     *  This is only called from WifiP2pServiceResponse
+     *
+     * @param status status code.
+     * @param dev source device.
+     * @param data DnsSd response data.
+     * @return DnsSd service response data.
+     * @hide
+     */
+    static WifiP2pDnsSdServiceResponse newInstance(int status,
+            int transId, WifiP2pDevice dev, byte[] data) {
+        if (status != WifiP2pServiceResponse.Status.SUCCESS) {
+            return new WifiP2pDnsSdServiceResponse(status,
+                    transId, dev, null);
+        }
+        try {
+            return new WifiP2pDnsSdServiceResponse(status,
+                    transId, dev, data);
+        } catch (IllegalArgumentException e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+}
diff --git a/android/net/wifi/p2p/nsd/WifiP2pServiceInfo.java b/android/net/wifi/p2p/nsd/WifiP2pServiceInfo.java
new file mode 100644
index 0000000..db0bdb8
--- /dev/null
+++ b/android/net/wifi/p2p/nsd/WifiP2pServiceInfo.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright (C) 2012 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.wifi.p2p.nsd;
+
+import android.annotation.UnsupportedAppUsage;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A class for storing service information that is advertised
+ * over a Wi-Fi peer-to-peer setup
+ *
+ * @see WifiP2pUpnpServiceInfo
+ * @see WifiP2pDnsSdServiceInfo
+ */
+public class WifiP2pServiceInfo implements Parcelable {
+
+    /**
+     * All service protocol types.
+     */
+    public static final int SERVICE_TYPE_ALL             = 0;
+
+    /**
+     * DNS based service discovery protocol.
+     */
+    public static final int SERVICE_TYPE_BONJOUR         = 1;
+
+    /**
+     * UPnP protocol.
+     */
+    public static final int SERVICE_TYPE_UPNP            = 2;
+
+    /**
+     * WS-Discovery protocol
+     * @hide
+     */
+    public static final int SERVICE_TYPE_WS_DISCOVERY    = 3;
+
+    /**
+     * Vendor Specific protocol
+     */
+    public static final int SERVICE_TYPE_VENDOR_SPECIFIC = 255;
+
+    /**
+     * the list of query string for wpa_supplicant
+     *
+     * e.g)
+     * # IP Printing over TCP (PTR) (RDATA=MyPrinter._ipp._tcp.local.)
+     * {"bonjour", "045f697070c00c000c01", "094d795072696e746572c027"
+     *
+     * # IP Printing over TCP (TXT) (RDATA=txtvers=1,pdl=application/postscript)
+     * {"bonjour", "096d797072696e746572045f697070c00c001001",
+     *  "09747874766572733d311a70646c3d6170706c69636174696f6e2f706f7374736372797074"}
+     *
+     * [UPnP]
+     * # UPnP uuid
+     * {"upnp", "10", "uuid:6859dede-8574-59ab-9332-123456789012"}
+     *
+     * # UPnP rootdevice
+     * {"upnp", "10", "uuid:6859dede-8574-59ab-9332-123456789012::upnp:rootdevice"}
+     *
+     * # UPnP device
+     * {"upnp", "10", "uuid:6859dede-8574-59ab-9332-123456789012::urn:schemas-upnp
+     * -org:device:InternetGatewayDevice:1"}
+     *
+     *  # UPnP service
+     * {"upnp", "10", "uuid:6859dede-8574-59ab-9322-123456789012::urn:schemas-upnp
+     * -org:service:ContentDirectory:2"}
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    private List<String> mQueryList;
+
+    /**
+     * This is only used in subclass.
+     *
+     * @param queryList query string for wpa_supplicant
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    protected WifiP2pServiceInfo(List<String> queryList) {
+        if (queryList == null) {
+            throw new IllegalArgumentException("query list cannot be null");
+        }
+        mQueryList = queryList;
+    }
+
+   /**
+    * Return the list of the query string for wpa_supplicant.
+    *
+    * @return the list of the query string for wpa_supplicant.
+    * @hide
+    */
+   public List<String> getSupplicantQueryList() {
+       return mQueryList;
+   }
+
+   /**
+    * Converts byte array to hex string.
+    *
+    * @param data
+    * @return hex string.
+    * @hide
+    */
+   static String bin2HexStr(byte[] data) {
+       StringBuffer sb = new StringBuffer();
+
+       for (byte b: data) {
+           String s = null;
+           try {
+               s = Integer.toHexString(b & 0xff);
+           } catch (Exception e) {
+               e.printStackTrace();
+               return null;
+           }
+           //add 0 padding
+           if (s.length() == 1) {
+               sb.append('0');
+           }
+           sb.append(s);
+       }
+       return sb.toString();
+   }
+
+   @Override
+   public boolean equals(Object o) {
+       if (o == this) {
+           return true;
+       }
+       if (!(o instanceof WifiP2pServiceInfo)) {
+           return false;
+       }
+
+       WifiP2pServiceInfo servInfo = (WifiP2pServiceInfo)o;
+       return  mQueryList.equals(servInfo.mQueryList);
+   }
+
+   @Override
+   public int hashCode() {
+       int result = 17;
+       result = 31 * result + (mQueryList == null ? 0 : mQueryList.hashCode());
+       return result;
+   }
+
+    /** Implement the Parcelable interface {@hide} */
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Implement the Parcelable interface {@hide} */
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeStringList(mQueryList);
+    }
+
+    /** Implement the Parcelable interface {@hide} */
+    @UnsupportedAppUsage
+    public static final @android.annotation.NonNull Creator<WifiP2pServiceInfo> CREATOR =
+        new Creator<WifiP2pServiceInfo>() {
+            public WifiP2pServiceInfo createFromParcel(Parcel in) {
+
+                List<String> data = new ArrayList<String>();
+                in.readStringList(data);
+                return new WifiP2pServiceInfo(data);
+            }
+
+            public WifiP2pServiceInfo[] newArray(int size) {
+                return new WifiP2pServiceInfo[size];
+            }
+        };
+}
diff --git a/android/net/wifi/p2p/nsd/WifiP2pServiceRequest.java b/android/net/wifi/p2p/nsd/WifiP2pServiceRequest.java
new file mode 100644
index 0000000..87528c4
--- /dev/null
+++ b/android/net/wifi/p2p/nsd/WifiP2pServiceRequest.java
@@ -0,0 +1,283 @@
+/*
+ * Copyright (C) 2012 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.wifi.p2p.nsd;
+
+import android.annotation.UnsupportedAppUsage;
+import android.net.wifi.p2p.WifiP2pManager;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.Locale;
+
+/**
+ * A class for creating a service discovery request for use with
+ * {@link WifiP2pManager#addServiceRequest} and {@link WifiP2pManager#removeServiceRequest}
+ *
+ * <p>This class is used to create service discovery request for custom
+ * vendor specific service discovery protocol {@link WifiP2pServiceInfo#SERVICE_TYPE_VENDOR_SPECIFIC}
+ * or to search all service protocols {@link WifiP2pServiceInfo#SERVICE_TYPE_ALL}.
+ *
+ * <p>For the purpose of creating a UPnP or Bonjour service request, use
+ * {@link WifiP2pUpnpServiceRequest} or {@link WifiP2pDnsSdServiceRequest} respectively.
+ *
+ * {@see WifiP2pManager}
+ * {@see WifiP2pUpnpServiceRequest}
+ * {@see WifiP2pDnsSdServiceRequest}
+ */
+public class WifiP2pServiceRequest implements Parcelable {
+
+    /**
+     * Service discovery protocol. It's defined in table63 in Wi-Fi Direct specification.
+     */
+    private int mProtocolType;
+
+    /**
+     * The length of the service request TLV.
+     * The value is equal to 2 plus the number of octets in the
+     * query data field.
+     */
+    private int mLength;
+
+    /**
+     * Service transaction ID.
+     * This is a nonzero value used to match the service request/response TLVs.
+     */
+    private int mTransId;
+
+    /**
+     * The hex dump string of query data for the requested service information.
+     *
+     * e.g) DnsSd apple file sharing over tcp (dns name=_afpovertcp._tcp.local.)
+     * 0b5f6166706f766572746370c00c000c01
+     */
+    private String mQuery;
+
+    /**
+     * This constructor is only used in newInstance().
+     *
+     * @param protocolType service discovery protocol.
+     * @param query The part of service specific query.
+     * @hide
+     */
+    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
+    protected WifiP2pServiceRequest(int protocolType, String query) {
+        validateQuery(query);
+
+        mProtocolType = protocolType;
+        mQuery = query;
+        if (query != null) {
+            mLength = query.length()/2 + 2;
+        } else {
+            mLength = 2;
+        }
+    }
+
+    /**
+     * This constructor is only used in Parcelable.
+     *
+     * @param serviceType service discovery type.
+     * @param length the length of service discovery packet.
+     * @param transId the transaction id
+     * @param query The part of service specific query.
+     */
+    private WifiP2pServiceRequest(int serviceType, int length,
+            int transId, String query) {
+        mProtocolType = serviceType;
+        mLength = length;
+        mTransId = transId;
+        mQuery = query;
+    }
+
+    /**
+     * Return transaction id.
+     *
+     * @return transaction id
+     * @hide
+     */
+    public int getTransactionId() {
+        return mTransId;
+    }
+
+    /**
+     * Set transaction id.
+     *
+     * @param id
+     * @hide
+     */
+    public void setTransactionId(int id) {
+        mTransId = id;
+    }
+
+    /**
+     * Return wpa_supplicant request string.
+     *
+     * The format is the hex dump of the following frame.
+     * <pre>
+     * _______________________________________________________________
+     * |        Length (2)        |   Type (1)   | Transaction ID (1) |
+     * |                  Query Data (variable)                       |
+     * </pre>
+     *
+     * @return wpa_supplicant request string.
+     * @hide
+     */
+    public String getSupplicantQuery() {
+        StringBuffer sb = new StringBuffer();
+        // length is retained as little endian format.
+        sb.append(String.format(Locale.US, "%02x", (mLength) & 0xff));
+        sb.append(String.format(Locale.US, "%02x", (mLength >> 8) & 0xff));
+        sb.append(String.format(Locale.US, "%02x", mProtocolType));
+        sb.append(String.format(Locale.US, "%02x", mTransId));
+        if (mQuery != null) {
+            sb.append(mQuery);
+        }
+
+        return sb.toString();
+    }
+
+    /**
+     * Validate query.
+     *
+     * <p>If invalid, throw IllegalArgumentException.
+     * @param query The part of service specific query.
+     */
+    private void validateQuery(String query) {
+        if (query == null) {
+            return;
+        }
+
+        int UNSIGNED_SHORT_MAX = 0xffff;
+        if (query.length()%2 == 1) {
+            throw new IllegalArgumentException(
+                    "query size is invalid. query=" + query);
+        }
+        if (query.length()/2 > UNSIGNED_SHORT_MAX) {
+            throw new IllegalArgumentException(
+                    "query size is too large. len=" + query.length());
+        }
+
+        // check whether query is hex string.
+        query = query.toLowerCase(Locale.ROOT);
+        char[] chars = query.toCharArray();
+        for (char c: chars) {
+            if (!((c >= '0' && c <= '9') ||
+                    (c >= 'a' && c <= 'f'))){
+                throw new IllegalArgumentException(
+                        "query should be hex string. query=" + query);
+            }
+        }
+    }
+
+    /**
+     * Create a service discovery request.
+     *
+     * @param protocolType can be {@link WifiP2pServiceInfo#SERVICE_TYPE_ALL}
+     * or {@link WifiP2pServiceInfo#SERVICE_TYPE_VENDOR_SPECIFIC}.
+     * In order to create a UPnP or Bonjour service request, use
+     * {@link WifiP2pUpnpServiceRequest} or {@link WifiP2pDnsSdServiceRequest}
+     * respectively
+     *
+     * @param queryData hex string that is vendor specific.  Can be null.
+     * @return service discovery request.
+     */
+    public static WifiP2pServiceRequest newInstance(int protocolType, String queryData) {
+        return new WifiP2pServiceRequest(protocolType, queryData);
+    }
+
+    /**
+     * Create a service discovery request.
+     *
+     * @param protocolType can be {@link WifiP2pServiceInfo#SERVICE_TYPE_ALL}
+     * or {@link WifiP2pServiceInfo#SERVICE_TYPE_VENDOR_SPECIFIC}.
+     * In order to create a UPnP or Bonjour service request, use
+     * {@link WifiP2pUpnpServiceRequest} or {@link WifiP2pDnsSdServiceRequest}
+     * respectively
+     *
+     * @return service discovery request.
+     */
+    public static WifiP2pServiceRequest newInstance(int protocolType ) {
+        return new WifiP2pServiceRequest(protocolType, null);
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o == this) {
+            return true;
+        }
+        if (!(o instanceof WifiP2pServiceRequest)) {
+            return false;
+        }
+
+        WifiP2pServiceRequest req = (WifiP2pServiceRequest)o;
+
+        /*
+         * Not compare transaction id.
+         * Transaction id may be changed on each service discovery operation.
+         */
+        if ((req.mProtocolType != mProtocolType) ||
+                (req.mLength != mLength)) {
+            return false;
+        }
+
+        if (req.mQuery == null && mQuery == null) {
+            return true;
+        } else if (req.mQuery != null) {
+            return req.mQuery.equals(mQuery);
+        }
+        return false;
+   }
+
+    @Override
+    public int hashCode() {
+        int result = 17;
+        result = 31 * result + mProtocolType;
+        result = 31 * result + mLength;
+        result = 31 * result + (mQuery == null ? 0 : mQuery.hashCode());
+        return result;
+    }
+
+    /** Implement the Parcelable interface {@hide} */
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Implement the Parcelable interface {@hide} */
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(mProtocolType);
+        dest.writeInt(mLength);
+        dest.writeInt(mTransId);
+        dest.writeString(mQuery);
+    }
+
+    /** Implement the Parcelable interface {@hide} */
+    @UnsupportedAppUsage
+    public static final @android.annotation.NonNull Creator<WifiP2pServiceRequest> CREATOR =
+        new Creator<WifiP2pServiceRequest>() {
+            public WifiP2pServiceRequest createFromParcel(Parcel in) {
+                int servType = in.readInt();
+                int length = in.readInt();
+                int transId = in.readInt();
+                String query = in.readString();
+                return new WifiP2pServiceRequest(servType, length, transId, query);
+            }
+
+            public WifiP2pServiceRequest[] newArray(int size) {
+                return new WifiP2pServiceRequest[size];
+            }
+        };
+}
diff --git a/android/net/wifi/p2p/nsd/WifiP2pServiceResponse.java b/android/net/wifi/p2p/nsd/WifiP2pServiceResponse.java
new file mode 100644
index 0000000..1b9c080
--- /dev/null
+++ b/android/net/wifi/p2p/nsd/WifiP2pServiceResponse.java
@@ -0,0 +1,381 @@
+/*
+ * Copyright (C) 2012 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.wifi.p2p.nsd;
+
+import android.net.wifi.p2p.WifiP2pDevice;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.io.ByteArrayInputStream;
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * The class for a response of service discovery.
+ *
+ * @hide
+ */
+public class WifiP2pServiceResponse implements Parcelable {
+
+    private static int MAX_BUF_SIZE = 1024;
+
+    /**
+     * Service type. It's defined in table63 in Wi-Fi Direct specification.
+     */
+    protected int mServiceType;
+
+    /**
+     * Status code of service discovery response.
+     * It's defined in table65 in Wi-Fi Direct specification.
+     * @see Status
+     */
+    protected int mStatus;
+
+    /**
+     * Service transaction ID.
+     * This is a nonzero value used to match the service request/response TLVs.
+     */
+    protected int mTransId;
+
+    /**
+     * Source device.
+     */
+    protected WifiP2pDevice mDevice;
+
+    /**
+     * Service discovery response data based on the requested on
+     * the service protocol type. The protocol format depends on the service type.
+     */
+    protected byte[] mData;
+
+
+    /**
+     * The status code of service discovery response.
+     * Currently 4 status codes are defined and the status codes from  4 to 255
+     * are reserved.
+     *
+     * See Wi-Fi Direct specification for the detail.
+     */
+    public static class Status {
+        /** success */
+        public static final int SUCCESS = 0;
+
+        /** the service protocol type is not available */
+        public static final int SERVICE_PROTOCOL_NOT_AVAILABLE = 1;
+
+        /** the requested information is not available */
+        public static final int REQUESTED_INFORMATION_NOT_AVAILABLE = 2;
+
+        /** bad request */
+        public static final int BAD_REQUEST = 3;
+
+        /** @hide */
+        public static String toString(int status) {
+            switch(status) {
+            case SUCCESS:
+                return "SUCCESS";
+            case SERVICE_PROTOCOL_NOT_AVAILABLE:
+                return "SERVICE_PROTOCOL_NOT_AVAILABLE";
+            case REQUESTED_INFORMATION_NOT_AVAILABLE:
+                return "REQUESTED_INFORMATION_NOT_AVAILABLE";
+            case BAD_REQUEST:
+                return "BAD_REQUEST";
+            default:
+                return "UNKNOWN";
+            }
+        }
+
+        /** not used */
+        private Status() {}
+    }
+
+    /**
+     * Hidden constructor. This is only used in framework.
+     *
+     * @param serviceType service discovery type.
+     * @param status status code.
+     * @param transId transaction id.
+     * @param device source device.
+     * @param data query data.
+     */
+    protected WifiP2pServiceResponse(int serviceType, int status, int transId,
+            WifiP2pDevice device, byte[] data) {
+        mServiceType = serviceType;
+        mStatus = status;
+        mTransId = transId;
+        mDevice = device;
+        mData = data;
+    }
+
+    /**
+     * Return the service type of service discovery response.
+     *
+     * @return service discovery type.<br>
+     * e.g) {@link WifiP2pServiceInfo#SERVICE_TYPE_BONJOUR}
+     */
+    public int getServiceType() {
+        return mServiceType;
+    }
+
+    /**
+     * Return the status code of service discovery response.
+     *
+     * @return status code.
+     * @see Status
+     */
+    public int getStatus() {
+        return mStatus;
+    }
+
+    /**
+     * Return the transaction id of service discovery response.
+     *
+     * @return transaction id.
+     * @hide
+     */
+    public int getTransactionId() {
+        return mTransId;
+    }
+
+    /**
+     * Return response data.
+     *
+     * <pre>Data format depends on service type
+     *
+     * @return a query or response data.
+     */
+    public byte[] getRawData() {
+        return mData;
+    }
+
+    /**
+     * Returns the source device of service discovery response.
+     *
+     * <pre>This is valid only when service discovery response.
+     *
+     * @return the source device of service discovery response.
+     */
+    public WifiP2pDevice getSrcDevice() {
+        return mDevice;
+    }
+
+    /** @hide */
+    public void setSrcDevice(WifiP2pDevice dev) {
+        if (dev == null) return;
+        this.mDevice = dev;
+    }
+
+
+    /**
+     * Create the list of  WifiP2pServiceResponse instance from supplicant event.
+     *
+     * @param srcAddr source address of the service response
+     * @param tlvsBin byte array containing the binary tlvs data
+     * @return if parse failed, return null
+     * @hide
+     */
+    public static List<WifiP2pServiceResponse> newInstance(String srcAddr, byte[] tlvsBin) {
+        //updateIndicator not used, and not passed up from supplicant
+
+        List<WifiP2pServiceResponse> respList = new ArrayList<WifiP2pServiceResponse>();
+        WifiP2pDevice dev = new WifiP2pDevice();
+        dev.deviceAddress = srcAddr;
+        if (tlvsBin == null) {
+            return null;
+        }
+
+
+        DataInputStream dis = new DataInputStream(new ByteArrayInputStream(tlvsBin));
+        try {
+            while (dis.available() > 0) {
+                /*
+                 * Service discovery header is as follows.
+                 * ______________________________________________________________
+                 * |           Length(2byte)     | Type(1byte) | TransId(1byte)}|
+                 * ______________________________________________________________
+                 * | status(1byte)  |            vendor specific(variable)      |
+                 */
+                // The length equals to 3 plus the number of octets in the vendor
+                // specific content field. And this is little endian.
+                int length = (dis.readUnsignedByte() +
+                        (dis.readUnsignedByte() << 8)) - 3;
+                int type = dis.readUnsignedByte();
+                int transId = dis.readUnsignedByte();
+                int status = dis.readUnsignedByte();
+                if (length < 0) {
+                    return null;
+                }
+                if (length == 0) {
+                    if (status == Status.SUCCESS) {
+                        respList.add(new WifiP2pServiceResponse(type, status,
+                            transId, dev, null));
+                    }
+                    continue;
+                }
+                if (length > MAX_BUF_SIZE) {
+                    dis.skip(length);
+                    continue;
+                }
+                byte[] data = new byte[length];
+                dis.readFully(data);
+
+                WifiP2pServiceResponse resp;
+                if (type ==  WifiP2pServiceInfo.SERVICE_TYPE_BONJOUR) {
+                    resp = WifiP2pDnsSdServiceResponse.newInstance(status,
+                            transId, dev, data);
+                } else if (type == WifiP2pServiceInfo.SERVICE_TYPE_UPNP) {
+                    resp = WifiP2pUpnpServiceResponse.newInstance(status,
+                            transId, dev, data);
+                } else {
+                    resp = new WifiP2pServiceResponse(type, status, transId, dev, data);
+                }
+                if (resp != null && resp.getStatus() == Status.SUCCESS) {
+                    respList.add(resp);
+                }
+            }
+            return respList;
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+
+        if (respList.size() > 0) {
+            return respList;
+        }
+        return null;
+    }
+
+    /**
+     * Converts hex string to byte array.
+     *
+     * @param hex hex string. if invalid, return null.
+     * @return binary data.
+     */
+    private static byte[] hexStr2Bin(String hex) {
+        int sz = hex.length()/2;
+        byte[] b = new byte[hex.length()/2];
+
+        for (int i=0;i<sz;i++) {
+            try {
+                b[i] = (byte)Integer.parseInt(hex.substring(i*2, i*2+2), 16);
+            } catch (Exception e) {
+                e.printStackTrace();
+                return null;
+            }
+        }
+        return b;
+    }
+
+    @Override
+    public String toString() {
+        StringBuffer sbuf = new StringBuffer();
+        sbuf.append("serviceType:").append(mServiceType);
+        sbuf.append(" status:").append(Status.toString(mStatus));
+        sbuf.append(" srcAddr:").append(mDevice.deviceAddress);
+        sbuf.append(" data:").append(Arrays.toString(mData));
+        return sbuf.toString();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (o == this) {
+            return true;
+        }
+        if (!(o instanceof WifiP2pServiceResponse)) {
+            return false;
+        }
+
+        WifiP2pServiceResponse req = (WifiP2pServiceResponse)o;
+
+        return (req.mServiceType == mServiceType) &&
+            (req.mStatus == mStatus) &&
+                equals(req.mDevice.deviceAddress, mDevice.deviceAddress) &&
+                Arrays.equals(req.mData, mData);
+    }
+
+    private boolean equals(Object a, Object b) {
+        if (a == null && b == null) {
+            return true;
+        } else if (a != null) {
+            return a.equals(b);
+        }
+        return false;
+    }
+
+    @Override
+    public int hashCode() {
+        int result = 17;
+        result = 31 * result + mServiceType;
+        result = 31 * result + mStatus;
+        result = 31 * result + mTransId;
+        result = 31 * result + (mDevice.deviceAddress == null ?
+                0 : mDevice.deviceAddress.hashCode());
+        result = 31 * result + (mData == null ? 0 : Arrays.hashCode(mData));
+        return result;
+    }
+
+    /** Implement the Parcelable interface {@hide} */
+    public int describeContents() {
+        return 0;
+    }
+
+    /** Implement the Parcelable interface {@hide} */
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(mServiceType);
+        dest.writeInt(mStatus);
+        dest.writeInt(mTransId);
+        dest.writeParcelable(mDevice, flags);
+        if (mData == null || mData.length == 0) {
+            dest.writeInt(0);
+        } else {
+            dest.writeInt(mData.length);
+            dest.writeByteArray(mData);
+        }
+    }
+
+    /** Implement the Parcelable interface {@hide} */
+    public static final @android.annotation.NonNull Creator<WifiP2pServiceResponse> CREATOR =
+        new Creator<WifiP2pServiceResponse>() {
+            public WifiP2pServiceResponse createFromParcel(Parcel in) {
+
+                int type = in.readInt();
+                int status = in.readInt();
+                int transId = in.readInt();
+                WifiP2pDevice dev = (WifiP2pDevice)in.readParcelable(null);
+                int len = in.readInt();
+                byte[] data = null;
+                if (len > 0) {
+                    data = new byte[len];
+                    in.readByteArray(data);
+                }
+                if (type ==  WifiP2pServiceInfo.SERVICE_TYPE_BONJOUR) {
+                    return WifiP2pDnsSdServiceResponse.newInstance(status,
+                            transId, dev, data);
+                } else if (type == WifiP2pServiceInfo.SERVICE_TYPE_UPNP) {
+                    return WifiP2pUpnpServiceResponse.newInstance(status,
+                            transId, dev, data);
+                }
+                return new WifiP2pServiceResponse(type, status, transId, dev, data);
+            }
+
+            public WifiP2pServiceResponse[] newArray(int size) {
+                return new WifiP2pServiceResponse[size];
+            }
+        };
+}
diff --git a/android/net/wifi/p2p/nsd/WifiP2pUpnpServiceInfo.java b/android/net/wifi/p2p/nsd/WifiP2pUpnpServiceInfo.java
new file mode 100644
index 0000000..a4cdfd9
--- /dev/null
+++ b/android/net/wifi/p2p/nsd/WifiP2pUpnpServiceInfo.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2012 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.wifi.p2p.nsd;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.UUID;
+
+/**
+ * A class for storing Upnp service information that is advertised
+ * over a Wi-Fi peer-to-peer setup.
+ *
+ * {@see android.net.wifi.p2p.WifiP2pManager#addLocalService}
+ * {@see android.net.wifi.p2p.WifiP2pManager#removeLocalService}
+ * {@see WifiP2pServiceInfo}
+ * {@see WifiP2pDnsSdServiceInfo}
+ */
+public class WifiP2pUpnpServiceInfo extends WifiP2pServiceInfo {
+
+    /**
+     * UPnP version 1.0.
+     *
+     * <pre>Query Version should always be set to 0x10 if the query values are
+     * compatible with UPnP Device Architecture 1.0.
+     * @hide
+     */
+    public static final int VERSION_1_0 = 0x10;
+
+    /**
+     * This constructor is only used in newInstance().
+     *
+     * @param queryList
+     */
+    private WifiP2pUpnpServiceInfo(List<String> queryList) {
+        super(queryList);
+    }
+
+    /**
+     * Create UPnP service information object.
+     *
+     * @param uuid a string representation of this UUID in the following format,
+     *  as per <a href="http://www.ietf.org/rfc/rfc4122.txt">RFC 4122</a>.<br>
+     *  e.g) 6859dede-8574-59ab-9332-123456789012
+     * @param device a string representation of this device in the following format,
+     * as per
+     * <a href="http://www.upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.1.pdf">
+     *  UPnP Device Architecture1.1</a><br>
+     *  e.g) urn:schemas-upnp-org:device:MediaServer:1
+     * @param services a string representation of this service in the following format,
+     * as per
+     * <a href="http://www.upnp.org/specs/arch/UPnP-arch-DeviceArchitecture-v1.1.pdf">
+     *  UPnP Device Architecture1.1</a><br>
+     *  e.g) urn:schemas-upnp-org:service:ContentDirectory:1
+     * @return UPnP service information object.
+     */
+    public static WifiP2pUpnpServiceInfo newInstance(String uuid,
+            String device, List<String> services) {
+        if (uuid == null || device == null) {
+            throw new IllegalArgumentException("uuid or device cannnot be null");
+        }
+        UUID.fromString(uuid);
+
+        ArrayList<String> info = new ArrayList<String>();
+
+        info.add(createSupplicantQuery(uuid, null));
+        info.add(createSupplicantQuery(uuid, "upnp:rootdevice"));
+        info.add(createSupplicantQuery(uuid, device));
+        if (services != null) {
+            for (String service:services) {
+                info.add(createSupplicantQuery(uuid, service));
+            }
+        }
+
+        return new WifiP2pUpnpServiceInfo(info);
+    }
+
+    /**
+     * Create wpa_supplicant service query for upnp.
+     *
+     * @param uuid
+     * @param data
+     * @return wpa_supplicant service query for upnp
+     */
+    private static String createSupplicantQuery(String uuid, String data) {
+        StringBuffer sb = new StringBuffer();
+        sb.append("upnp ");
+        sb.append(String.format(Locale.US, "%02x ", VERSION_1_0));
+        sb.append("uuid:");
+        sb.append(uuid);
+        if (data != null) {
+            sb.append("::");
+            sb.append(data);
+        }
+        return sb.toString();
+    }
+}
diff --git a/android/net/wifi/p2p/nsd/WifiP2pUpnpServiceRequest.java b/android/net/wifi/p2p/nsd/WifiP2pUpnpServiceRequest.java
new file mode 100644
index 0000000..98e447e
--- /dev/null
+++ b/android/net/wifi/p2p/nsd/WifiP2pUpnpServiceRequest.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2012 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.wifi.p2p.nsd;
+
+import android.net.wifi.p2p.WifiP2pManager;
+
+import java.util.Locale;
+
+/**
+ * A class for creating a Upnp service discovery request for use with
+ * {@link WifiP2pManager#addServiceRequest} and {@link WifiP2pManager#removeServiceRequest}
+ *
+ * {@see WifiP2pManager}
+ * {@see WifiP2pServiceRequest}
+ * {@see WifiP2pDnsSdServiceRequest}
+ */
+public class WifiP2pUpnpServiceRequest extends WifiP2pServiceRequest {
+
+    /**
+     * This constructor is only used in newInstance().
+     *
+     * @param query The part of service specific query.
+     * @hide
+     */
+    protected WifiP2pUpnpServiceRequest(String query) {
+        super(WifiP2pServiceInfo.SERVICE_TYPE_UPNP, query);
+    }
+
+    /**
+     * This constructor is only used in newInstance().
+     * @hide
+     */
+    protected WifiP2pUpnpServiceRequest() {
+        super(WifiP2pServiceInfo.SERVICE_TYPE_UPNP, null);
+    }
+
+    /**
+     * Create a service discovery request to search all UPnP services.
+     *
+     * @return service request for UPnP.
+     */
+    public static WifiP2pUpnpServiceRequest newInstance() {
+        return new WifiP2pUpnpServiceRequest();
+    }
+    /**
+     * Create a service discovery request to search specified UPnP services.
+     *
+     * @param st ssdp search target.  Cannot be null.<br>
+     * e.g ) <br>
+     * <ul>
+     * <li>"ssdp:all"
+     * <li>"upnp:rootdevice"
+     * <li>"urn:schemas-upnp-org:device:MediaServer:2"
+     * <li>"urn:schemas-upnp-org:service:ContentDirectory:2"
+     * <li>"uuid:6859dede-8574-59ab-9332-123456789012"
+     * </ul>
+     * @return service request for UPnP.
+     */
+    public static WifiP2pUpnpServiceRequest newInstance(String st) {
+        if (st == null) {
+            throw new IllegalArgumentException("search target cannot be null");
+        }
+        StringBuffer sb = new StringBuffer();
+        sb.append(String.format(Locale.US, "%02x", WifiP2pUpnpServiceInfo.VERSION_1_0));
+        sb.append(WifiP2pServiceInfo.bin2HexStr(st.getBytes()));
+        return new WifiP2pUpnpServiceRequest(sb.toString());
+    }
+}
diff --git a/android/net/wifi/p2p/nsd/WifiP2pUpnpServiceResponse.java b/android/net/wifi/p2p/nsd/WifiP2pUpnpServiceResponse.java
new file mode 100644
index 0000000..ab95af6
--- /dev/null
+++ b/android/net/wifi/p2p/nsd/WifiP2pUpnpServiceResponse.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright (C) 2012 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.wifi.p2p.nsd;
+
+import android.net.wifi.p2p.WifiP2pDevice;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A class for a response of upnp service discovery.
+ *
+ * @hide
+ */
+public class WifiP2pUpnpServiceResponse extends WifiP2pServiceResponse {
+
+    /**
+     * UPnP version. should be {@link WifiP2pUpnpServiceInfo#VERSION_1_0}
+     */
+    private int mVersion;
+
+    /**
+     * The list of Unique Service Name.
+     * e.g)
+     *{"uuid:6859dede-8574-59ab-9332-123456789012",
+     *"uuid:6859dede-8574-59ab-9332-123456789012::upnp:rootdevice"}
+     */
+    private List<String> mUniqueServiceNames;
+
+    /**
+     * Return UPnP version number.
+     *
+     * @return version number.
+     * @see WifiP2pUpnpServiceInfo#VERSION_1_0
+     */
+    public int getVersion() {
+        return mVersion;
+    }
+
+    /**
+     * Return Unique Service Name strings.
+     *
+     * @return Unique Service Name.<br>
+     * e.g ) <br>
+     * <ul>
+     * <li>"uuid:6859dede-8574-59ab-9332-123456789012"
+     * <li>"uuid:6859dede-8574-59ab-9332-123456789012::upnp:rootdevice"
+     * <li>"uuid:6859dede-8574-59ab-9332-123456789012::urn:schemas-upnp-org:device:
+     * MediaServer:2"
+     * <li>"uuid:6859dede-8574-59ab-9332-123456789012::urn:schemas-upnp-org:service:
+     * ContentDirectory:2"
+     * </ul>
+     */
+    public List<String> getUniqueServiceNames() {
+        return mUniqueServiceNames;
+    }
+
+    /**
+     * hidden constructor.
+     *
+     * @param status status code
+     * @param transId transaction id
+     * @param dev source device
+     * @param data UPnP response data.
+     */
+    protected WifiP2pUpnpServiceResponse(int status,
+            int transId, WifiP2pDevice dev, byte[] data) {
+        super(WifiP2pServiceInfo.SERVICE_TYPE_UPNP,
+                status, transId, dev, data);
+        if (!parse()) {
+            throw new IllegalArgumentException("Malformed upnp service response");
+        }
+    }
+
+    /**
+     * Parse UPnP service discovery response
+     *
+     * @return {@code true} if the operation succeeded
+     */
+    private boolean parse() {
+        /*
+         * The data format is as follows.
+         *
+         * ______________________________________________________
+         * |  Version (1)  |          USN (Variable)            |
+         */
+        if (mData == null) {
+            // the empty is OK.
+            return true;
+        }
+
+        if (mData.length < 1) {
+            return false;
+        }
+
+        mVersion = mData[0] & 0xff;
+        String[] names = new String(mData, 1, mData.length-1).split(",");
+        mUniqueServiceNames = new ArrayList<String>();
+        for (String name : names) {
+            mUniqueServiceNames.add(name);
+        }
+        return true;
+    }
+
+    @Override
+    public String toString() {
+        StringBuffer sbuf = new StringBuffer();
+        sbuf.append("serviceType:UPnP(").append(mServiceType).append(")");
+        sbuf.append(" status:").append(Status.toString(mStatus));
+        sbuf.append(" srcAddr:").append(mDevice.deviceAddress);
+        sbuf.append(" version:").append(String.format("%02x", mVersion));
+        if (mUniqueServiceNames != null) {
+            for (String name : mUniqueServiceNames) {
+                sbuf.append(" usn:").append(name);
+            }
+        }
+        return sbuf.toString();
+    }
+
+    /**
+     * Create upnp service response.
+     *
+     * <pre>This is only used in{@link WifiP2pServiceResponse}
+     *
+     * @param status status code.
+     * @param transId transaction id.
+     * @param device source device.
+     * @param data UPnP response data.
+     * @return UPnP service response data.
+     * @hide
+     */
+    static WifiP2pUpnpServiceResponse newInstance(int status,
+            int transId, WifiP2pDevice device, byte[] data) {
+        if (status != WifiP2pServiceResponse.Status.SUCCESS) {
+            return new WifiP2pUpnpServiceResponse(status, transId, device, null);
+        }
+
+        try {
+            return new WifiP2pUpnpServiceResponse(status, transId, device, data);
+        } catch (IllegalArgumentException e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+}
diff --git a/android/net/wifi/rtt/CivicLocation.java b/android/net/wifi/rtt/CivicLocation.java
new file mode 100644
index 0000000..1d41177
--- /dev/null
+++ b/android/net/wifi/rtt/CivicLocation.java
@@ -0,0 +1,333 @@
+/*
+ * Copyright (C) 2018 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.wifi.rtt;
+
+import android.annotation.Nullable;
+import android.location.Address;
+import android.net.wifi.rtt.CivicLocationKeys.CivicLocationKeysType;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.Parcelable.Creator;
+import android.util.SparseArray;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Locale;
+import java.util.Objects;
+
+/**
+ * Decodes the Type Length Value (TLV) elements found in a Location Civic Record as defined by IEEE
+ * P802.11-REVmc/D8.0 section 9.4.2.22.13 using the format described in IETF RFC 4776.
+ *
+ * <p>The TLVs each define a key, value pair for a civic address type such as apt, street, city,
+ * county, and country. The class provides a general getter method to extract a value for an element
+ * key, returning null if not set.
+ *
+ * @hide
+ */
+public final class CivicLocation implements Parcelable {
+    // Address (class) line indexes
+    private static final int ADDRESS_LINE_0_ROOM_DESK_FLOOR = 0;
+    private static final int ADDRESS_LINE_1_NUMBER_ROAD_SUFFIX_APT = 1;
+    private static final int ADDRESS_LINE_2_CITY = 2;
+    private static final int ADDRESS_LINE_3_STATE_POSTAL_CODE = 3;
+    private static final int ADDRESS_LINE_4_COUNTRY = 4;
+
+    // Buffer management
+    private static final int MIN_CIVIC_BUFFER_SIZE = 3;
+    private static final int MAX_CIVIC_BUFFER_SIZE = 256;
+    private static final int COUNTRY_CODE_LENGTH = 2;
+    private static final int BYTE_MASK = 0xFF;
+    private static final int TLV_TYPE_INDEX = 0;
+    private static final int TLV_LENGTH_INDEX = 1;
+    private static final int TLV_VALUE_INDEX = 2;
+
+    private final boolean mIsValid;
+    private final String mCountryCode; // Two character country code (ISO 3166 standard).
+    private SparseArray<String> mCivicAddressElements =
+            new SparseArray<>(MIN_CIVIC_BUFFER_SIZE);
+
+
+    /**
+     * Constructor
+     *
+     * @param civicTLVs    a byte buffer containing parameters in the form type, length, value
+     * @param countryCode the two letter code defined by the ISO 3166 standard
+     *
+     * @hide
+     */
+    public CivicLocation(@Nullable byte[] civicTLVs, @Nullable String countryCode) {
+        this.mCountryCode = countryCode;
+        if (countryCode == null || countryCode.length() != COUNTRY_CODE_LENGTH) {
+            this.mIsValid = false;
+            return;
+        }
+        boolean isValid = false;
+        if (civicTLVs != null
+                && civicTLVs.length >= MIN_CIVIC_BUFFER_SIZE
+                && civicTLVs.length < MAX_CIVIC_BUFFER_SIZE) {
+            isValid = parseCivicTLVs(civicTLVs);
+        }
+
+        mIsValid = isValid;
+    }
+
+    private CivicLocation(Parcel in) {
+        mIsValid = in.readByte() != 0;
+        mCountryCode = in.readString();
+        mCivicAddressElements = in.readSparseArray(this.getClass().getClassLoader());
+    }
+
+    public static final @android.annotation.NonNull Creator<CivicLocation> CREATOR = new Creator<CivicLocation>() {
+        @Override
+        public CivicLocation createFromParcel(Parcel in) {
+            return new CivicLocation(in);
+        }
+
+        @Override
+        public CivicLocation[] newArray(int size) {
+            return new CivicLocation[size];
+        }
+    };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel parcel, int flags) {
+        parcel.writeByte((byte) (mIsValid ? 1 : 0));
+        parcel.writeString(mCountryCode);
+        parcel.writeSparseArray((android.util.SparseArray) mCivicAddressElements);
+    }
+
+    /**
+     * Check TLV format and store TLV key/value pairs in this object so they can be queried by key.
+     *
+     * @param civicTLVs the buffer of TLV elements
+     * @return a boolean indicating success of the parsing process
+     */
+    private boolean parseCivicTLVs(byte[] civicTLVs) {
+        int bufferPtr = 0;
+        int bufferLength = civicTLVs.length;
+
+        // Iterate through the sub-elements contained in the LCI IE checking the accumulated
+        // element lengths do not overflow the total buffer length
+        while (bufferPtr < bufferLength) {
+            int civicAddressType = civicTLVs[bufferPtr + TLV_TYPE_INDEX] & BYTE_MASK;
+            int civicAddressTypeLength = civicTLVs[bufferPtr + TLV_LENGTH_INDEX];
+            if (civicAddressTypeLength != 0) {
+                if (bufferPtr + TLV_VALUE_INDEX + civicAddressTypeLength > bufferLength) {
+                    return false;
+                }
+                mCivicAddressElements.put(civicAddressType,
+                        new String(civicTLVs, bufferPtr + TLV_VALUE_INDEX,
+                                civicAddressTypeLength, StandardCharsets.UTF_8));
+            }
+            bufferPtr += civicAddressTypeLength + TLV_VALUE_INDEX;
+        }
+        return true;
+    }
+
+    /**
+     * Getter for the value of a civic Address element type.
+     *
+     * @param key an integer code for the element type key
+     * @return the string value associated with that element type
+     */
+    @Nullable
+    public String getCivicElementValue(@CivicLocationKeysType int key) {
+        return mCivicAddressElements.get(key);
+    }
+
+    /**
+     * Converts a CivicLocation object to a SparseArray.
+     *
+     * @return the SparseArray<string> representation of the CivicLocation
+     */
+    @Nullable
+    public SparseArray<String> toSparseArray() {
+        return mCivicAddressElements;
+    }
+
+    /**
+     * Generates a comma separated string of all the defined elements.
+     *
+     * @return a compiled string representing all elements
+     */
+    @Override
+    public String toString() {
+        return mCivicAddressElements.toString();
+    }
+
+    /**
+     * Converts Civic Location to the best effort Address Object.
+     *
+     * @return the {@link Address} object based on the Civic Location data
+     */
+    @Nullable
+    public Address toAddress() {
+        if (!mIsValid) {
+            return null;
+        }
+        Address address = new Address(Locale.US);
+        String room = formatAddressElement("Room: ", getCivicElementValue(CivicLocationKeys.ROOM));
+        String desk =
+                formatAddressElement(" Desk: ", getCivicElementValue(CivicLocationKeys.DESK));
+        String floor =
+                formatAddressElement(", Flr: ", getCivicElementValue(CivicLocationKeys.FLOOR));
+        String houseNumber = formatAddressElement("", getCivicElementValue(CivicLocationKeys.HNO));
+        String houseNumberSuffix =
+                formatAddressElement("", getCivicElementValue(CivicLocationKeys.HNS));
+        String road =
+                formatAddressElement(" ", getCivicElementValue(
+                        CivicLocationKeys.PRIMARY_ROAD_NAME));
+        String roadSuffix = formatAddressElement(" ", getCivicElementValue(CivicLocationKeys.STS));
+        String apt = formatAddressElement(", Apt: ", getCivicElementValue(CivicLocationKeys.APT));
+        String city = formatAddressElement("", getCivicElementValue(CivicLocationKeys.CITY));
+        String state = formatAddressElement("", getCivicElementValue(CivicLocationKeys.STATE));
+        String postalCode =
+                formatAddressElement(" ", getCivicElementValue(CivicLocationKeys.POSTAL_CODE));
+
+        // Aggregation into common address format
+        String addressLine0 =
+                new StringBuilder().append(room).append(desk).append(floor).toString();
+        String addressLine1 =
+                new StringBuilder().append(houseNumber).append(houseNumberSuffix).append(road)
+                        .append(roadSuffix).append(apt).toString();
+        String addressLine2 = city;
+        String addressLine3 = new StringBuilder().append(state).append(postalCode).toString();
+        String addressLine4 = mCountryCode;
+
+        // Setting Address object line fields by common convention.
+        address.setAddressLine(ADDRESS_LINE_0_ROOM_DESK_FLOOR, addressLine0);
+        address.setAddressLine(ADDRESS_LINE_1_NUMBER_ROAD_SUFFIX_APT, addressLine1);
+        address.setAddressLine(ADDRESS_LINE_2_CITY, addressLine2);
+        address.setAddressLine(ADDRESS_LINE_3_STATE_POSTAL_CODE, addressLine3);
+        address.setAddressLine(ADDRESS_LINE_4_COUNTRY, addressLine4);
+
+        // Other compatible fields between the CIVIC_ADDRESS and the Address Class.
+        address.setFeatureName(getCivicElementValue(CivicLocationKeys.NAM)); // Structure name
+        address.setSubThoroughfare(getCivicElementValue(CivicLocationKeys.HNO));
+        address.setThoroughfare(getCivicElementValue(CivicLocationKeys.PRIMARY_ROAD_NAME));
+        address.setSubLocality(getCivicElementValue(CivicLocationKeys.NEIGHBORHOOD));
+        address.setSubAdminArea(getCivicElementValue(CivicLocationKeys.COUNTY));
+        address.setAdminArea(getCivicElementValue(CivicLocationKeys.STATE));
+        address.setPostalCode(getCivicElementValue(CivicLocationKeys.POSTAL_CODE));
+        address.setCountryCode(mCountryCode); // Country
+        return address;
+    }
+
+    /**
+     * Prepares an address element so that it can be integrated into an address line convention.
+     *
+     * <p>If an address element is null, the return string will be empty e.g. "".
+     *
+     * @param label a string defining the type of address element
+     * @param value a string defining the elements value
+     * @return the formatted version of the value, with null values converted to empty strings
+     */
+    private String formatAddressElement(String label, String value) {
+        if (value != null) {
+            return label + value;
+        } else {
+            return "";
+        }
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (!(obj instanceof CivicLocation)) {
+            return false;
+        }
+        CivicLocation other = (CivicLocation) obj;
+        return mIsValid == other.mIsValid
+                && Objects.equals(mCountryCode, other.mCountryCode)
+                && isSparseArrayStringEqual(mCivicAddressElements, other.mCivicAddressElements);
+    }
+
+    @Override
+    public int hashCode() {
+        int[] civicAddressKeys = getSparseArrayKeys(mCivicAddressElements);
+        String[] civicAddressValues = getSparseArrayValues(mCivicAddressElements);
+        return Objects.hash(mIsValid, mCountryCode, civicAddressKeys, civicAddressValues);
+    }
+
+    /**
+     * Tests if the Civic Location object is valid
+     *
+     * @return a boolean defining mIsValid
+     */
+    public boolean isValid() {
+        return mIsValid;
+    }
+
+    /**
+     * Tests if two sparse arrays are equal on a key for key basis
+     *
+     * @param sa1 the first sparse array
+     * @param sa2 the second sparse array
+     * @return the boolean result after comparing values key by key
+     */
+    private boolean isSparseArrayStringEqual(SparseArray<String> sa1, SparseArray<String> sa2) {
+        int size = sa1.size();
+        if (size != sa2.size()) {
+            return false;
+        }
+        for (int i = 0; i < size; i++) {
+            String sa1Value = sa1.valueAt(i);
+            String sa2Value = sa2.valueAt(i);
+            if (!sa1Value.equals(sa2Value)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Extract an array of all the keys in a SparseArray<String>
+     *
+     * @param sa the sparse array of Strings
+     * @return an integer array of all keys in the SparseArray<String>
+     */
+    private int[] getSparseArrayKeys(SparseArray<String> sa) {
+        int size = sa.size();
+        int[] keys = new int[size];
+        for (int i = 0; i < size; i++) {
+            keys[i] = sa.keyAt(i);
+        }
+        return keys;
+    }
+
+    /**
+     * Extract an array of all the String values in a SparseArray<String>
+     *
+     * @param sa the sparse array of Strings
+     * @return a String array of all values in the SparseArray<String>
+     */
+    private String[] getSparseArrayValues(SparseArray<String> sa) {
+        int size = sa.size();
+        String[] values = new String[size];
+        for (int i = 0; i < size; i++) {
+            values[i] = sa.valueAt(i);
+        }
+        return values;
+    }
+}
diff --git a/android/net/wifi/rtt/CivicLocationKeys.java b/android/net/wifi/rtt/CivicLocationKeys.java
new file mode 100644
index 0000000..b03f4a9
--- /dev/null
+++ b/android/net/wifi/rtt/CivicLocationKeys.java
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2018 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.wifi.rtt;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.annotation.IntDef;
+
+import java.lang.annotation.Retention;
+
+/**
+ * Civic Address key types used to define address elements.
+ *
+ * <p>These keys can be used with {@code ResponderLocation.toCivicLocationSparseArray()}
+ * to look-up the corresponding string values.</p>
+ */
+public class CivicLocationKeys {
+
+    /**
+     * An enumeration of all civic location keys.
+     *
+     * @hide
+     */
+    @Retention(SOURCE)
+    @IntDef({LANGUAGE, STATE, COUNTY, CITY, BOROUGH, NEIGHBORHOOD, GROUP_OF_STREETS, PRD, POD, STS,
+            HNO, HNS, LMK, LOC, NAM, POSTAL_CODE, BUILDING, APT, FLOOR, ROOM, TYPE_OF_PLACE, PCN,
+            PO_BOX, ADDITIONAL_CODE, DESK, PRIMARY_ROAD_NAME, ROAD_SECTION, BRANCH_ROAD_NAME,
+            SUBBRANCH_ROAD_NAME, STREET_NAME_PRE_MODIFIER, STREET_NAME_POST_MODIFIER, SCRIPT})
+    public @interface CivicLocationKeysType {
+    }
+
+    /** Language key e.g. i-default. */
+    public static final int LANGUAGE = 0;
+    /** Category label A1 key e.g. California. */
+    public static final int STATE = 1;
+    /** Category label A2 key e.g. Marin. */
+    public static final int COUNTY = 2;
+    /** Category label A3 key e.g. San Francisco. */
+    public static final int CITY = 3;
+    /** Category label A4 key e.g. Westminster. */
+    public static final int BOROUGH = 4;
+    /** Category label A5 key e.g. Pacific Heights. */
+    public static final int NEIGHBORHOOD = 5;
+    /** Category label A6 key e.g. University District. */
+    public static final int GROUP_OF_STREETS = 6;
+    // 7 - 15 not defined
+    /** Leading Street direction key e.g. N. */
+    public static final int PRD = 16;
+    /** Trailing street suffix key e.g. SW. */
+    public static final int POD = 17;
+    /** Street suffix or Type key e.g Ave, Platz. */
+    public static final int STS = 18;
+    /** House Number key e.g. 123. */
+    public static final int HNO = 19;
+    /** House number suffix key e.g. A, 1/2. */
+    public static final int HNS = 20;
+    /** Landmark or vanity address key e.g. Golden Gate Bridge. */
+    public static final int LMK = 21;
+    /** Additional Location info key e.g. South Wing. */
+    public static final int LOC = 22;
+    /** Name of residence key e.g. Joe's Barbershop. */
+    public static final int NAM = 23;
+    /** Postal or ZIP code key e.g. 10027-1234. */
+    public static final int POSTAL_CODE = 24;
+    /** Building key e.g. Lincoln Library. */
+    public static final int BUILDING = 25;
+    /** Apartment or suite key e.g. Apt 42. */
+    public static final int APT = 26;
+    /** Floor key e.g. 4. */
+    public static final int FLOOR = 27;
+    /** Room key e.g. 450F. */
+    public static final int ROOM = 28;
+    /** Type of place key e.g. office. */
+    public static final int TYPE_OF_PLACE = 29;
+    /** Postal community name key e.g. Leonia. */
+    public static final int PCN = 30;
+    /** Post Office Box key e.g. 12345. */
+    public static final int PO_BOX = 31;
+    /** Additional Code key e.g. 13203000003. */
+    public static final int ADDITIONAL_CODE = 32;
+    /** Seat, desk, pole, or cubical key e.g. WS 181. */
+    public static final int DESK = 33;
+    /** Primary road name key e.g. Shoreline. */
+    public static final int PRIMARY_ROAD_NAME = 34;
+    /** Road Section key e.g. 14. */
+    public static final int ROAD_SECTION = 35;
+    /** Branch Rd Name key e.g. Lane 7. */
+    public static final int BRANCH_ROAD_NAME = 36;
+    /** Subbranch Rd Name key e.g. Alley 8. */
+    public static final int SUBBRANCH_ROAD_NAME = 37;
+    /** Premodifier key e.g. Old. */
+    public static final int STREET_NAME_PRE_MODIFIER = 38;
+    /** Postmodifier key e.g. Service. */
+    public static final int STREET_NAME_POST_MODIFIER = 39;
+    /** Script key e.g. Latn. */
+    public static final int SCRIPT = 128;
+
+    /** private constructor */
+    private CivicLocationKeys() {}
+}
+
diff --git a/android/net/wifi/rtt/RangingRequest.java b/android/net/wifi/rtt/RangingRequest.java
new file mode 100644
index 0000000..70af03e
--- /dev/null
+++ b/android/net/wifi/rtt/RangingRequest.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.rtt;
+
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.net.MacAddress;
+import android.net.wifi.ScanResult;
+import android.net.wifi.aware.AttachCallback;
+import android.net.wifi.aware.DiscoverySessionCallback;
+import android.net.wifi.aware.IdentityChangedListener;
+import android.net.wifi.aware.PeerHandle;
+import android.net.wifi.aware.WifiAwareManager;
+import android.os.Handler;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.StringJoiner;
+
+/**
+ * Defines the ranging request to other devices. The ranging request is built using
+ * {@link RangingRequest.Builder}.
+ * A ranging request is executed using
+ * {@link WifiRttManager#startRanging(RangingRequest, java.util.concurrent.Executor, RangingResultCallback)}.
+ * <p>
+ * The ranging request is a batch request - specifying a set of devices (specified using
+ * {@link RangingRequest.Builder#addAccessPoint(ScanResult)} and
+ * {@link RangingRequest.Builder#addAccessPoints(List)}).
+ */
+public final class RangingRequest implements Parcelable {
+    private static final int MAX_PEERS = 10;
+
+    /**
+     * Returns the maximum number of peers to range which can be specified in a single {@code
+     * RangingRequest}. The limit applies no matter how the peers are added to the request, e.g.
+     * through {@link RangingRequest.Builder#addAccessPoint(ScanResult)} or
+     * {@link RangingRequest.Builder#addAccessPoints(List)}.
+     *
+     * @return Maximum number of peers.
+     */
+    public static int getMaxPeers() {
+        return MAX_PEERS;
+    }
+
+    /** @hide */
+    public final List<ResponderConfig> mRttPeers;
+
+    /** @hide */
+    private RangingRequest(List<ResponderConfig> rttPeers) {
+        mRttPeers = rttPeers;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeList(mRttPeers);
+    }
+
+    public static final @android.annotation.NonNull Creator<RangingRequest> CREATOR = new Creator<RangingRequest>() {
+        @Override
+        public RangingRequest[] newArray(int size) {
+            return new RangingRequest[size];
+        }
+
+        @Override
+        public RangingRequest createFromParcel(Parcel in) {
+            return new RangingRequest(in.readArrayList(null));
+        }
+    };
+
+    /** @hide */
+    @Override
+    public String toString() {
+        StringJoiner sj = new StringJoiner(", ", "RangingRequest: mRttPeers=[", "]");
+        for (ResponderConfig rc : mRttPeers) {
+            sj.add(rc.toString());
+        }
+        return sj.toString();
+    }
+
+    /** @hide */
+    public void enforceValidity(boolean awareSupported) {
+        if (mRttPeers.size() > MAX_PEERS) {
+            throw new IllegalArgumentException(
+                    "Ranging to too many peers requested. Use getMaxPeers() API to get limit.");
+        }
+
+        for (ResponderConfig peer: mRttPeers) {
+            if (!peer.isValid(awareSupported)) {
+                throw new IllegalArgumentException("Invalid Responder specification");
+            }
+        }
+    }
+
+    /**
+     * Builder class used to construct {@link RangingRequest} objects.
+     */
+    public static final class Builder {
+        private List<ResponderConfig> mRttPeers = new ArrayList<>();
+
+        /**
+         * Add the device specified by the {@link ScanResult} to the list of devices with
+         * which to measure range. The total number of peers added to a request cannot exceed the
+         * limit specified by {@link #getMaxPeers()}.
+         * <p>
+         * Ranging may not be supported if the Access Point does not support IEEE 802.11mc. Use
+         * {@link ScanResult#is80211mcResponder()} to verify the Access Point's capabilities. If
+         * not supported the result status will be
+         * {@link RangingResult#STATUS_RESPONDER_DOES_NOT_SUPPORT_IEEE80211MC}.
+         *
+         * @param apInfo Information of an Access Point (AP) obtained in a Scan Result.
+         * @return The builder to facilitate chaining
+         *         {@code builder.setXXX(..).setXXX(..)}.
+         */
+        public Builder addAccessPoint(@NonNull ScanResult apInfo) {
+            if (apInfo == null) {
+                throw new IllegalArgumentException("Null ScanResult!");
+            }
+            return addResponder(ResponderConfig.fromScanResult(apInfo));
+        }
+
+        /**
+         * Add the devices specified by the {@link ScanResult}s to the list of devices with
+         * which to measure range. The total number of peers added to a request cannot exceed the
+         * limit specified by {@link #getMaxPeers()}.
+         * <p>
+         * Ranging may not be supported if the Access Point does not support IEEE 802.11mc. Use
+         * {@link ScanResult#is80211mcResponder()} to verify the Access Point's capabilities. If
+         * not supported the result status will be
+         * {@link RangingResult#STATUS_RESPONDER_DOES_NOT_SUPPORT_IEEE80211MC}.
+         *
+         * @param apInfos Information of an Access Points (APs) obtained in a Scan Result.
+         * @return The builder to facilitate chaining
+         *         {@code builder.setXXX(..).setXXX(..)}.
+         */
+        public Builder addAccessPoints(@NonNull List<ScanResult> apInfos) {
+            if (apInfos == null) {
+                throw new IllegalArgumentException("Null list of ScanResults!");
+            }
+            for (ScanResult scanResult : apInfos) {
+                addAccessPoint(scanResult);
+            }
+            return this;
+        }
+
+        /**
+         * Add the device specified by the {@code peerMacAddress} to the list of devices with
+         * which to measure range.
+         * <p>
+         * The MAC address may be obtained out-of-band from a peer Wi-Fi Aware device. A Wi-Fi
+         * Aware device may obtain its MAC address using the {@link IdentityChangedListener}
+         * provided to
+         * {@link WifiAwareManager#attach(AttachCallback, IdentityChangedListener, Handler)}.
+         * <p>
+         * Note: in order to use this API the device must support Wi-Fi Aware
+         * {@link android.net.wifi.aware}. The peer device which is being ranged to must be
+         * configured to publish a service (with any name) with:
+         * <li>Type {@link android.net.wifi.aware.PublishConfig#PUBLISH_TYPE_UNSOLICITED}.
+         * <li>Ranging enabled
+         * {@link android.net.wifi.aware.PublishConfig.Builder#setRangingEnabled(boolean)}.
+         *
+         * @param peerMacAddress The MAC address of the Wi-Fi Aware peer.
+         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+         */
+        public Builder addWifiAwarePeer(@NonNull MacAddress peerMacAddress) {
+            if (peerMacAddress == null) {
+                throw new IllegalArgumentException("Null peer MAC address");
+            }
+            return addResponder(
+                    ResponderConfig.fromWifiAwarePeerMacAddressWithDefaults(peerMacAddress));
+        }
+
+        /**
+         * Add a device specified by a {@link PeerHandle} to the list of devices with which to
+         * measure range.
+         * <p>
+         * The {@link PeerHandle} may be obtained as part of the Wi-Fi Aware discovery process. E.g.
+         * using {@link DiscoverySessionCallback#onServiceDiscovered(PeerHandle, byte[], List)}.
+         * <p>
+         * Note: in order to use this API the device must support Wi-Fi Aware
+         * {@link android.net.wifi.aware}. The peer device which is being ranged to must be
+         * configured to publish a service (with any name) with:
+         * <li>Type {@link android.net.wifi.aware.PublishConfig#PUBLISH_TYPE_UNSOLICITED}.
+         * <li>Ranging enabled
+         * {@link android.net.wifi.aware.PublishConfig.Builder#setRangingEnabled(boolean)}.
+         *
+         * @param peerHandle The peer handler of the peer Wi-Fi Aware device.
+         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+         */
+        public Builder addWifiAwarePeer(@NonNull PeerHandle peerHandle) {
+            if (peerHandle == null) {
+                throw new IllegalArgumentException("Null peer handler (identifier)");
+            }
+
+            return addResponder(ResponderConfig.fromWifiAwarePeerHandleWithDefaults(peerHandle));
+        }
+
+        /**
+         * Add the Responder device specified by the {@link ResponderConfig} to the list of devices
+         * with which to measure range. The total number of peers added to the request cannot exceed
+         * the limit specified by {@link #getMaxPeers()}.
+         *
+         * @param responder Information on the RTT Responder.
+         * @return The builder, to facilitate chaining {@code builder.setXXX(..).setXXX(..)}.
+         *
+         * @hide
+         */
+        @SystemApi
+        public Builder addResponder(@NonNull ResponderConfig responder) {
+            if (responder == null) {
+                throw new IllegalArgumentException("Null Responder!");
+            }
+
+            mRttPeers.add(responder);
+            return this;
+        }
+
+        /**
+         * Build {@link RangingRequest} given the current configurations made on the
+         * builder.
+         */
+        public RangingRequest build() {
+            return new RangingRequest(mRttPeers);
+        }
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+
+        if (!(o instanceof RangingRequest)) {
+            return false;
+        }
+
+        RangingRequest lhs = (RangingRequest) o;
+
+        return mRttPeers.size() == lhs.mRttPeers.size() && mRttPeers.containsAll(lhs.mRttPeers);
+    }
+
+    @Override
+    public int hashCode() {
+        return mRttPeers.hashCode();
+    }
+}
diff --git a/android/net/wifi/rtt/RangingResult.java b/android/net/wifi/rtt/RangingResult.java
new file mode 100644
index 0000000..a065bbc
--- /dev/null
+++ b/android/net/wifi/rtt/RangingResult.java
@@ -0,0 +1,438 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.rtt;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.net.MacAddress;
+import android.net.wifi.aware.PeerHandle;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Ranging result for a request started by
+ * {@link WifiRttManager#startRanging(RangingRequest, java.util.concurrent.Executor, RangingResultCallback)}.
+ * Results are returned in {@link RangingResultCallback#onRangingResults(List)}.
+ * <p>
+ * A ranging result is the distance measurement result for a single device specified in the
+ * {@link RangingRequest}.
+ */
+public final class RangingResult implements Parcelable {
+    private static final String TAG = "RangingResult";
+    private static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
+
+    /** @hide */
+    @IntDef({STATUS_SUCCESS, STATUS_FAIL, STATUS_RESPONDER_DOES_NOT_SUPPORT_IEEE80211MC})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface RangeResultStatus {
+    }
+
+    /**
+     * Individual range request status, {@link #getStatus()}. Indicates ranging operation was
+     * successful and distance value is valid.
+     */
+    public static final int STATUS_SUCCESS = 0;
+
+    /**
+     * Individual range request status, {@link #getStatus()}. Indicates ranging operation failed
+     * and the distance value is invalid.
+     */
+    public static final int STATUS_FAIL = 1;
+
+    /**
+     * Individual range request status, {@link #getStatus()}. Indicates that the ranging operation
+     * failed because the specified peer does not support IEEE 802.11mc RTT operations. Support by
+     * an Access Point can be confirmed using
+     * {@link android.net.wifi.ScanResult#is80211mcResponder()}.
+     * <p>
+     * On such a failure, the individual result fields of {@link RangingResult} such as
+     * {@link RangingResult#getDistanceMm()} are invalid.
+     */
+    public static final int STATUS_RESPONDER_DOES_NOT_SUPPORT_IEEE80211MC = 2;
+
+    private final int mStatus;
+    private final MacAddress mMac;
+    private final PeerHandle mPeerHandle;
+    private final int mDistanceMm;
+    private final int mDistanceStdDevMm;
+    private final int mRssi;
+    private final int mNumAttemptedMeasurements;
+    private final int mNumSuccessfulMeasurements;
+    private final byte[] mLci;
+    private final byte[] mLcr;
+    private final ResponderLocation mResponderLocation;
+    private final long mTimestamp;
+
+    /** @hide */
+    public RangingResult(@RangeResultStatus int status, @NonNull MacAddress mac, int distanceMm,
+            int distanceStdDevMm, int rssi, int numAttemptedMeasurements,
+            int numSuccessfulMeasurements, byte[] lci, byte[] lcr,
+            ResponderLocation responderLocation, long timestamp) {
+        mStatus = status;
+        mMac = mac;
+        mPeerHandle = null;
+        mDistanceMm = distanceMm;
+        mDistanceStdDevMm = distanceStdDevMm;
+        mRssi = rssi;
+        mNumAttemptedMeasurements = numAttemptedMeasurements;
+        mNumSuccessfulMeasurements = numSuccessfulMeasurements;
+        mLci = lci == null ? EMPTY_BYTE_ARRAY : lci;
+        mLcr = lcr == null ? EMPTY_BYTE_ARRAY : lcr;
+        mResponderLocation = responderLocation;
+        mTimestamp = timestamp;
+    }
+
+    /** @hide */
+    public RangingResult(@RangeResultStatus int status, PeerHandle peerHandle, int distanceMm,
+            int distanceStdDevMm, int rssi, int numAttemptedMeasurements,
+            int numSuccessfulMeasurements, byte[] lci, byte[] lcr,
+            ResponderLocation responderLocation, long timestamp) {
+        mStatus = status;
+        mMac = null;
+        mPeerHandle = peerHandle;
+        mDistanceMm = distanceMm;
+        mDistanceStdDevMm = distanceStdDevMm;
+        mRssi = rssi;
+        mNumAttemptedMeasurements = numAttemptedMeasurements;
+        mNumSuccessfulMeasurements = numSuccessfulMeasurements;
+        mLci = lci == null ? EMPTY_BYTE_ARRAY : lci;
+        mLcr = lcr == null ? EMPTY_BYTE_ARRAY : lcr;
+        mResponderLocation = responderLocation;
+        mTimestamp = timestamp;
+    }
+
+    /**
+     * @return The status of ranging measurement: {@link #STATUS_SUCCESS} in case of success, and
+     * {@link #STATUS_FAIL} in case of failure.
+     */
+    @RangeResultStatus
+    public int getStatus() {
+        return mStatus;
+    }
+
+    /**
+     * @return The MAC address of the device whose range measurement was requested. Will correspond
+     * to the MAC address of the device in the {@link RangingRequest}.
+     * <p>
+     * Will return a {@code null} for results corresponding to requests issued using a {@code
+     * PeerHandle}, i.e. using the {@link RangingRequest.Builder#addWifiAwarePeer(PeerHandle)} API.
+     */
+    @Nullable
+    public MacAddress getMacAddress() {
+        return mMac;
+    }
+
+    /**
+     * @return The PeerHandle of the device whose reange measurement was requested. Will correspond
+     * to the PeerHandle of the devices requested using
+     * {@link RangingRequest.Builder#addWifiAwarePeer(PeerHandle)}.
+     * <p>
+     * Will return a {@code null} for results corresponding to requests issued using a MAC address.
+     */
+    @Nullable public PeerHandle getPeerHandle() {
+        return mPeerHandle;
+    }
+
+    /**
+     * @return The distance (in mm) to the device specified by {@link #getMacAddress()} or
+     * {@link #getPeerHandle()}.
+     * <p>
+     * Note: the measured distance may be negative for very close devices.
+     * <p>
+     * Only valid if {@link #getStatus()} returns {@link #STATUS_SUCCESS}, otherwise will throw an
+     * exception.
+     */
+    public int getDistanceMm() {
+        if (mStatus != STATUS_SUCCESS) {
+            throw new IllegalStateException(
+                    "getDistanceMm(): invoked on an invalid result: getStatus()=" + mStatus);
+        }
+        return mDistanceMm;
+    }
+
+    /**
+     * @return The standard deviation of the measured distance (in mm) to the device specified by
+     * {@link #getMacAddress()} or {@link #getPeerHandle()}. The standard deviation is calculated
+     * over the measurements executed in a single RTT burst. The number of measurements is returned
+     * by {@link #getNumSuccessfulMeasurements()} - 0 successful measurements indicate that the
+     * standard deviation is not valid (a valid standard deviation requires at least 2 data points).
+     * <p>
+     * Only valid if {@link #getStatus()} returns {@link #STATUS_SUCCESS}, otherwise will throw an
+     * exception.
+     */
+    public int getDistanceStdDevMm() {
+        if (mStatus != STATUS_SUCCESS) {
+            throw new IllegalStateException(
+                    "getDistanceStdDevMm(): invoked on an invalid result: getStatus()=" + mStatus);
+        }
+        return mDistanceStdDevMm;
+    }
+
+    /**
+     * @return The average RSSI, in units of dBm, observed during the RTT measurement.
+     * <p>
+     * Only valid if {@link #getStatus()} returns {@link #STATUS_SUCCESS}, otherwise will throw an
+     * exception.
+     */
+    public int getRssi() {
+        if (mStatus != STATUS_SUCCESS) {
+            throw new IllegalStateException(
+                    "getRssi(): invoked on an invalid result: getStatus()=" + mStatus);
+        }
+        return mRssi;
+    }
+
+    /**
+     * @return The number of attempted measurements used in the RTT exchange resulting in this set
+     * of results. The number of successful measurements is returned by
+     * {@link #getNumSuccessfulMeasurements()} which at most, if there are no errors, will be 1 less
+     * that the number of attempted measurements.
+     * <p>
+     * Only valid if {@link #getStatus()} returns {@link #STATUS_SUCCESS}, otherwise will throw an
+     * exception.
+     */
+    public int getNumAttemptedMeasurements() {
+        if (mStatus != STATUS_SUCCESS) {
+            throw new IllegalStateException(
+                    "getNumAttemptedMeasurements(): invoked on an invalid result: getStatus()="
+                            + mStatus);
+        }
+        return mNumAttemptedMeasurements;
+    }
+
+    /**
+     * @return The number of successful measurements used to calculate the distance and standard
+     * deviation. If the number of successful measurements if 1 then then standard deviation,
+     * returned by {@link #getDistanceStdDevMm()}, is not valid (a 0 is returned for the standard
+     * deviation).
+     * <p>
+     * The total number of measurement attempts is returned by
+     * {@link #getNumAttemptedMeasurements()}. The number of successful measurements will be at
+     * most 1 less then the number of attempted measurements.
+     * <p>
+     * Only valid if {@link #getStatus()} returns {@link #STATUS_SUCCESS}, otherwise will throw an
+     * exception.
+     */
+    public int getNumSuccessfulMeasurements() {
+        if (mStatus != STATUS_SUCCESS) {
+            throw new IllegalStateException(
+                    "getNumSuccessfulMeasurements(): invoked on an invalid result: getStatus()="
+                            + mStatus);
+        }
+        return mNumSuccessfulMeasurements;
+    }
+
+    /**
+     * @return The unverified responder location represented as {@link ResponderLocation} which
+     * captures location information the responder is programmed to broadcast. The responder
+     * location is referred to as unverified, because we are relying on the device/site
+     * administrator to correctly configure its location data.
+     * <p>
+     * Will return a {@code null} when the location information cannot be parsed.
+     * <p>
+     * Only valid if {@link #getStatus()} returns {@link #STATUS_SUCCESS}, otherwise will throw an
+     * exception.
+     */
+    @Nullable
+    public ResponderLocation getUnverifiedResponderLocation() {
+        if (mStatus != STATUS_SUCCESS) {
+            throw new IllegalStateException(
+                    "getUnverifiedResponderLocation(): invoked on an invalid result: getStatus()="
+                            + mStatus);
+        }
+        return mResponderLocation;
+    }
+
+    /**
+     * @return The Location Configuration Information (LCI) as self-reported by the peer. The format
+     * is specified in the IEEE 802.11-2016 specifications, section 9.4.2.22.10.
+     * <p>
+     * Note: the information is NOT validated - use with caution. Consider validating it with
+     * other sources of information before using it.
+     *
+     * @hide
+     */
+    @SystemApi
+    @NonNull
+    public byte[] getLci() {
+        if (mStatus != STATUS_SUCCESS) {
+            throw new IllegalStateException(
+                    "getLci(): invoked on an invalid result: getStatus()=" + mStatus);
+        }
+        return mLci;
+    }
+
+    /**
+     * @return The Location Civic report (LCR) as self-reported by the peer. The format
+     * is specified in the IEEE 802.11-2016 specifications, section 9.4.2.22.13.
+     * <p>
+     * Note: the information is NOT validated - use with caution. Consider validating it with
+     * other sources of information before using it.
+     *
+     * @hide
+     */
+    @SystemApi
+    @NonNull
+    public byte[] getLcr() {
+        if (mStatus != STATUS_SUCCESS) {
+            throw new IllegalStateException(
+                    "getReportedLocationCivic(): invoked on an invalid result: getStatus()="
+                            + mStatus);
+        }
+        return mLcr;
+    }
+
+    /**
+     * @return The timestamp at which the ranging operation was performed. The timestamp is in
+     * milliseconds since boot, including time spent in sleep, corresponding to values provided by
+     * {@link android.os.SystemClock#elapsedRealtime()}.
+     * <p>
+     * Only valid if {@link #getStatus()} returns {@link #STATUS_SUCCESS}, otherwise will throw an
+     * exception.
+     */
+    public long getRangingTimestampMillis() {
+        if (mStatus != STATUS_SUCCESS) {
+            throw new IllegalStateException(
+                    "getRangingTimestampMillis(): invoked on an invalid result: getStatus()="
+                            + mStatus);
+        }
+        return mTimestamp;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        dest.writeInt(mStatus);
+        if (mMac == null) {
+            dest.writeBoolean(false);
+        } else {
+            dest.writeBoolean(true);
+            mMac.writeToParcel(dest, flags);
+        }
+        if (mPeerHandle == null) {
+            dest.writeBoolean(false);
+        } else {
+            dest.writeBoolean(true);
+            dest.writeInt(mPeerHandle.peerId);
+        }
+        dest.writeInt(mDistanceMm);
+        dest.writeInt(mDistanceStdDevMm);
+        dest.writeInt(mRssi);
+        dest.writeInt(mNumAttemptedMeasurements);
+        dest.writeInt(mNumSuccessfulMeasurements);
+        dest.writeByteArray(mLci);
+        dest.writeByteArray(mLcr);
+        dest.writeParcelable(mResponderLocation, flags);
+        dest.writeLong(mTimestamp);
+    }
+
+    public static final @android.annotation.NonNull Creator<RangingResult> CREATOR = new Creator<RangingResult>() {
+        @Override
+        public RangingResult[] newArray(int size) {
+            return new RangingResult[size];
+        }
+
+        @Override
+        public RangingResult createFromParcel(Parcel in) {
+            int status = in.readInt();
+            boolean macAddressPresent = in.readBoolean();
+            MacAddress mac = null;
+            if (macAddressPresent) {
+                mac = MacAddress.CREATOR.createFromParcel(in);
+            }
+            boolean peerHandlePresent = in.readBoolean();
+            PeerHandle peerHandle = null;
+            if (peerHandlePresent) {
+                peerHandle = new PeerHandle(in.readInt());
+            }
+            int distanceMm = in.readInt();
+            int distanceStdDevMm = in.readInt();
+            int rssi = in.readInt();
+            int numAttemptedMeasurements = in.readInt();
+            int numSuccessfulMeasurements = in.readInt();
+            byte[] lci = in.createByteArray();
+            byte[] lcr = in.createByteArray();
+            ResponderLocation responderLocation =
+                    in.readParcelable(this.getClass().getClassLoader());
+            long timestamp = in.readLong();
+            if (peerHandlePresent) {
+                return new RangingResult(status, peerHandle, distanceMm, distanceStdDevMm, rssi,
+                        numAttemptedMeasurements, numSuccessfulMeasurements, lci, lcr,
+                        responderLocation, timestamp);
+            } else {
+                return new RangingResult(status, mac, distanceMm, distanceStdDevMm, rssi,
+                        numAttemptedMeasurements, numSuccessfulMeasurements, lci, lcr,
+                        responderLocation, timestamp);
+            }
+        }
+    };
+
+    /** @hide */
+    @Override
+    public String toString() {
+        return new StringBuilder("RangingResult: [status=").append(mStatus).append(", mac=").append(
+                mMac).append(", peerHandle=").append(
+                mPeerHandle == null ? "<null>" : mPeerHandle.peerId).append(", distanceMm=").append(
+                mDistanceMm).append(", distanceStdDevMm=").append(mDistanceStdDevMm).append(
+                ", rssi=").append(mRssi).append(", numAttemptedMeasurements=").append(
+                mNumAttemptedMeasurements).append(", numSuccessfulMeasurements=").append(
+                mNumSuccessfulMeasurements).append(", lci=").append(mLci).append(", lcr=").append(
+                mLcr).append(", responderLocation=").append(mResponderLocation)
+                .append(", timestamp=").append(mTimestamp).append("]").toString();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+
+        if (!(o instanceof RangingResult)) {
+            return false;
+        }
+
+        RangingResult lhs = (RangingResult) o;
+
+        return mStatus == lhs.mStatus && Objects.equals(mMac, lhs.mMac) && Objects.equals(
+                mPeerHandle, lhs.mPeerHandle) && mDistanceMm == lhs.mDistanceMm
+                && mDistanceStdDevMm == lhs.mDistanceStdDevMm && mRssi == lhs.mRssi
+                && mNumAttemptedMeasurements == lhs.mNumAttemptedMeasurements
+                && mNumSuccessfulMeasurements == lhs.mNumSuccessfulMeasurements
+                && Arrays.equals(mLci, lhs.mLci) && Arrays.equals(mLcr, lhs.mLcr)
+                && mTimestamp == lhs.mTimestamp
+                && Objects.equals(mResponderLocation, lhs.mResponderLocation);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mStatus, mMac, mPeerHandle, mDistanceMm, mDistanceStdDevMm, mRssi,
+                mNumAttemptedMeasurements, mNumSuccessfulMeasurements, mLci, mLcr,
+                mResponderLocation, mTimestamp);
+    }
+}
diff --git a/android/net/wifi/rtt/RangingResultCallback.java b/android/net/wifi/rtt/RangingResultCallback.java
new file mode 100644
index 0000000..fa7d79e
--- /dev/null
+++ b/android/net/wifi/rtt/RangingResultCallback.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.rtt;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.List;
+
+/**
+ * Base class for ranging result callbacks. Should be extended by applications and set when calling
+ * {@link WifiRttManager#startRanging(RangingRequest, java.util.concurrent.Executor, RangingResultCallback)}.
+ * If the ranging operation fails in whole (not attempted) then {@link #onRangingFailure(int)}
+ * will be called with a failure code. If the ranging operation is performed for each of the
+ * requested peers then the {@link #onRangingResults(List)} will be called with the set of
+ * results (@link {@link RangingResult}, each of which has its own success/failure code
+ * {@link RangingResult#getStatus()}.
+ */
+public abstract class RangingResultCallback {
+    /** @hide */
+    @IntDef({STATUS_CODE_FAIL, STATUS_CODE_FAIL_RTT_NOT_AVAILABLE})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface RangingOperationStatus {
+    }
+
+    /**
+     * A failure code for the whole ranging request operation. Indicates a failure.
+     */
+    public static final int STATUS_CODE_FAIL = 1;
+
+    /**
+     * A failure code for the whole ranging request operation. Indicates that the request failed due
+     * to RTT not being available - e.g. Wi-Fi was disabled. Use the
+     * {@link WifiRttManager#isAvailable()} and {@link WifiRttManager#ACTION_WIFI_RTT_STATE_CHANGED}
+     * to track RTT availability.
+     */
+    public static final int STATUS_CODE_FAIL_RTT_NOT_AVAILABLE = 2;
+
+    /**
+     * Called when a ranging operation failed in whole - i.e. no ranging operation to any of the
+     * devices specified in the request was attempted.
+     *
+     * @param code A status code indicating the type of failure.
+     */
+    public abstract void onRangingFailure(@RangingOperationStatus int code);
+
+    /**
+     * Called when a ranging operation was executed. The list of results corresponds to devices
+     * specified in the ranging request.
+     *
+     * @param results List of range measurements, one per requested device.
+     */
+    public abstract void onRangingResults(@NonNull List<RangingResult> results);
+}
diff --git a/android/net/wifi/rtt/ResponderConfig.java b/android/net/wifi/rtt/ResponderConfig.java
new file mode 100644
index 0000000..e6ae483
--- /dev/null
+++ b/android/net/wifi/rtt/ResponderConfig.java
@@ -0,0 +1,500 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.net.wifi.rtt;
+
+import static android.net.wifi.ScanResult.InformationElement.EID_HT_CAPABILITIES;
+import static android.net.wifi.ScanResult.InformationElement.EID_VHT_CAPABILITIES;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.SystemApi;
+import android.net.MacAddress;
+import android.net.wifi.ScanResult;
+import android.net.wifi.aware.PeerHandle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.Log;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Objects;
+
+/**
+ * Defines the configuration of an IEEE 802.11mc Responder. The Responder may be an Access Point
+ * (AP), a Wi-Fi Aware device, or a manually configured Responder.
+ * <p>
+ * A Responder configuration may be constructed from a {@link ScanResult} or manually (with the
+ * data obtained out-of-band from a peer).
+ *
+ * @hide
+ */
+@SystemApi
+public final class ResponderConfig implements Parcelable {
+    private static final String TAG = "ResponderConfig";
+    private static final int AWARE_BAND_2_DISCOVERY_CHANNEL = 2437;
+
+    /** @hide */
+    @IntDef({RESPONDER_AP, RESPONDER_STA, RESPONDER_P2P_GO, RESPONDER_P2P_CLIENT, RESPONDER_AWARE})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ResponderType {
+    }
+
+    /**
+     * Responder is an AP.
+     */
+    public static final int RESPONDER_AP = 0;
+    /**
+     * Responder is a STA.
+     */
+    public static final int RESPONDER_STA = 1;
+    /**
+     * Responder is a Wi-Fi Direct Group Owner (GO).
+     */
+    public static final int RESPONDER_P2P_GO = 2;
+    /**
+     * Responder is a Wi-Fi Direct Group Client.
+     */
+    public static final int RESPONDER_P2P_CLIENT = 3;
+    /**
+     * Responder is a Wi-Fi Aware device.
+     */
+    public static final int RESPONDER_AWARE = 4;
+
+
+    /** @hide */
+    @IntDef({
+            CHANNEL_WIDTH_20MHZ, CHANNEL_WIDTH_40MHZ, CHANNEL_WIDTH_80MHZ, CHANNEL_WIDTH_160MHZ,
+            CHANNEL_WIDTH_80MHZ_PLUS_MHZ})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface ChannelWidth {
+    }
+
+    /**
+     * Channel bandwidth is 20 MHZ
+     */
+    public static final int CHANNEL_WIDTH_20MHZ = 0;
+    /**
+     * Channel bandwidth is 40 MHZ
+     */
+    public static final int CHANNEL_WIDTH_40MHZ = 1;
+    /**
+     * Channel bandwidth is 80 MHZ
+     */
+    public static final int CHANNEL_WIDTH_80MHZ = 2;
+    /**
+     * Channel bandwidth is 160 MHZ
+     */
+    public static final int CHANNEL_WIDTH_160MHZ = 3;
+    /**
+     * Channel bandwidth is 160 MHZ, but 80MHZ + 80MHZ
+     */
+    public static final int CHANNEL_WIDTH_80MHZ_PLUS_MHZ = 4;
+
+    /** @hide */
+    @IntDef({PREAMBLE_LEGACY, PREAMBLE_HT, PREAMBLE_VHT})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface PreambleType {
+    }
+
+    /**
+     * Preamble type: Legacy.
+     */
+    public static final int PREAMBLE_LEGACY = 0;
+
+    /**
+     * Preamble type: HT.
+     */
+    public static final int PREAMBLE_HT = 1;
+
+    /**
+     * Preamble type: VHT.
+     */
+    public static final int PREAMBLE_VHT = 2;
+
+
+    /**
+     * The MAC address of the Responder. Will be null if a Wi-Fi Aware peer identifier (the
+     * peerHandle field) ise used to identify the Responder.
+     */
+    public final MacAddress macAddress;
+
+    /**
+     * The peer identifier of a Wi-Fi Aware Responder. Will be null if a MAC Address (the macAddress
+     * field) is used to identify the Responder.
+     */
+    public final PeerHandle peerHandle;
+
+    /**
+     * The device type of the Responder.
+     */
+    public final int responderType;
+
+    /**
+     * Indicates whether the Responder device supports IEEE 802.11mc.
+     */
+    public final boolean supports80211mc;
+
+    /**
+     * Responder channel bandwidth, specified using {@link ChannelWidth}.
+     */
+    public final int channelWidth;
+
+    /**
+     * The primary 20 MHz frequency (in MHz) of the channel of the Responder.
+     */
+    public final int frequency;
+
+    /**
+     * Not used if the {@link #channelWidth} is 20 MHz. If the Responder uses 40, 80 or 160 MHz,
+     * this is the center frequency (in MHz), if the Responder uses 80 + 80 MHz, this is the
+     * center frequency of the first segment (in MHz).
+     */
+    public final int centerFreq0;
+
+    /**
+     * Only used if the {@link #channelWidth} is 80 + 80 MHz. If the Responder uses 80 + 80 MHz,
+     * this is the center frequency of the second segment (in MHz).
+     */
+    public final int centerFreq1;
+
+    /**
+     * The preamble used by the Responder, specified using {@link PreambleType}.
+     */
+    public final int preamble;
+
+    /**
+     * Constructs Responder configuration, using a MAC address to identify the Responder.
+     *
+     * @param macAddress      The MAC address of the Responder.
+     * @param responderType   The type of the responder device, specified using
+     *                        {@link ResponderType}.
+     * @param supports80211mc Indicates whether the responder supports IEEE 802.11mc.
+     * @param channelWidth    Responder channel bandwidth, specified using {@link ChannelWidth}.
+     * @param frequency       The primary 20 MHz frequency (in MHz) of the channel of the Responder.
+     * @param centerFreq0     Not used if the {@code channelWidth} is 20 MHz. If the Responder uses
+     *                        40, 80 or 160 MHz, this is the center frequency (in MHz), if the
+     *                        Responder uses 80 + 80 MHz, this is the center frequency of the first
+     *                        segment (in MHz).
+     * @param centerFreq1     Only used if the {@code channelWidth} is 80 + 80 MHz. If the
+     *                        Responder
+     *                        uses 80 + 80 MHz, this is the center frequency of the second segment
+     *                        (in
+     *                        MHz).
+     * @param preamble        The preamble used by the Responder, specified using
+     *                        {@link PreambleType}.
+     */
+    public ResponderConfig(@NonNull MacAddress macAddress, @ResponderType int responderType,
+            boolean supports80211mc, @ChannelWidth int channelWidth, int frequency, int centerFreq0,
+            int centerFreq1, @PreambleType int preamble) {
+        if (macAddress == null) {
+            throw new IllegalArgumentException(
+                    "Invalid ResponderConfig - must specify a MAC address");
+        }
+        this.macAddress = macAddress;
+        this.peerHandle = null;
+        this.responderType = responderType;
+        this.supports80211mc = supports80211mc;
+        this.channelWidth = channelWidth;
+        this.frequency = frequency;
+        this.centerFreq0 = centerFreq0;
+        this.centerFreq1 = centerFreq1;
+        this.preamble = preamble;
+    }
+
+    /**
+     * Constructs Responder configuration, using a Wi-Fi Aware PeerHandle to identify the Responder.
+     *
+     * @param peerHandle      The Wi-Fi Aware peer identifier of the Responder.
+     * @param responderType   The type of the responder device, specified using
+     *                        {@link ResponderType}.
+     * @param supports80211mc Indicates whether the responder supports IEEE 802.11mc.
+     * @param channelWidth    Responder channel bandwidth, specified using {@link ChannelWidth}.
+     * @param frequency       The primary 20 MHz frequency (in MHz) of the channel of the Responder.
+     * @param centerFreq0     Not used if the {@code channelWidth} is 20 MHz. If the Responder uses
+     *                        40, 80 or 160 MHz, this is the center frequency (in MHz), if the
+     *                        Responder uses 80 + 80 MHz, this is the center frequency of the first
+     *                        segment (in MHz).
+     * @param centerFreq1     Only used if the {@code channelWidth} is 80 + 80 MHz. If the
+     *                        Responder
+     *                        uses 80 + 80 MHz, this is the center frequency of the second segment
+     *                        (in
+     *                        MHz).
+     * @param preamble        The preamble used by the Responder, specified using
+     *                        {@link PreambleType}.
+     */
+    public ResponderConfig(@NonNull PeerHandle peerHandle, @ResponderType int responderType,
+            boolean supports80211mc, @ChannelWidth int channelWidth, int frequency, int centerFreq0,
+            int centerFreq1, @PreambleType int preamble) {
+        this.macAddress = null;
+        this.peerHandle = peerHandle;
+        this.responderType = responderType;
+        this.supports80211mc = supports80211mc;
+        this.channelWidth = channelWidth;
+        this.frequency = frequency;
+        this.centerFreq0 = centerFreq0;
+        this.centerFreq1 = centerFreq1;
+        this.preamble = preamble;
+    }
+
+    /**
+     * Constructs Responder configuration. This is an internal-only constructor which specifies both
+     * a MAC address and a Wi-Fi PeerHandle to identify the Responder.
+     *
+     * @param macAddress      The MAC address of the Responder.
+     * @param peerHandle      The Wi-Fi Aware peer identifier of the Responder.
+     * @param responderType   The type of the responder device, specified using
+     *                        {@link ResponderType}.
+     * @param supports80211mc Indicates whether the responder supports IEEE 802.11mc.
+     * @param channelWidth    Responder channel bandwidth, specified using {@link ChannelWidth}.
+     * @param frequency       The primary 20 MHz frequency (in MHz) of the channel of the Responder.
+     * @param centerFreq0     Not used if the {@code channelWidth} is 20 MHz. If the Responder uses
+     *                        40, 80 or 160 MHz, this is the center frequency (in MHz), if the
+     *                        Responder uses 80 + 80 MHz, this is the center frequency of the first
+     *                        segment (in MHz).
+     * @param centerFreq1     Only used if the {@code channelWidth} is 80 + 80 MHz. If the
+     *                        Responder
+     *                        uses 80 + 80 MHz, this is the center frequency of the second segment
+     *                        (in
+     *                        MHz).
+     * @param preamble        The preamble used by the Responder, specified using
+     *                        {@link PreambleType}.
+     * @hide
+     */
+    public ResponderConfig(@NonNull MacAddress macAddress, @NonNull PeerHandle peerHandle,
+            @ResponderType int responderType, boolean supports80211mc,
+            @ChannelWidth int channelWidth, int frequency, int centerFreq0, int centerFreq1,
+            @PreambleType int preamble) {
+        this.macAddress = macAddress;
+        this.peerHandle = peerHandle;
+        this.responderType = responderType;
+        this.supports80211mc = supports80211mc;
+        this.channelWidth = channelWidth;
+        this.frequency = frequency;
+        this.centerFreq0 = centerFreq0;
+        this.centerFreq1 = centerFreq1;
+        this.preamble = preamble;
+    }
+
+    /**
+     * Creates a Responder configuration from a {@link ScanResult} corresponding to an Access
+     * Point (AP), which can be obtained from {@link android.net.wifi.WifiManager#getScanResults()}.
+     */
+    public static ResponderConfig fromScanResult(ScanResult scanResult) {
+        MacAddress macAddress = MacAddress.fromString(scanResult.BSSID);
+        int responderType = RESPONDER_AP;
+        boolean supports80211mc = scanResult.is80211mcResponder();
+        int channelWidth = translateScanResultChannelWidth(scanResult.channelWidth);
+        int frequency = scanResult.frequency;
+        int centerFreq0 = scanResult.centerFreq0;
+        int centerFreq1 = scanResult.centerFreq1;
+
+        int preamble;
+        if (scanResult.informationElements != null && scanResult.informationElements.length != 0) {
+            boolean htCapabilitiesPresent = false;
+            boolean vhtCapabilitiesPresent = false;
+            for (ScanResult.InformationElement ie : scanResult.informationElements) {
+                if (ie.id == EID_HT_CAPABILITIES) {
+                    htCapabilitiesPresent = true;
+                } else if (ie.id == EID_VHT_CAPABILITIES) {
+                    vhtCapabilitiesPresent = true;
+                }
+            }
+            if (vhtCapabilitiesPresent) {
+                preamble = PREAMBLE_VHT;
+            } else if (htCapabilitiesPresent) {
+                preamble = PREAMBLE_HT;
+            } else {
+                preamble = PREAMBLE_LEGACY;
+            }
+        } else {
+            Log.e(TAG, "Scan Results do not contain IEs - using backup method to select preamble");
+            if (channelWidth == CHANNEL_WIDTH_80MHZ || channelWidth == CHANNEL_WIDTH_160MHZ) {
+                preamble = PREAMBLE_VHT;
+            } else {
+                preamble = PREAMBLE_HT;
+            }
+        }
+
+        return new ResponderConfig(macAddress, responderType, supports80211mc, channelWidth,
+                frequency, centerFreq0, centerFreq1, preamble);
+    }
+
+    /**
+     * Creates a Responder configuration from a MAC address corresponding to a Wi-Fi Aware
+     * Responder. The Responder parameters are set to defaults.
+     */
+    public static ResponderConfig fromWifiAwarePeerMacAddressWithDefaults(MacAddress macAddress) {
+        /* Note: the parameters are those of the Aware discovery channel (channel 6). A Responder
+         * is expected to be brought up and available to negotiate a maximum accuracy channel
+         * (i.e. Band 5 @ 80MHz). A Responder is brought up on the peer by starting an Aware
+         * Unsolicited Publisher with Ranging enabled.
+         */
+        return new ResponderConfig(macAddress, RESPONDER_AWARE, true, CHANNEL_WIDTH_20MHZ,
+                AWARE_BAND_2_DISCOVERY_CHANNEL, 0, 0, PREAMBLE_HT);
+    }
+
+    /**
+     * Creates a Responder configuration from a {@link PeerHandle} corresponding to a Wi-Fi Aware
+     * Responder. The Responder parameters are set to defaults.
+     */
+    public static ResponderConfig fromWifiAwarePeerHandleWithDefaults(PeerHandle peerHandle) {
+        /* Note: the parameters are those of the Aware discovery channel (channel 6). A Responder
+         * is expected to be brought up and available to negotiate a maximum accuracy channel
+         * (i.e. Band 5 @ 80MHz). A Responder is brought up on the peer by starting an Aware
+         * Unsolicited Publisher with Ranging enabled.
+         */
+        return new ResponderConfig(peerHandle, RESPONDER_AWARE, true, CHANNEL_WIDTH_20MHZ,
+                AWARE_BAND_2_DISCOVERY_CHANNEL, 0, 0, PREAMBLE_HT);
+    }
+
+    /**
+     * Check whether the Responder configuration is valid.
+     *
+     * @return true if valid, false otherwise.
+     * @hide
+     */
+    public boolean isValid(boolean awareSupported) {
+        if (macAddress == null && peerHandle == null || macAddress != null && peerHandle != null) {
+            return false;
+        }
+        if (!awareSupported && responderType == RESPONDER_AWARE) {
+            return false;
+        }
+
+        return true;
+    }
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel dest, int flags) {
+        if (macAddress == null) {
+            dest.writeBoolean(false);
+        } else {
+            dest.writeBoolean(true);
+            macAddress.writeToParcel(dest, flags);
+        }
+        if (peerHandle == null) {
+            dest.writeBoolean(false);
+        } else {
+            dest.writeBoolean(true);
+            dest.writeInt(peerHandle.peerId);
+        }
+        dest.writeInt(responderType);
+        dest.writeInt(supports80211mc ? 1 : 0);
+        dest.writeInt(channelWidth);
+        dest.writeInt(frequency);
+        dest.writeInt(centerFreq0);
+        dest.writeInt(centerFreq1);
+        dest.writeInt(preamble);
+    }
+
+    public static final @android.annotation.NonNull Creator<ResponderConfig> CREATOR = new Creator<ResponderConfig>() {
+        @Override
+        public ResponderConfig[] newArray(int size) {
+            return new ResponderConfig[size];
+        }
+
+        @Override
+        public ResponderConfig createFromParcel(Parcel in) {
+            boolean macAddressPresent = in.readBoolean();
+            MacAddress macAddress = null;
+            if (macAddressPresent) {
+                macAddress = MacAddress.CREATOR.createFromParcel(in);
+            }
+            boolean peerHandlePresent = in.readBoolean();
+            PeerHandle peerHandle = null;
+            if (peerHandlePresent) {
+                peerHandle = new PeerHandle(in.readInt());
+            }
+            int responderType = in.readInt();
+            boolean supports80211mc = in.readInt() == 1;
+            int channelWidth = in.readInt();
+            int frequency = in.readInt();
+            int centerFreq0 = in.readInt();
+            int centerFreq1 = in.readInt();
+            int preamble = in.readInt();
+
+            if (peerHandle == null) {
+                return new ResponderConfig(macAddress, responderType, supports80211mc, channelWidth,
+                        frequency, centerFreq0, centerFreq1, preamble);
+            } else {
+                return new ResponderConfig(peerHandle, responderType, supports80211mc, channelWidth,
+                        frequency, centerFreq0, centerFreq1, preamble);
+            }
+        }
+    };
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+
+        if (!(o instanceof ResponderConfig)) {
+            return false;
+        }
+
+        ResponderConfig lhs = (ResponderConfig) o;
+
+        return Objects.equals(macAddress, lhs.macAddress) && Objects.equals(peerHandle,
+                lhs.peerHandle) && responderType == lhs.responderType
+                && supports80211mc == lhs.supports80211mc && channelWidth == lhs.channelWidth
+                && frequency == lhs.frequency && centerFreq0 == lhs.centerFreq0
+                && centerFreq1 == lhs.centerFreq1 && preamble == lhs.preamble;
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(macAddress, peerHandle, responderType, supports80211mc, channelWidth,
+                frequency, centerFreq0, centerFreq1, preamble);
+    }
+
+    /** @hide */
+    @Override
+    public String toString() {
+        return new StringBuffer("ResponderConfig: macAddress=").append(macAddress).append(
+                ", peerHandle=").append(peerHandle == null ? "<null>" : peerHandle.peerId).append(
+                ", responderType=").append(responderType).append(", supports80211mc=").append(
+                supports80211mc).append(", channelWidth=").append(channelWidth).append(
+                ", frequency=").append(frequency).append(", centerFreq0=").append(
+                centerFreq0).append(", centerFreq1=").append(centerFreq1).append(
+                ", preamble=").append(preamble).toString();
+    }
+
+    /** @hide */
+    static int translateScanResultChannelWidth(int scanResultChannelWidth) {
+        switch (scanResultChannelWidth) {
+            case ScanResult.CHANNEL_WIDTH_20MHZ:
+                return CHANNEL_WIDTH_20MHZ;
+            case ScanResult.CHANNEL_WIDTH_40MHZ:
+                return CHANNEL_WIDTH_40MHZ;
+            case ScanResult.CHANNEL_WIDTH_80MHZ:
+                return CHANNEL_WIDTH_80MHZ;
+            case ScanResult.CHANNEL_WIDTH_160MHZ:
+                return CHANNEL_WIDTH_160MHZ;
+            case ScanResult.CHANNEL_WIDTH_80MHZ_PLUS_MHZ:
+                return CHANNEL_WIDTH_80MHZ_PLUS_MHZ;
+            default:
+                throw new IllegalArgumentException(
+                        "translateScanResultChannelWidth: bad " + scanResultChannelWidth);
+        }
+    }
+}
diff --git a/android/net/wifi/rtt/ResponderLocation.java b/android/net/wifi/rtt/ResponderLocation.java
new file mode 100644
index 0000000..e1d82f8
--- /dev/null
+++ b/android/net/wifi/rtt/ResponderLocation.java
@@ -0,0 +1,1424 @@
+/*
+ * Copyright (C) 2018 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.wifi.rtt;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.location.Address;
+import android.location.Location;
+import android.net.MacAddress;
+import android.net.Uri;
+import android.net.wifi.rtt.CivicLocationKeys.CivicLocationKeysType;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+import android.util.SparseArray;
+import android.webkit.MimeTypeMap;
+
+import java.lang.annotation.Retention;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * ResponderLocation is both a Location Configuration Information (LCI) decoder and a Location Civic
+ * Report (LCR) decoder for information received from a Wi-Fi Access Point (AP) during Wi-Fi RTT
+ * ranging process.
+ *
+ * <p>This is based on the IEEE P802.11-REVmc/D8.0 spec section 9.4.2.22, under Measurement Report
+ * Element. Subelement location data-fields parsed from separate input LCI and LCR Information
+ * Elements are unified in this class.</p>
+ *
+ * <p>Note: The information provided by this class is broadcast by a responder (usually an Access
+ * Point), and passed on as-is. There is no guarantee this information is accurate or correct, and
+ * as a result developers should carefully consider how this information should be used and provide
+ * corresponding advice to users.</p>
+ */
+public final class ResponderLocation implements Parcelable {
+    private static final int BYTE_MASK = 0xFF;
+    private static final int LSB_IN_BYTE = 0x01;
+    private static final int MSB_IN_BYTE = 0x80;
+    private static final int MIN_BUFFER_SIZE = 3; // length of LEAD_LCI_ELEMENT_BYTES
+    private static final int MAX_BUFFER_SIZE = 256;
+
+    // Information Element (IE) fields
+    private static final byte MEASUREMENT_TOKEN_AUTONOMOUS = 0x01;
+    private static final byte MEASUREMENT_REPORT_MODE = 0x00;
+    private static final byte MEASUREMENT_TYPE_LCI = 0x08;
+    private static final byte MEASUREMENT_TYPE_LCR = 0x0b;
+
+    // LCI Subelement IDs
+    private static final byte SUBELEMENT_LCI = 0x00;
+    private static final byte SUBELEMENT_Z = 0x04;
+    private static final byte SUBELEMENT_USAGE = 0x06;
+    private static final byte SUBELEMENT_BSSID_LIST = 0x07;
+
+    // LCI Subelement Lengths
+    private static final int SUBELEMENT_LCI_LENGTH = 16;
+    private static final int SUBELEMENT_Z_LENGTH = 6;
+    private static final int SUBELEMENT_USAGE_LENGTH1 = 1;
+    private static final int SUBELEMENT_USAGE_LENGTH3 = 3;
+    private static final int SUBELEMENT_BSSID_LIST_MIN_BUFFER_LENGTH = 1;
+
+    private static final byte[] LEAD_LCI_ELEMENT_BYTES = {
+            MEASUREMENT_TOKEN_AUTONOMOUS, MEASUREMENT_REPORT_MODE, MEASUREMENT_TYPE_LCI
+    };
+
+    // Subelement LCI constants
+
+    /* The LCI subelement bit-field lengths are described in Figure 9-214 of the REVmc spec and
+    represented here as a an array of integers */
+    private static final int[] SUBELEMENT_LCI_BIT_FIELD_LENGTHS = {
+            6, 34, 6, 34, 4, 6, 30, 3, 1, 1, 1, 2
+    };
+    private static final int LATLNG_FRACTION_BITS = 25;
+    private static final int LATLNG_UNCERTAINTY_BASE = 8;
+    private static final int ALTITUDE_FRACTION_BITS = 8;
+    private static final int ALTITUDE_UNCERTAINTY_BASE = 21;
+    private static final double LAT_ABS_LIMIT = 90.0;
+    private static final double LNG_ABS_LIMIT = 180.0;
+    private static final int UNCERTAINTY_UNDEFINED = 0;
+
+    // Subelement LCI fields indices
+    private static final int SUBELEMENT_LCI_LAT_UNCERTAINTY_INDEX = 0;
+    private static final int SUBELEMENT_LCI_LAT_INDEX = 1;
+    private static final int SUBELEMENT_LCI_LNG_UNCERTAINTY_INDEX = 2;
+    private static final int SUBELEMENT_LCI_LNG_INDEX = 3;
+    private static final int SUBELEMENT_LCI_ALT_TYPE_INDEX = 4;
+    private static final int SUBELEMENT_LCI_ALT_UNCERTAINTY_INDEX = 5;
+    private static final int SUBELEMENT_LCI_ALT_INDEX = 6;
+    private static final int SUBELEMENT_LCI_DATUM_INDEX = 7;
+    private static final int SUBELEMENT_LCI_REGLOC_AGREEMENT_INDEX = 8;
+    private static final int SUBELEMENT_LCI_REGLOC_DSE_INDEX = 9;
+    private static final int SUBELEMENT_LCI_DEPENDENT_STA_INDEX = 10;
+    private static final int SUBELEMENT_LCI_VERSION_INDEX = 11;
+
+    /**
+     * The Altitude value is interpreted based on the Altitude Type, and the selected mDatum.
+     *
+     * @hide
+     */
+    @Retention(SOURCE)
+    @IntDef({ALTITUDE_UNDEFINED, ALTITUDE_METERS, ALTITUDE_FLOORS})
+    public @interface AltitudeType {
+    }
+
+    /**
+     * Altitude is not defined for the Responder.
+     * The altitude and altitude uncertainty should not be used: see section 2.4 of IETF RFC 6225.
+     */
+    public static final int ALTITUDE_UNDEFINED = 0;
+    /** Responder Altitude is measured in meters.  */
+    public static final int ALTITUDE_METERS = 1;
+    /** Responder Altitude is measured in floors. */
+    public static final int ALTITUDE_FLOORS = 2;
+
+    /**
+     * The Datum value determines how coordinates are organized in relation to the real world.
+     *
+     * @hide
+     */
+    @Retention(SOURCE)
+    @IntDef({DATUM_UNDEFINED, DATUM_WGS84, DATUM_NAD83_NAV88, DATUM_NAD83_MLLW})
+    public @interface DatumType {
+    }
+
+    /** Datum is not defined. */
+    public static final int DATUM_UNDEFINED = 0;
+    /** Datum used is WGS84. */
+    public static final int DATUM_WGS84 = 1;
+    /** Datum used is NAD83 NAV88. */
+    public static final int DATUM_NAD83_NAV88 = 2;
+    /** Datum used is NAD83 MLLW. */
+    public static final int DATUM_NAD83_MLLW = 3;
+
+
+    /** Version of the LCI protocol is 1.0, the only defined protocol at this time. */
+    public static final int LCI_VERSION_1 = 1;
+
+    /** Provider/Source of the location */
+    private static final String LOCATION_PROVIDER = "WiFi Access Point";
+
+    // LCI Subelement Z constants
+    private static final int[] SUBELEMENT_Z_BIT_FIELD_LENGTHS = {2, 14, 24, 8};
+    private static final int Z_FLOOR_NUMBER_FRACTION_BITS = 4;
+    private static final int Z_FLOOR_HEIGHT_FRACTION_BITS = 12;
+    private static final int Z_MAX_HEIGHT_UNCERTAINTY_FACTOR = 25;
+
+    // LCI Subelement Z fields indices
+    private static final int SUBELEMENT_Z_LAT_EXPECTED_TO_MOVE_INDEX = 0;
+    private static final int SUBELEMENT_Z_FLOOR_NUMBER_INDEX = 1;
+    private static final int SUBELEMENT_Z_HEIGHT_ABOVE_FLOOR_INDEX = 2;
+    private static final int SUBELEMENT_Z_HEIGHT_ABOVE_FLOOR_UNCERTAINTY_INDEX = 3;
+
+    // LCI Subelement Usage Rules constants
+    private static final int SUBELEMENT_USAGE_MASK_RETRANSMIT = 0x01;
+    private static final int SUBELEMENT_USAGE_MASK_RETENTION_EXPIRES = 0x02;
+    private static final int SUBELEMENT_USAGE_MASK_STA_LOCATION_POLICY = 0x04;
+
+    // LCI Subelement Usage Rules field indices
+    private static final int SUBELEMENT_USAGE_PARAMS_INDEX = 0; // 8 bits
+
+    // LCI Subelement BSSID List
+    private static final int SUBELEMENT_BSSID_MAX_INDICATOR_INDEX = 0;
+    private static final int SUBELEMENT_BSSID_LIST_INDEX = 1;
+    private static final int BYTES_IN_A_BSSID = 6;
+
+    /**
+     * The Expected-To-Move value determines how mobile we expect the STA to be.
+     *
+     * @hide
+     */
+    @Retention(SOURCE)
+    @IntDef({LOCATION_FIXED, LOCATION_VARIABLE, LOCATION_MOVEMENT_UNKNOWN, LOCATION_RESERVED})
+    public @interface ExpectedToMoveType {
+    }
+
+    /** Location of responder is fixed (does not move) */
+    public static final int LOCATION_FIXED = 0;
+    /** Location of the responder is variable, and may move */
+    public static final int LOCATION_VARIABLE = 1;
+    /** Location of the responder is not known to be either fixed or variable. */
+    public static final int LOCATION_MOVEMENT_UNKNOWN = 2;
+    /** Location of the responder status is a reserved value */
+    public static final int LOCATION_RESERVED = 3;
+
+    // LCR Subelement IDs
+    private static final byte SUBELEMENT_LOCATION_CIVIC = 0x00;
+    private static final byte SUBELEMENT_MAP_IMAGE = 0x05;
+
+    // LCR Subelement Lengths
+    private static final int SUBELEMENT_LOCATION_CIVIC_MIN_LENGTH = 2;
+    private static final int SUBELEMENT_LOCATION_CIVIC_MAX_LENGTH = 256;
+    private static final int SUBELEMENT_MAP_IMAGE_URL_MAX_LENGTH = 256;
+
+    private static final byte[] LEAD_LCR_ELEMENT_BYTES = {
+            MEASUREMENT_TOKEN_AUTONOMOUS, MEASUREMENT_REPORT_MODE, MEASUREMENT_TYPE_LCR
+    };
+
+    // LCR Location Civic Subelement
+    private static final int CIVIC_COUNTRY_CODE_INDEX = 0;
+    private static final int CIVIC_TLV_LIST_INDEX = 2;
+
+    // LCR Map Image Subelement field indexes.
+    private static final int SUBELEMENT_IMAGE_MAP_TYPE_INDEX = 0;
+    private static final int MAP_TYPE_URL_DEFINED = 0;
+    private static final String[] SUPPORTED_IMAGE_FILE_EXTENSIONS = {
+            "",
+            "png",
+            "gif",
+            "jpg",
+            "svg",
+            "dxf",
+            "dwg",
+            "dwf",
+            "cad",
+            "tif",
+            "gml",
+            "kml",
+            "bmp",
+            "pgm",
+            "ppm",
+            "xbm",
+            "xpm",
+            "ico"
+    };
+
+    // General LCI and LCR state
+    private final boolean mIsValid;
+
+    /*
+      These members are not final because we are not sure if the corresponding subelement will be
+      present until after the parsing process. However, the default value should be set as listed.
+    */
+    private boolean mIsLciValid = false;
+    private boolean mIsZValid = false;
+    private boolean mIsUsageValid = true; // By default this is assumed true
+    private boolean mIsBssidListValid = false;
+    private boolean mIsLocationCivicValid = false;
+    private boolean mIsMapImageValid = false;
+
+    // LCI Subelement LCI state
+    private double mLatitudeUncertainty;
+    private double mLatitude;
+    private double mLongitudeUncertainty;
+    private double mLongitude;
+    private int mAltitudeType;
+    private double mAltitudeUncertainty;
+    private double mAltitude;
+    private int mDatum;
+    private boolean mLciRegisteredLocationAgreement;
+    private boolean mLciRegisteredLocationDse;
+    private boolean mLciDependentStation;
+    private int mLciVersion;
+
+    // LCI Subelement Z state
+    private int mExpectedToMove;
+    private double mFloorNumber;
+    private double mHeightAboveFloorMeters;
+    private double mHeightAboveFloorUncertaintyMeters;
+
+    // LCI Subelement Usage Rights state
+    private boolean mUsageRetransmit;
+    private boolean mUsageRetentionExpires;
+    private boolean mUsageExtraInfoOnAssociation;
+
+    // LCI Subelement BSSID List state
+    private ArrayList<MacAddress> mBssidList;
+
+    // LCR Subelement Location Civic state
+    private String mCivicLocationCountryCode;
+    private String mCivicLocationString;
+    private CivicLocation mCivicLocation;
+
+    // LCR Subelement Map Image state
+    private int mMapImageType;
+    private Uri mMapImageUri;
+
+    /**
+     * Constructor
+     *
+     * @param lciBuffer the bytes received in the LCI Measurement Report Information Element
+     * @param lcrBuffer the bytes received in the LCR Measurement Report Information Element
+     *
+     * @hide
+     */
+    public ResponderLocation(byte[] lciBuffer, byte[] lcrBuffer) {
+        boolean isLciIeValid = false;
+        boolean isLcrIeValid = false;
+        setLciSubelementDefaults();
+        setZaxisSubelementDefaults();
+        setUsageSubelementDefaults();
+        setBssidListSubelementDefaults();
+        setCivicLocationSubelementDefaults();
+        setMapImageSubelementDefaults();
+        if (lciBuffer != null && lciBuffer.length > LEAD_LCI_ELEMENT_BYTES.length) {
+            isLciIeValid = parseInformationElementBuffer(
+                MEASUREMENT_TYPE_LCI, lciBuffer, LEAD_LCI_ELEMENT_BYTES);
+        }
+        if (lcrBuffer != null && lcrBuffer.length > LEAD_LCR_ELEMENT_BYTES.length) {
+            isLcrIeValid = parseInformationElementBuffer(
+                MEASUREMENT_TYPE_LCR, lcrBuffer, LEAD_LCR_ELEMENT_BYTES);
+        }
+
+        boolean isLciValid = isLciIeValid && mIsUsageValid
+                && (mIsLciValid || mIsZValid || mIsBssidListValid);
+
+        boolean isLcrValid = isLcrIeValid && mIsUsageValid
+                && (mIsLocationCivicValid || mIsMapImageValid);
+
+        mIsValid = isLciValid || isLcrValid;
+
+        if (!mIsValid) {
+            setLciSubelementDefaults();
+            setZaxisSubelementDefaults();
+            setCivicLocationSubelementDefaults();
+            setMapImageSubelementDefaults();
+        }
+    }
+
+    private ResponderLocation(Parcel in) {
+        // Object Validation
+        mIsValid = in.readByte() != 0;
+        mIsLciValid = in.readByte() != 0;
+        mIsZValid = in.readByte() != 0;
+        mIsUsageValid = in.readByte() != 0;
+        mIsBssidListValid = in.readByte() != 0;
+        mIsLocationCivicValid = in.readByte() != 0;
+        mIsMapImageValid = in.readByte() != 0;
+
+        // LCI Subelement LCI state
+        mLatitudeUncertainty = in.readDouble();
+        mLatitude = in.readDouble();
+        mLongitudeUncertainty = in.readDouble();
+        mLongitude = in.readDouble();
+        mAltitudeType = in.readInt();
+        mAltitudeUncertainty = in.readDouble();
+        mAltitude = in.readDouble();
+        mDatum = in.readInt();
+        mLciRegisteredLocationAgreement = in.readByte() != 0;
+        mLciRegisteredLocationDse = in.readByte() != 0;
+        mLciDependentStation = in.readByte() != 0;
+        mLciVersion = in.readInt();
+
+        // LCI Subelement Z state
+        mExpectedToMove = in.readInt();
+        mFloorNumber = in.readDouble();
+        mHeightAboveFloorMeters = in.readDouble();
+        mHeightAboveFloorUncertaintyMeters = in.readDouble();
+
+        // LCI Usage Rights
+        mUsageRetransmit = in.readByte() != 0;
+        mUsageRetentionExpires = in.readByte() != 0;
+        mUsageExtraInfoOnAssociation = in.readByte() != 0;
+
+        // LCI Subelement BSSID List
+        mBssidList = in.readArrayList(MacAddress.class.getClassLoader());
+
+        // LCR Subelement Location Civic
+        mCivicLocationCountryCode = in.readString();
+        mCivicLocationString = in.readString();
+        mCivicLocation = in.readParcelable(this.getClass().getClassLoader());
+
+        // LCR Subelement Map Image
+        mMapImageType = in.readInt();
+        String urlString = in.readString();
+        if (TextUtils.isEmpty(urlString)) {
+            mMapImageUri = null;
+        } else {
+            mMapImageUri = Uri.parse(urlString);
+        }
+    }
+
+    public static final @android.annotation.NonNull Creator<ResponderLocation> CREATOR = new Creator<ResponderLocation>() {
+        @Override
+        public ResponderLocation createFromParcel(Parcel in) {
+            return new ResponderLocation(in);
+        }
+
+        @Override
+        public ResponderLocation[] newArray(int size) {
+            return new ResponderLocation[size];
+        }
+    };
+
+    @Override
+    public int describeContents() {
+        return 0;
+    }
+
+    @Override
+    public void writeToParcel(Parcel parcel, int flags) {
+        // Object
+        parcel.writeByte((byte) (mIsValid ? 1 : 0));
+        parcel.writeByte((byte) (mIsLciValid ? 1 : 0));
+        parcel.writeByte((byte) (mIsZValid ? 1 : 0));
+        parcel.writeByte((byte) (mIsUsageValid ? 1 : 0));
+        parcel.writeByte((byte) (mIsBssidListValid ? 1 : 0));
+        parcel.writeByte((byte) (mIsLocationCivicValid ? 1 : 0));
+        parcel.writeByte((byte) (mIsMapImageValid ? 1 : 0));
+
+        // LCI Subelement LCI state
+        parcel.writeDouble(mLatitudeUncertainty);
+        parcel.writeDouble(mLatitude);
+        parcel.writeDouble(mLongitudeUncertainty);
+        parcel.writeDouble(mLongitude);
+        parcel.writeInt(mAltitudeType);
+        parcel.writeDouble(mAltitudeUncertainty);
+        parcel.writeDouble(mAltitude);
+        parcel.writeInt(mDatum);
+        parcel.writeByte((byte) (mLciRegisteredLocationAgreement ? 1 : 0));
+        parcel.writeByte((byte) (mLciRegisteredLocationDse ? 1 : 0));
+        parcel.writeByte((byte) (mLciDependentStation ? 1 : 0));
+        parcel.writeInt(mLciVersion);
+
+        // LCI Subelement Z state
+        parcel.writeInt(mExpectedToMove);
+        parcel.writeDouble(mFloorNumber);
+        parcel.writeDouble(mHeightAboveFloorMeters);
+        parcel.writeDouble(mHeightAboveFloorUncertaintyMeters);
+
+        // LCI Usage Rights
+        parcel.writeByte((byte) (mUsageRetransmit ? 1 : 0));
+        parcel.writeByte((byte) (mUsageRetentionExpires ? 1 : 0));
+        parcel.writeByte((byte) (mUsageExtraInfoOnAssociation ? 1 : 0));
+
+        // LCI Subelement BSSID List
+        parcel.writeList(mBssidList);
+
+        // LCR Subelement Location Civic
+        parcel.writeString(mCivicLocationCountryCode);
+        parcel.writeString(mCivicLocationString);
+        parcel.writeParcelable(mCivicLocation, flags);
+
+        // LCR Subelement Map Image
+        parcel.writeInt(mMapImageType);
+        if (mMapImageUri != null) {
+            parcel.writeString(mMapImageUri.toString());
+        } else {
+            parcel.writeString("");
+        }
+    }
+
+    /**
+     * Test if the Information Element (IE) is in the correct format, and then parse its Subelements
+     * based on their type, and setting state in this object when present.
+     *
+     * @return a boolean indicating the success of the parsing function
+     */
+    private boolean parseInformationElementBuffer(
+            int ieType, byte[] buffer, byte[] expectedLeadBytes) {
+        int bufferPtr = 0;
+        int bufferLength = buffer.length;
+
+        // Ensure the buffer size is within expected limits
+        if (bufferLength < MIN_BUFFER_SIZE || bufferLength > MAX_BUFFER_SIZE) {
+            return false;
+        }
+
+        // Ensure the IE contains the correct leading bytes
+        byte[] leadBufferBytes = Arrays.copyOfRange(buffer, bufferPtr, expectedLeadBytes.length);
+        if (!Arrays.equals(leadBufferBytes, expectedLeadBytes)) {
+            return false;
+        }
+
+        // Iterate through the sub-elements contained in the Information Element (IE)
+        bufferPtr += expectedLeadBytes.length;
+        // Loop over the buffer ensuring there are 2-bytes available for each new subelement tested.
+        while (bufferPtr + 1 < bufferLength) {
+            byte subelement = buffer[bufferPtr++];
+            int subelementLength = buffer[bufferPtr++];
+            // Check there is enough data for the next subelement
+            if ((bufferPtr + subelementLength) > bufferLength || subelementLength <= 0) {
+                return false;
+            }
+
+            byte[] subelementData =
+                    Arrays.copyOfRange(buffer, bufferPtr, bufferPtr + subelementLength);
+
+            if (ieType == MEASUREMENT_TYPE_LCI) {
+                switch (subelement) {
+                    case SUBELEMENT_LCI:
+                        mIsLciValid = parseSubelementLci(subelementData);
+                        if (!mIsLciValid || mLciVersion != LCI_VERSION_1) {
+                            setLciSubelementDefaults();
+                        }
+                        break;
+                    case SUBELEMENT_Z:
+                        mIsZValid = parseSubelementZ(subelementData);
+                        if (!mIsZValid) {
+                            setZaxisSubelementDefaults();
+                        }
+                        break;
+                    case SUBELEMENT_USAGE:
+                        mIsUsageValid = parseSubelementUsage(subelementData);
+                        // Note: if the Usage Subelement is not valid, don't reset the state, as
+                        // it is now indicating the whole ResponderLocation is invalid.
+                        break;
+                    case SUBELEMENT_BSSID_LIST:
+                        mIsBssidListValid = parseSubelementBssidList(subelementData);
+                        if (!mIsBssidListValid) {
+                            setBssidListSubelementDefaults();
+                        }
+                        break;
+                    default:
+                        break; // skip over unused or vendor specific subelements
+                }
+            } else if (ieType == MEASUREMENT_TYPE_LCR) {
+                switch (subelement) {
+                    case SUBELEMENT_LOCATION_CIVIC:
+                        mIsLocationCivicValid = parseSubelementLocationCivic(subelementData);
+                        if (!mIsLocationCivicValid) {
+                            setCivicLocationSubelementDefaults();
+                        }
+                        break;
+                    case SUBELEMENT_MAP_IMAGE:
+                        mIsMapImageValid = parseSubelementMapImage(subelementData);
+                        if (!mIsMapImageValid) {
+                            setMapImageSubelementDefaults();
+                        }
+                        break;
+                    default:
+                        break; // skip over unused or other vendor specific subelements
+                }
+            }
+
+            bufferPtr += subelementLength;
+        }
+        return true;
+    }
+
+    /**
+     * Parse the LCI Sub-Element in the LCI Information Element (IE).
+     *
+     * @param buffer a buffer containing the subelement
+     * @return boolean true indicates success
+     */
+    private boolean parseSubelementLci(byte[] buffer) {
+        if (buffer.length > SUBELEMENT_LCI_LENGTH) {
+            return false;
+        }
+        swapEndianByteByByte(buffer);
+        long[] subelementLciFields = getFieldData(buffer, SUBELEMENT_LCI_BIT_FIELD_LENGTHS);
+        if (subelementLciFields == null) {
+            return false;
+        }
+        // Set member state based on parsed buffer data
+        mLatitudeUncertainty = decodeLciLatLngUncertainty(
+                subelementLciFields[SUBELEMENT_LCI_LAT_UNCERTAINTY_INDEX]);
+        mLatitude = decodeLciLatLng(subelementLciFields, SUBELEMENT_LCI_BIT_FIELD_LENGTHS,
+                SUBELEMENT_LCI_LAT_INDEX, LAT_ABS_LIMIT);
+        mLongitudeUncertainty = decodeLciLatLngUncertainty(
+                subelementLciFields[SUBELEMENT_LCI_LNG_UNCERTAINTY_INDEX]);
+        mLongitude =
+                decodeLciLatLng(subelementLciFields, SUBELEMENT_LCI_BIT_FIELD_LENGTHS,
+                        SUBELEMENT_LCI_LNG_INDEX, LNG_ABS_LIMIT);
+        mAltitudeType = (int) subelementLciFields[SUBELEMENT_LCI_ALT_TYPE_INDEX] & BYTE_MASK;
+        mAltitudeUncertainty =
+                decodeLciAltUncertainty(subelementLciFields[SUBELEMENT_LCI_ALT_UNCERTAINTY_INDEX]);
+        mAltitude =
+                Math.scalb(subelementLciFields[SUBELEMENT_LCI_ALT_INDEX], -ALTITUDE_FRACTION_BITS);
+        mDatum = (int) subelementLciFields[SUBELEMENT_LCI_DATUM_INDEX] & BYTE_MASK;
+        mLciRegisteredLocationAgreement =
+                (subelementLciFields[SUBELEMENT_LCI_REGLOC_AGREEMENT_INDEX] == 1);
+        mLciRegisteredLocationDse =
+                (subelementLciFields[SUBELEMENT_LCI_REGLOC_DSE_INDEX] == 1);
+        mLciDependentStation =
+                (subelementLciFields[SUBELEMENT_LCI_DEPENDENT_STA_INDEX] == 1);
+        mLciVersion = (int) subelementLciFields[SUBELEMENT_LCI_VERSION_INDEX];
+        return true;
+    }
+
+    /**
+     * Decode the floating point value of an encoded lat or lng in the LCI subelement field.
+     *
+     * @param fields        the array of field data represented as longs
+     * @param bitFieldSizes the lengths of each field
+     * @param offset        the offset of the field being decoded
+     * @param limit the maximum absolute value (note: different for lat vs lng)
+     * @return the floating point value of the lat or lng
+     */
+    private double decodeLciLatLng(long[] fields, int[] bitFieldSizes, int offset, double limit) {
+        double angle;
+        if ((fields[offset] & (long) Math.pow(2, bitFieldSizes[offset] - 1)) != 0) {
+            // Negative 2's complement value
+            // Note: The Math.pow(...) method cannot return a NaN value because the bitFieldSize
+            // for Lat or Lng is limited to exactly 34 bits.
+            angle = Math.scalb(fields[offset] - Math.pow(2, bitFieldSizes[offset]),
+                    -LATLNG_FRACTION_BITS);
+        } else {
+            // Positive 2's complement value
+            angle = Math.scalb(fields[offset], -LATLNG_FRACTION_BITS);
+        }
+        if (angle > limit) {
+            angle = limit;
+        } else if (angle < -limit) {
+            angle = -limit;
+        }
+        return angle;
+    }
+
+    /**
+     * Coverts an encoded Lat/Lng uncertainty into a number of degrees.
+     *
+     * @param encodedValue the encoded uncertainty
+     * @return the value in degrees
+     */
+    private double decodeLciLatLngUncertainty(long encodedValue) {
+        return Math.pow(2, LATLNG_UNCERTAINTY_BASE - encodedValue);
+    }
+
+    /**
+     * Converts an encoded Alt uncertainty into a number of degrees.
+     *
+     * @param encodedValue the encoded uncertainty
+     * @return the value in degrees
+     */
+    private double decodeLciAltUncertainty(long encodedValue) {
+        return Math.pow(2, ALTITUDE_UNCERTAINTY_BASE - encodedValue);
+    }
+
+    /**
+     * Parse the Z subelement of the LCI IE.
+     *
+     * @param buffer a buffer containing the subelement
+     * @return boolean true indicates success
+     */
+    private boolean parseSubelementZ(byte[] buffer) {
+        if (buffer.length != SUBELEMENT_Z_LENGTH) {
+            return false;
+        }
+        swapEndianByteByByte(buffer);
+        long[] subelementZFields = getFieldData(buffer, SUBELEMENT_Z_BIT_FIELD_LENGTHS);
+        if (subelementZFields == null) {
+            return false;
+        }
+
+        mExpectedToMove =
+                (int) subelementZFields[SUBELEMENT_Z_LAT_EXPECTED_TO_MOVE_INDEX] & BYTE_MASK;
+        mFloorNumber = decodeZUnsignedToSignedValue(subelementZFields,
+                SUBELEMENT_Z_BIT_FIELD_LENGTHS, SUBELEMENT_Z_FLOOR_NUMBER_INDEX,
+                Z_FLOOR_NUMBER_FRACTION_BITS);
+
+        mHeightAboveFloorMeters = decodeZUnsignedToSignedValue(subelementZFields,
+                SUBELEMENT_Z_BIT_FIELD_LENGTHS, SUBELEMENT_Z_HEIGHT_ABOVE_FLOOR_INDEX,
+                Z_FLOOR_HEIGHT_FRACTION_BITS);
+
+        long zHeightUncertainty =
+                subelementZFields[SUBELEMENT_Z_HEIGHT_ABOVE_FLOOR_UNCERTAINTY_INDEX];
+        if (zHeightUncertainty > 0 && zHeightUncertainty < Z_MAX_HEIGHT_UNCERTAINTY_FACTOR) {
+            mHeightAboveFloorUncertaintyMeters =
+                    Math.pow(2, Z_FLOOR_HEIGHT_FRACTION_BITS - zHeightUncertainty - 1);
+        } else {
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * Decode a two's complement encoded value, to a signed double based on the field length.
+     *
+     * @param fieldValues the array of field values reprented as longs
+     * @param fieldLengths the array of field lengths
+     * @param index the index of the field being decoded
+     * @param fraction the number of fractional bits in the value
+     * @return the signed value represented as a double
+     */
+    private double decodeZUnsignedToSignedValue(long[] fieldValues, int[] fieldLengths, int index,
+            int fraction) {
+        int value = (int) fieldValues[index];
+        int maxPositiveValue = (int) Math.pow(2, fieldLengths[index] - 1) - 1;
+        if (value > maxPositiveValue) {
+            value -= Math.pow(2, fieldLengths[index]);
+        }
+        return Math.scalb(value, -fraction);
+    }
+
+    /**
+     * Parse Subelement Usage Rights
+     */
+    private boolean parseSubelementUsage(byte[] buffer) {
+        if (buffer.length != SUBELEMENT_USAGE_LENGTH1
+                && buffer.length != SUBELEMENT_USAGE_LENGTH3) {
+            return false;
+        }
+        mUsageRetransmit =
+                (buffer[SUBELEMENT_USAGE_PARAMS_INDEX] & SUBELEMENT_USAGE_MASK_RETRANSMIT) != 0;
+        mUsageRetentionExpires =
+                (buffer[SUBELEMENT_USAGE_PARAMS_INDEX] & SUBELEMENT_USAGE_MASK_RETENTION_EXPIRES)
+                        != 0;
+        mUsageExtraInfoOnAssociation =
+                (buffer[SUBELEMENT_USAGE_PARAMS_INDEX] & SUBELEMENT_USAGE_MASK_STA_LOCATION_POLICY)
+                        != 0;
+        // Note: the Retransmit flag must be true, and RetentionExpires, false for the
+        // ResponderLocation object to be usable by public applications.
+        return (mUsageRetransmit && !mUsageRetentionExpires);
+    }
+
+    /**
+     * Parse the BSSID List Subelement of the LCI IE.
+     *
+     * @param buffer a buffer containing the subelement
+     * @return boolean true indicates success
+     */
+    private boolean parseSubelementBssidList(byte[] buffer) {
+        if (buffer.length < SUBELEMENT_BSSID_LIST_MIN_BUFFER_LENGTH) {
+            return false;
+        }
+        if ((buffer.length - 1) % BYTES_IN_A_BSSID != 0) {
+            return false;
+        }
+
+        int maxBssidIndicator = (int) buffer[SUBELEMENT_BSSID_MAX_INDICATOR_INDEX] & BYTE_MASK;
+        int bssidListLength = (buffer.length - 1) / BYTES_IN_A_BSSID;
+        // Check the max number of BSSIDs agrees with the list length.
+        if (maxBssidIndicator != bssidListLength) {
+            return false;
+        }
+
+        int bssidOffset = SUBELEMENT_BSSID_LIST_INDEX;
+        for (int i = 0; i < bssidListLength; i++) {
+            byte[] bssid = Arrays.copyOfRange(buffer, bssidOffset, bssidOffset + BYTES_IN_A_BSSID);
+            MacAddress macAddress = MacAddress.fromBytes(bssid);
+            mBssidList.add(macAddress);
+            bssidOffset += BYTES_IN_A_BSSID;
+        }
+        return true;
+    }
+
+    /**
+     * Parse the Location Civic subelement in the LCR IE.
+     *
+     * @param buffer a buffer containing the subelement
+     * @return boolean true indicates success
+     */
+    private boolean parseSubelementLocationCivic(byte[] buffer) {
+        if (buffer.length <  SUBELEMENT_LOCATION_CIVIC_MIN_LENGTH
+                || buffer.length > SUBELEMENT_LOCATION_CIVIC_MAX_LENGTH) {
+            return false;
+        }
+        mCivicLocationCountryCode =
+                new String(Arrays.copyOfRange(buffer, CIVIC_COUNTRY_CODE_INDEX,
+                        CIVIC_TLV_LIST_INDEX)).toUpperCase();
+        CivicLocation civicLocation =
+                new CivicLocation(
+                        Arrays.copyOfRange(buffer, CIVIC_TLV_LIST_INDEX, buffer.length),
+                        mCivicLocationCountryCode);
+        if (!civicLocation.isValid()) {
+            return false;
+        }
+        this.mCivicLocation = civicLocation;
+        mCivicLocationString = civicLocation.toString();
+        return true;
+    }
+
+    /**
+     * Parse the Map Image subelement in the LCR IE.
+     *
+     * @param buffer a buffer containing the subelement
+     * @return boolean true indicates success
+     */
+    private boolean parseSubelementMapImage(byte[] buffer) {
+        if (buffer.length > SUBELEMENT_MAP_IMAGE_URL_MAX_LENGTH) {
+            return false;
+        }
+        int mapImageType = buffer[SUBELEMENT_IMAGE_MAP_TYPE_INDEX];
+        int supportedTypesMax = SUPPORTED_IMAGE_FILE_EXTENSIONS.length - 1;
+        if (mapImageType < MAP_TYPE_URL_DEFINED || mapImageType > supportedTypesMax) {
+            return false;
+        }
+        this.mMapImageType = mapImageType;
+        byte[] urlBytes = Arrays.copyOfRange(buffer, 1, buffer.length);
+        mMapImageUri = Uri.parse(new String(urlBytes, StandardCharsets.UTF_8));
+        return true;
+    }
+
+    /**
+     * Convert an image type code to a Mime type
+     *
+     * @param imageTypeCode encoded as an integer
+     * @return the mime type of the image file
+     */
+    private String imageTypeToMime(int imageTypeCode, String imageUrl) {
+        int supportedExtensionsMax = SUPPORTED_IMAGE_FILE_EXTENSIONS.length - 1;
+        if ((imageTypeCode == 0 && imageUrl == null) || imageTypeCode > supportedExtensionsMax) {
+            return null;
+        }
+        MimeTypeMap mimeTypeMap = MimeTypeMap.getSingleton();
+        if (imageTypeCode == 0) {
+            return mimeTypeMap.getMimeTypeFromExtension(
+                    MimeTypeMap.getFileExtensionFromUrl(imageUrl));
+        } else {
+            return mimeTypeMap.getMimeTypeFromExtension(
+                    SUPPORTED_IMAGE_FILE_EXTENSIONS[imageTypeCode]);
+        }
+    }
+
+    /**
+     * Converts a byte array containing fields of variable size, into an array of longs where the
+     * field boundaries are defined in a constant int array passed as an argument.
+     *
+     * @param buffer        the byte array containing all the fields
+     * @param bitFieldSizes the int array defining the size of each field
+     */
+    private long[] getFieldData(byte[] buffer, int[] bitFieldSizes) {
+        int bufferLengthBits = buffer.length * Byte.SIZE;
+        int sumBitFieldSizes = 0;
+        for (int i : bitFieldSizes) {
+            if (i > Long.SIZE) {
+                return null;
+            }
+            sumBitFieldSizes += i;
+        }
+        if (bufferLengthBits != sumBitFieldSizes) {
+            return null;
+        }
+        long[] fieldData = new long[bitFieldSizes.length];
+        int bufferBitPos = 0;
+        for (int fieldIndex = 0; fieldIndex < bitFieldSizes.length; fieldIndex++) {
+            int bitFieldSize = bitFieldSizes[fieldIndex];
+            long field = 0;
+            for (int n = 0; n < bitFieldSize; n++) {
+                field |= ((long) getBitAtBitOffsetInByteArray(buffer, bufferBitPos + n) << n);
+            }
+            fieldData[fieldIndex] = field;
+            bufferBitPos += bitFieldSize;
+        }
+        return fieldData;
+    }
+
+    /**
+     * Retrieves state of a bit at the bit-offset in a byte array, where the offset represents the
+     * bytes in contiguous data with each value in big endian order.
+     *
+     * @param buffer          the data buffer of bytes containing all the fields
+     * @param bufferBitOffset the bit offset into the entire buffer
+     * @return a zero or one value, representing the state of that bit.
+     */
+    private int getBitAtBitOffsetInByteArray(byte[] buffer, int bufferBitOffset) {
+        int bufferIndex = bufferBitOffset / Byte.SIZE; // The byte index that contains the bit
+        int bitOffsetInByte = bufferBitOffset % Byte.SIZE; // The bit offset within that byte
+        int result = (buffer[bufferIndex] & (MSB_IN_BYTE >> bitOffsetInByte)) == 0 ? 0 : 1;
+        return result;
+    }
+
+    /**
+     * Reverses the order of the bits in each byte of a byte array.
+     *
+     * @param buffer the array containing each byte that will be reversed
+     */
+    private void swapEndianByteByByte(byte[] buffer) {
+        for (int n = 0; n < buffer.length; n++) {
+            byte currentByte = buffer[n];
+            byte reversedByte = 0; // Cleared value
+            byte bitSelectorMask = LSB_IN_BYTE;
+            for (int i = 0; i < Byte.SIZE; i++) {
+                reversedByte = (byte) (reversedByte << 1);
+                if ((currentByte & bitSelectorMask) != 0) {
+                    reversedByte = (byte) (reversedByte | LSB_IN_BYTE);
+                }
+                bitSelectorMask = (byte) (bitSelectorMask << 1);
+            }
+            buffer[n] = reversedByte;
+        }
+    }
+
+    /**
+     * Sets the LCI subelement fields to the default undefined values.
+     */
+    private void setLciSubelementDefaults() {
+        mIsLciValid = false;
+        mLatitudeUncertainty = UNCERTAINTY_UNDEFINED;
+        mLatitude = 0;
+        mLongitudeUncertainty = UNCERTAINTY_UNDEFINED;
+        mLongitude = 0;
+        mAltitudeType = ALTITUDE_UNDEFINED;
+        mAltitudeUncertainty = UNCERTAINTY_UNDEFINED;
+        mAltitude = 0;
+        mDatum = DATUM_UNDEFINED;
+        mLciRegisteredLocationAgreement = false;
+        mLciRegisteredLocationDse = false;
+        mLciDependentStation = false;
+        mLciVersion = 0;
+    }
+
+    /**
+     * Sets the Z subelement fields to the default values when undefined.
+     */
+    private void setZaxisSubelementDefaults() {
+        mIsZValid = false;
+        mExpectedToMove = 0;
+        mFloorNumber = 0;
+        mHeightAboveFloorMeters = 0;
+        mHeightAboveFloorUncertaintyMeters = 0;
+    }
+
+    /**
+     * Sets the Usage Policy subelement fields to the default undefined values.
+     */
+    private void setUsageSubelementDefaults() {
+        mUsageRetransmit = true;
+        mUsageRetentionExpires = false;
+        mUsageExtraInfoOnAssociation = false;
+    }
+
+    /**
+     * Sets the BSSID List subelement fields to the default values when undefined.
+     */
+    private void setBssidListSubelementDefaults() {
+        mIsBssidListValid = false;
+        mBssidList = new ArrayList<>();
+    }
+
+    /**
+     * Sets the LCR Civic Location subelement field to the default undefined value.
+     *
+     * @hide
+     */
+    public void setCivicLocationSubelementDefaults() {
+        mIsLocationCivicValid = false;
+        mCivicLocationCountryCode = "";
+        mCivicLocationString = "";
+        mCivicLocation = null;
+    }
+
+    /**
+     * Sets the LCR Map Image subelement field to the default values when undefined.
+     */
+    private void setMapImageSubelementDefaults() {
+        mIsMapImageValid = false;
+        mMapImageType = MAP_TYPE_URL_DEFINED;
+        mMapImageUri = null;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (this == obj) {
+            return true;
+        }
+        if (obj == null || getClass() != obj.getClass()) {
+            return false;
+        }
+        ResponderLocation other = (ResponderLocation) obj;
+        return mIsValid == other.mIsValid
+                && mIsLciValid == other.mIsLciValid
+                && mIsZValid == other.mIsZValid
+                && mIsUsageValid == other.mIsUsageValid
+                && mIsBssidListValid == other.mIsBssidListValid
+                && mIsLocationCivicValid == other.mIsLocationCivicValid
+                && mIsMapImageValid == other.mIsMapImageValid
+                && mLatitudeUncertainty == other.mLatitudeUncertainty
+                && mLatitude == other.mLatitude
+                && mLongitudeUncertainty == other.mLongitudeUncertainty
+                && mLongitude == other.mLongitude
+                && mAltitudeType == other.mAltitudeType
+                && mAltitudeUncertainty == other.mAltitudeUncertainty
+                && mAltitude == other.mAltitude
+                && mDatum == other.mDatum
+                && mLciRegisteredLocationAgreement == other.mLciRegisteredLocationAgreement
+                && mLciRegisteredLocationDse == other.mLciRegisteredLocationDse
+                && mLciDependentStation == other.mLciDependentStation
+                && mLciVersion == other.mLciVersion
+                && mExpectedToMove == other.mExpectedToMove
+                && mFloorNumber == other.mFloorNumber
+                && mHeightAboveFloorMeters == other.mHeightAboveFloorMeters
+                && mHeightAboveFloorUncertaintyMeters
+                        == other.mHeightAboveFloorUncertaintyMeters
+                && mUsageRetransmit == other.mUsageRetransmit
+                && mUsageRetentionExpires == other.mUsageRetentionExpires
+                && mUsageExtraInfoOnAssociation == other.mUsageExtraInfoOnAssociation
+                && mBssidList.equals(other.mBssidList)
+                && mCivicLocationCountryCode.equals(other.mCivicLocationCountryCode)
+                && mCivicLocationString.equals(other.mCivicLocationString)
+                && Objects.equals(mCivicLocation, other.mCivicLocation)
+                && mMapImageType == other.mMapImageType
+                && Objects.equals(mMapImageUri, other.mMapImageUri);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(mIsValid, mIsLciValid, mIsZValid, mIsUsageValid, mIsBssidListValid,
+                mIsLocationCivicValid, mIsMapImageValid, mLatitudeUncertainty, mLatitude,
+                mLongitudeUncertainty, mLongitude, mAltitudeType, mAltitudeUncertainty, mAltitude,
+                mDatum, mLciRegisteredLocationAgreement,
+                mLciRegisteredLocationDse, mLciDependentStation, mLciVersion,
+                mExpectedToMove, mFloorNumber, mHeightAboveFloorMeters,
+                mHeightAboveFloorUncertaintyMeters, mUsageRetransmit, mUsageRetentionExpires,
+                mUsageExtraInfoOnAssociation, mBssidList, mCivicLocationCountryCode,
+                mCivicLocationString, mCivicLocation, mMapImageType, mMapImageUri);
+    }
+
+    /**
+     * @return true if the ResponderLocation object is valid and contains useful information
+     * relevant to the location of the Responder. If this is ever false, this object will not be
+     * available to developers, and have a null value.
+     *
+     * @hide
+     */
+    public boolean isValid() {
+        return mIsValid;
+    }
+
+    /**
+     * @return true if the LCI subelement (containing Latitude, Longitude and Altitude) is valid.
+     *
+     * <p> This method tells us if the responder has provided its Location Configuration
+     * Information (LCI) directly, and is useful when an external database of responder locations
+     * is not available</p>
+     *
+     * <p>If isLciSubelementValid() returns true, all the LCI values provided by the corresponding
+     * getter methods will have been set as described by the responder, or else if false, they
+     * should not be used and will throw an IllegalStateException.</p>
+     */
+    public boolean isLciSubelementValid() {
+        return mIsLciValid;
+    }
+
+    /**
+     * @return the latitude uncertainty in degrees.
+     * <p>
+     * Only valid if {@link #isLciSubelementValid()} returns true, or will throw an exception.
+     * </p>
+     * <p> An unknown uncertainty is indicated by 0.</p>
+     */
+    public double getLatitudeUncertainty() {
+        if (!mIsLciValid) {
+            throw new IllegalStateException(
+                "getLatitudeUncertainty(): invoked on an invalid result: mIsLciValid = false.");
+        }
+        return mLatitudeUncertainty;
+    }
+
+    /**
+     * @return the latitude in degrees
+     * <p>
+     * Only valid if {@link #isLciSubelementValid()} returns true, or will throw an exception.
+     */
+    public double getLatitude() {
+        if (!mIsLciValid) {
+            throw new IllegalStateException(
+                "getLatitude(): invoked on an invalid result: mIsLciValid = false.");
+        }
+        return mLatitude;
+    }
+
+    /**
+     * @return the Longitude uncertainty in degrees.
+     * <p>
+     * Only valid if {@link #isLciSubelementValid()} returns true, or will throw an exception.
+     * </p>
+     * <p> An unknown uncertainty is indicated by 0.</p>
+     */
+    public double getLongitudeUncertainty() {
+        if (!mIsLciValid) {
+            throw new IllegalStateException(
+                "getLongitudeUncertainty(): invoked on an invalid result: mIsLciValid = false.");
+        }
+        return mLongitudeUncertainty;
+    }
+
+    /**
+     * @return the Longitude in degrees..
+     * <p>
+     * Only valid if {@link #isLciSubelementValid()} returns true, or will throw an exception.
+     */
+    public double getLongitude() {
+        if (!mIsLciValid) {
+            throw new IllegalStateException(
+                "getLatitudeUncertainty(): invoked on an invalid result: mIsLciValid = false.");
+        }
+        return mLongitude;
+    }
+
+    /**
+     * @return the Altitude type.
+     * <p>
+     * Only valid if {@link #isLciSubelementValid()} returns true, or will throw an exception.
+     */
+    @AltitudeType
+    public int getAltitudeType() {
+        if (!mIsLciValid) {
+            throw new IllegalStateException(
+                "getLatitudeUncertainty(): invoked on an invalid result: mIsLciValid = false.");
+        }
+        return mAltitudeType;
+    }
+
+    /**
+     * @return the Altitude uncertainty in meters.
+     * <p>
+     * Only valid if {@link #isLciSubelementValid()} returns true, or will throw an exception.
+     * </p>
+     * <p>An unknown uncertainty is indicated by 0.</p>
+     */
+    public double getAltitudeUncertainty() {
+        if (!mIsLciValid) {
+            throw new IllegalStateException(
+                "getLatitudeUncertainty(): invoked on an invalid result: mIsLciValid = false.");
+        }
+        return mAltitudeUncertainty;
+    }
+
+    /**
+     * @return the Altitude in units defined by the altitude type.
+     * <p>
+     * Only valid if {@link #isLciSubelementValid()} returns true, or will throw an exception.
+     */
+    public double getAltitude() {
+        if (!mIsLciValid) {
+            throw new IllegalStateException(
+                "getAltitude(): invoked on an invalid result: mIsLciValid = false.");
+        }
+        return mAltitude;
+    }
+
+    /**
+     * @return the Datum used for the LCI positioning information.
+     * <p>
+     * Only valid if {@link #isLciSubelementValid()} returns true, or will throw an exception.
+     */
+    @DatumType
+    public int getDatum() {
+        if (!mIsLciValid) {
+            throw new IllegalStateException(
+                "getDatum(): invoked on an invalid result: mIsLciValid = false.");
+        }
+        return mDatum;
+    }
+
+    /**
+     * @return true if the station is operating within a national policy area or an international
+     * agreement area near a national border, otherwise false
+     * (see 802.11REVmc Section 11.12.3 - Registered STA Operation).
+     * <p>
+     * Only valid if {@link #isLciSubelementValid()} returns true, or will throw an exception.
+     */
+    public boolean getRegisteredLocationAgreementIndication() {
+        if (!mIsLciValid) {
+            throw new IllegalStateException(
+                "getRegisteredLocationAgreementIndication(): "
+                        + "invoked on an invalid result: mIsLciValid = false.");
+        }
+        return mLciRegisteredLocationAgreement;
+    }
+
+    /**
+     * @return true indicating this is an enabling station, enabling the operation of nearby STAs
+     * with Dynamic Station Enablement (DSE), otherwise false.
+     * (see 802.11REVmc Section 11.12.3 - Registered STA Operation).
+     * <p>
+     * Only valid if {@link #isLciSubelementValid()} returns true, or will throw an exception.
+     *
+     * @hide
+     */
+    public boolean getRegisteredLocationDseIndication() {
+        if (!mIsLciValid) {
+            throw new IllegalStateException(
+                "getRegisteredLocationDseIndication(): "
+                    + "invoked on an invalid result: mIsLciValid = false.");
+        }
+        return mLciRegisteredLocationDse;
+    }
+
+    /**
+     * @return true indicating this is a dependent station that is operating with the enablement of
+     * an enabling station whose LCI is being reported, otherwise false.
+     * (see 802.11REVmc Section 11.12.3 - Registered STA Operation).
+     * <p>
+     * Only valid if {@link #isLciSubelementValid()} returns true, or will throw an exception.
+     *
+     * @hide
+     */
+    public boolean getDependentStationIndication() {
+        if (!mIsLciValid) {
+            throw new IllegalStateException(
+                "getDependentStationIndication(): "
+                    + "invoked on an invalid result: mIsLciValid = false.");
+        }
+        return mLciDependentStation;
+    }
+
+    /**
+     * @return a value greater or equal to 1, indicating the current version number
+     * of the LCI protocol.
+     * <p>
+     * Only valid if {@link #isLciSubelementValid()} returns true, or will throw an exception.
+     */
+    public int getLciVersion() {
+        if (!mIsLciValid) {
+            throw new IllegalStateException(
+                "getLciVersion(): "
+                    + "invoked on an invalid result: mIsLciValid = false.");
+        }
+        return mLciVersion;
+    }
+
+    /**
+     * @return the LCI location represented as a {@link Location} object (best effort).
+     * <p>
+     * Only valid if {@link #isLciSubelementValid()} returns true, or will throw an exception.
+     */
+    @NonNull
+    public Location toLocation() {
+        if (!mIsLciValid) {
+            throw new IllegalStateException(
+                "toLocation(): "
+                    + "invoked on an invalid result: mIsLciValid = false.");
+        }
+        Location location = new Location(LOCATION_PROVIDER);
+        location.setLatitude(mLatitude);
+        location.setLongitude(mLongitude);
+        location.setAccuracy((float) (mLatitudeUncertainty + mLongitudeUncertainty) / 2);
+        location.setAltitude(mAltitude);
+        location.setVerticalAccuracyMeters((float) mAltitudeUncertainty);
+        location.setTime(System.currentTimeMillis());
+        return location;
+    }
+
+    /**
+     * @return if the Z subelement (containing mobility, Floor, Height above floor) is valid.
+     */
+    public boolean isZaxisSubelementValid() {
+        return mIsZValid;
+    }
+
+    /**
+     * @return an integer representing the mobility of the responder.
+     * <p>
+     * Only valid if {@link #isZaxisSubelementValid()} returns true, or will throw an exception.
+     */
+    @ExpectedToMoveType
+    public int getExpectedToMove() {
+        if (!mIsZValid) {
+            throw new IllegalStateException(
+                "getExpectedToMove(): invoked on an invalid result: mIsZValid = false.");
+        }
+        return mExpectedToMove;
+    }
+
+    /**
+     * @return the Z sub element Floor Number.
+     * <p>
+     * Only valid if {@link #isZaxisSubelementValid()} returns true, or will throw an exception.
+     * </p>
+     * <p>Note: this number can be positive or negative, with value increments of +/- 1/16 of a
+     * floor.</p>.
+     */
+    public double getFloorNumber() {
+        if (!mIsZValid) {
+            throw new IllegalStateException(
+                "getFloorNumber(): invoked on an invalid result: mIsZValid = false)");
+        }
+        return mFloorNumber;
+    }
+
+    /**
+     * @return the Z subelement Height above the floor in meters.
+     * <p>
+     * Only valid if {@link #isZaxisSubelementValid()} returns true, or will throw an exception.
+     * </p>
+     * <p>This value can be positive or negative. </p>
+     */
+    public double getHeightAboveFloorMeters() {
+        if (!mIsZValid) {
+            throw new IllegalStateException(
+                "getHeightAboveFloorMeters(): invoked on an invalid result: mIsZValid = false)");
+        }
+        return mHeightAboveFloorMeters;
+    }
+
+    /**
+     * @return the Z subelement Height above the floor uncertainty in meters.
+     * <p>
+     * Only valid if {@link #isZaxisSubelementValid()} returns true, or will throw an exception.
+     * </p>
+     * <p>An unknown uncertainty is indicated by 0.</p>
+     */
+    public double getHeightAboveFloorUncertaintyMeters() {
+        if (!mIsZValid) {
+            throw new IllegalStateException(
+                "getHeightAboveFloorUncertaintyMeters():"
+                    + "invoked on an invalid result: mIsZValid = false)");
+        }
+        return mHeightAboveFloorUncertaintyMeters;
+    }
+
+    /**
+     * @return true if the location information received from the responder can be
+     * retransmitted to another device, physically separate from the one that received it.
+     *
+     * @hide
+     */
+    public boolean getRetransmitPolicyIndication() {
+        return mUsageRetransmit;
+    }
+
+    /**
+     * @return true if location-data received should expire (and be deleted)
+     * by the time provided in the getRelativeExpirationTimeHours() method.
+     *
+     * @hide
+     */
+    public boolean getRetentionExpiresIndication() {
+        return mUsageRetentionExpires;
+    }
+
+    /**
+     * @return true if there is extra location info available on association.
+     *
+     * @hide
+     */
+    @SystemApi
+    public boolean getExtraInfoOnAssociationIndication() {
+        return mUsageExtraInfoOnAssociation;
+    }
+
+    /**
+     * @return the Immutable list of colocated BSSIDs at the responder.
+     *
+     * <p> Will return an empty list when there are no bssids listed.
+     */
+    public List<MacAddress> getColocatedBssids() {
+        return Collections.unmodifiableList(mBssidList);
+    }
+
+    /**
+     * @return the civic location represented as an {@link Address} object (best effort).
+     *
+     * <p> Will return a {@code null} when there is no Civic Location defined.
+     */
+    @Nullable
+    public Address toCivicLocationAddress() {
+        if (mCivicLocation != null && mCivicLocation.isValid()) {
+            return mCivicLocation.toAddress();
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * @return the civic location represented as a {@link SparseArray}
+     * <p>
+     * Valid keys to access the SparseArray can be found in {@code CivicLocationKeys}.
+     * </p>
+     * <p> Will return a {@code null} when there is no Civic Location defined.
+     *
+     */
+    @Nullable
+    public SparseArray toCivicLocationSparseArray() {
+        if (mCivicLocation != null && mCivicLocation.isValid()) {
+            return mCivicLocation.toSparseArray();
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * @return the civic location two upper-case ASCII character country code defined in ISO 3166.
+     *
+     * <p> Will return a {@code null} when there is no country code defined.
+     *
+     * @hide
+     */
+    @Nullable
+    public String getCivicLocationCountryCode() {
+        return mCivicLocationCountryCode;
+    }
+
+    /**
+     * @return the value of the Civic Location String associated with a key.
+     *
+     * <p> Will return a {@code null} when there is no value associated with the key provided.
+     *
+     * @param key used to find a corresponding value in the Civic Location Tuple list
+     *
+     * @hide
+     */
+    @Nullable
+    public String getCivicLocationElementValue(@CivicLocationKeysType int key) {
+        return mCivicLocation.getCivicElementValue(key);
+    }
+
+    /**
+     * @return the Map Image file Mime type, referred to by getMapImageUrl().
+     */
+    @Nullable
+    public String getMapImageMimeType() {
+        if (mMapImageUri == null) {
+            return null;
+        } else {
+            return imageTypeToMime(mMapImageType, mMapImageUri.toString());
+        }
+    }
+
+    /**
+     * @return a URI referencing a map-file showing the local floor plan.
+     *
+     * <p> Will return a {@code null} when there is no URI defined.
+     */
+    @Nullable
+    public Uri getMapImageUri() {
+        return mMapImageUri;
+    }
+}
diff --git a/android/net/wifi/rtt/WifiRttManager.java b/android/net/wifi/rtt/WifiRttManager.java
new file mode 100644
index 0000000..457e904
--- /dev/null
+++ b/android/net/wifi/rtt/WifiRttManager.java
@@ -0,0 +1,190 @@
+/*
+ * Copyright (C) 2018 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.wifi.rtt;
+
+import static android.Manifest.permission.ACCESS_FINE_LOCATION;
+import static android.Manifest.permission.ACCESS_WIFI_STATE;
+import static android.Manifest.permission.CHANGE_WIFI_STATE;
+import static android.Manifest.permission.LOCATION_HARDWARE;
+
+import android.annotation.CallbackExecutor;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.RequiresPermission;
+import android.annotation.SdkConstant;
+import android.annotation.SystemApi;
+import android.annotation.SystemService;
+import android.content.Context;
+import android.os.Binder;
+import android.os.RemoteException;
+import android.os.WorkSource;
+import android.util.Log;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/**
+ * This class provides the primary API for measuring distance (range) to other devices using the
+ * IEEE 802.11mc Wi-Fi Round Trip Time (RTT) technology.
+ * <p>
+ * The devices which can be ranged include:
+ * <li>Access Points (APs)
+ * <li>Wi-Fi Aware peers
+ * <p>
+ * Ranging requests are triggered using
+ * {@link #startRanging(RangingRequest, Executor, RangingResultCallback)}. Results (in case of
+ * successful operation) are returned in the {@link RangingResultCallback#onRangingResults(List)}
+ * callback.
+ * <p>
+ *     Wi-Fi RTT may not be usable at some points, e.g. when Wi-Fi is disabled. To validate that
+ *     the functionality is available use the {@link #isAvailable()} function. To track
+ *     changes in RTT usability register for the {@link #ACTION_WIFI_RTT_STATE_CHANGED}
+ *     broadcast. Note that this broadcast is not sticky - you should register for it and then
+ *     check the above API to avoid a race condition.
+ */
+@SystemService(Context.WIFI_RTT_RANGING_SERVICE)
+public class WifiRttManager {
+    private static final String TAG = "WifiRttManager";
+    private static final boolean VDBG = false;
+
+    private final Context mContext;
+    private final IWifiRttManager mService;
+
+    /**
+     * Broadcast intent action to indicate that the state of Wi-Fi RTT availability has changed.
+     * Use the {@link #isAvailable()} to query the current status.
+     * This broadcast is <b>not</b> sticky, use the {@link #isAvailable()} API after registering
+     * the broadcast to check the current state of Wi-Fi RTT.
+     * <p>Note: The broadcast is only delivered to registered receivers - no manifest registered
+     * components will be launched.
+     */
+    @SdkConstant(SdkConstant.SdkConstantType.BROADCAST_INTENT_ACTION)
+    public static final String ACTION_WIFI_RTT_STATE_CHANGED =
+            "android.net.wifi.rtt.action.WIFI_RTT_STATE_CHANGED";
+
+    /** @hide */
+    public WifiRttManager(Context context, IWifiRttManager service) {
+        mContext = context;
+        mService = service;
+    }
+
+    /**
+     * Returns the current status of RTT API: whether or not RTT is available. To track
+     * changes in the state of RTT API register for the
+     * {@link #ACTION_WIFI_RTT_STATE_CHANGED} broadcast.
+     * <p>Note: availability of RTT does not mean that the app can use the API. The app's
+     * permissions and platform Location Mode are validated at run-time.
+     *
+     * @return A boolean indicating whether the app can use the RTT API at this time (true) or
+     * not (false).
+     */
+    public boolean isAvailable() {
+        try {
+            return mService.isAvailable();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Initiate a request to range to a set of devices specified in the {@link RangingRequest}.
+     * Results will be returned in the {@link RangingResultCallback} set of callbacks.
+     *
+     * @param request  A request specifying a set of devices whose distance measurements are
+     *                 requested.
+     * @param executor The Executor on which to run the callback.
+     * @param callback A callback for the result of the ranging request.
+     */
+    @RequiresPermission(allOf = {ACCESS_FINE_LOCATION, CHANGE_WIFI_STATE, ACCESS_WIFI_STATE})
+    public void startRanging(@NonNull RangingRequest request,
+            @NonNull @CallbackExecutor Executor executor, @NonNull RangingResultCallback callback) {
+        startRanging(null, request, executor, callback);
+    }
+
+    /**
+     * Initiate a request to range to a set of devices specified in the {@link RangingRequest}.
+     * Results will be returned in the {@link RangingResultCallback} set of callbacks.
+     *
+     * @param workSource A mechanism to specify an alternative work-source for the request.
+     * @param request  A request specifying a set of devices whose distance measurements are
+     *                 requested.
+     * @param executor The Executor on which to run the callback.
+     * @param callback A callback for the result of the ranging request.
+     *
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(allOf = {LOCATION_HARDWARE, ACCESS_FINE_LOCATION, CHANGE_WIFI_STATE,
+            ACCESS_WIFI_STATE})
+    public void startRanging(@Nullable WorkSource workSource, @NonNull RangingRequest request,
+            @NonNull @CallbackExecutor Executor executor, @NonNull RangingResultCallback callback) {
+        if (VDBG) {
+            Log.v(TAG, "startRanging: workSource=" + workSource + ", request=" + request
+                    + ", callback=" + callback + ", executor=" + executor);
+        }
+
+        if (executor == null) {
+            throw new IllegalArgumentException("Null executor provided");
+        }
+        if (callback == null) {
+            throw new IllegalArgumentException("Null callback provided");
+        }
+
+        Binder binder = new Binder();
+        try {
+            mService.startRanging(binder, mContext.getOpPackageName(), workSource, request,
+                    new IRttCallback.Stub() {
+                        @Override
+                        public void onRangingFailure(int status) throws RemoteException {
+                            clearCallingIdentity();
+                            executor.execute(() -> callback.onRangingFailure(status));
+                        }
+
+                        @Override
+                        public void onRangingResults(List<RangingResult> results)
+                                throws RemoteException {
+                            clearCallingIdentity();
+                            executor.execute(() -> callback.onRangingResults(results));
+                        }
+                    });
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Cancel all ranging requests for the specified work sources. The requests have been requested
+     * using {@link #startRanging(WorkSource, RangingRequest, Executor, RangingResultCallback)}.
+     *
+     * @param workSource The work-sources of the requesters.
+     *
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(allOf = {LOCATION_HARDWARE})
+    public void cancelRanging(@Nullable WorkSource workSource) {
+        if (VDBG) {
+            Log.v(TAG, "cancelRanging: workSource=" + workSource);
+        }
+
+        try {
+            mService.cancelRanging(workSource);
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+}
diff --git a/android/net/wimax/WimaxManagerConstants.java b/android/net/wimax/WimaxManagerConstants.java
new file mode 100644
index 0000000..b4aaf5b
--- /dev/null
+++ b/android/net/wimax/WimaxManagerConstants.java
@@ -0,0 +1,104 @@
+package android.net.wimax;
+
+/**
+ * {@hide}
+ */
+public class WimaxManagerConstants
+{
+
+    /**
+     * Used by android.net.wimax.WimaxManager for handling management of
+     * Wimax access.
+     */
+    public static final String WIMAX_SERVICE = "WiMax";
+
+    /**
+     * Broadcast intent action indicating that Wimax has been enabled, disabled,
+     * enabling, disabling, or unknown. One extra provides this state as an int.
+     * Another extra provides the previous state, if available.
+     */
+    public static final String NET_4G_STATE_CHANGED_ACTION =
+        "android.net.fourG.NET_4G_STATE_CHANGED";
+
+    /**
+     * The lookup key for an int that indicates whether Wimax is enabled,
+     * disabled, enabling, disabling, or unknown.
+     */
+    public static final String EXTRA_WIMAX_STATUS = "wimax_status";
+
+    /**
+     * Broadcast intent action indicating that Wimax state has been changed
+     * state could be scanning, connecting, connected, disconnecting, disconnected
+     * initializing, initialized, unknown and ready. One extra provides this state as an int.
+     * Another extra provides the previous state, if available.
+     */
+    public static final String  WIMAX_NETWORK_STATE_CHANGED_ACTION =
+        "android.net.fourG.wimax.WIMAX_NETWORK_STATE_CHANGED";
+
+    /**
+     * Broadcast intent action indicating that Wimax signal level has been changed.
+     * Level varies from 0 to 3.
+     */
+    public static final String SIGNAL_LEVEL_CHANGED_ACTION =
+        "android.net.wimax.SIGNAL_LEVEL_CHANGED";
+
+    /**
+     * The lookup key for an int that indicates whether Wimax state is
+     * scanning, connecting, connected, disconnecting, disconnected
+     * initializing, initialized, unknown and ready.
+     */
+    public static final String EXTRA_WIMAX_STATE = "WimaxState";
+    public static final String EXTRA_4G_STATE = "4g_state";
+    public static final String EXTRA_WIMAX_STATE_INT = "WimaxStateInt";
+    /**
+     * The lookup key for an int that indicates whether state of Wimax
+     * is idle.
+     */
+    public static final String EXTRA_WIMAX_STATE_DETAIL = "WimaxStateDetail";
+
+    /**
+     * The lookup key for an int that indicates Wimax signal level.
+     */
+    public static final String EXTRA_NEW_SIGNAL_LEVEL = "newSignalLevel";
+
+    /**
+     * Indicatates Wimax is disabled.
+     */
+    public static final int NET_4G_STATE_DISABLED = 1;
+
+    /**
+     * Indicatates Wimax is enabled.
+     */
+    public static final int NET_4G_STATE_ENABLED = 3;
+
+    /**
+     * Indicatates Wimax status is known.
+     */
+    public static final int NET_4G_STATE_UNKNOWN = 4;
+
+    /**
+     * Indicatates Wimax is in idle state.
+     */
+    public static final int WIMAX_IDLE = 6;
+
+    /**
+     * Indicatates Wimax is being deregistered.
+     */
+    public static final int WIMAX_DEREGISTRATION = 8;
+
+    /**
+     * Indicatates wimax state is unknown.
+     */
+    public static final int WIMAX_STATE_UNKNOWN = 0;
+
+    /**
+     * Indicatates wimax state is connected.
+     */
+    public static final int WIMAX_STATE_CONNECTED = 7;
+
+    /**
+     * Indicatates wimax state is disconnected.
+     */
+    public static final int WIMAX_STATE_DISCONNECTED = 9;
+
+}