Merge "Revert "Whitelist packages from VPN lockdown.""
diff --git a/core/java/android/net/CaptivePortal.java b/core/java/android/net/CaptivePortal.java
index 4047068..3b01266 100644
--- a/core/java/android/net/CaptivePortal.java
+++ b/core/java/android/net/CaptivePortal.java
@@ -45,6 +45,8 @@
     private final IBinder mBinder;
 
     /** @hide */
+    @SystemApi
+    @TestApi
     public CaptivePortal(IBinder binder) {
         mBinder = binder;
     }
@@ -107,6 +109,8 @@
      * 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);
diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java
index cee3a40..5bb24ba 100644
--- a/core/java/android/net/ConnectivityManager.java
+++ b/core/java/android/net/ConnectivityManager.java
@@ -15,6 +15,9 @@
  */
 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;
@@ -28,6 +31,8 @@
 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;
@@ -58,6 +63,7 @@
 
 import libcore.net.event.NetworkEventDispatcher;
 
+import java.io.FileDescriptor;
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.net.InetAddress;
@@ -66,6 +72,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.Executor;
 
 /**
  * Class that answers queries about the state of network connectivity. It also
@@ -1699,6 +1706,8 @@
      * {@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 {
@@ -1802,6 +1811,8 @@
     /**
      * Starts an IPsec NAT-T keepalive packet with the specified parameters.
      *
+     * @deprecated Use {@link #createSocketKeepalive} instead.
+     *
      * @hide
      */
     @UnsupportedAppUsage
@@ -1821,6 +1832,62 @@
     }
 
     /**
+     * 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, which can be used to control this keepalive object.
+     **/
+    public SocketKeepalive createSocketKeepalive(@NonNull Network network,
+            @NonNull UdpEncapsulationSocket socket,
+            @NonNull InetAddress source,
+            @NonNull InetAddress destination,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull Callback callback) {
+        return new NattSocketKeepalive(mService, network, socket.getFileDescriptor(),
+            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 fd The {@link FileDescriptor} that needs to be kept alive. The provided
+     *        {@link FileDescriptor} 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.
+     *
+     * @hide
+     */
+    @SystemApi
+    @RequiresPermission(android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD)
+    public SocketKeepalive createNattKeepalive(@NonNull Network network,
+            @NonNull FileDescriptor fd,
+            @NonNull InetAddress source,
+            @NonNull InetAddress destination,
+            @NonNull @CallbackExecutor Executor executor,
+            @NonNull Callback callback) {
+        return new NattSocketKeepalive(mService, network, fd, INVALID_RESOURCE_ID /* Unused */,
+                source, destination, 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.
@@ -3699,6 +3766,19 @@
     }
 
     /**
+     * Determine whether the device is configured to avoid bad wifi.
+     * @hide
+     */
+    @SystemApi
+    public boolean getAvoidBadWifi() {
+        try {
+            return mService.getAvoidBadWifi();
+        } 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
diff --git a/core/java/android/net/DnsResolver.java b/core/java/android/net/DnsResolver.java
new file mode 100644
index 0000000..6d54264
--- /dev/null
+++ b/core/java/android/net/DnsResolver.java
@@ -0,0 +1,289 @@
+/*
+ * 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.resNetworkQuery;
+import static android.net.NetworkUtils.resNetworkResult;
+import static android.net.NetworkUtils.resNetworkSend;
+import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_ERROR;
+import static android.os.MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.os.Handler;
+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.function.Consumer;
+
+
+/**
+ * Dns resolver class for asynchronous dns querying
+ *
+ */
+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;
+
+    @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;
+
+    private static final int DNS_RAW_RESPONSE = 1;
+
+    private static final int NETID_UNSET = 0;
+
+    private static final DnsResolver sInstance = new DnsResolver();
+
+    /**
+     * listener for receiving raw answers
+     */
+    public interface RawAnswerListener {
+        /**
+         * {@code byte[]} is {@code null} if query timed out
+         */
+        void onAnswer(@Nullable byte[] answer);
+    }
+
+    /**
+     * listener for receiving parsed answers
+     */
+    public interface InetAddressAnswerListener {
+        /**
+         * Will be called exactly once with all the answers to the query.
+         * size of addresses will be zero if no available answer could be parsed.
+         */
+        void onAnswer(@NonNull List<InetAddress> addresses);
+    }
+
+    /**
+     * Get instance for DnsResolver
+     */
+    public static DnsResolver getInstance() {
+        return sInstance;
+    }
+
+    private DnsResolver() {}
+
+    /**
+     * Pass in a blob and corresponding setting,
+     * get a blob back asynchronously with the entire raw answer.
+     *
+     * @param network {@link Network} specifying which network for querying.
+     *         {@code null} for query on default network.
+     * @param query blob message
+     * @param flags flags as a combination of the FLAGS_* constants
+     * @param handler {@link Handler} to specify the thread
+     *         upon which the {@link RawAnswerListener} will be invoked.
+     * @param listener a {@link RawAnswerListener} which will be called to notify the caller
+     *         of the result of dns query.
+     */
+    public void query(@Nullable Network network, @NonNull byte[] query, @QueryFlag int flags,
+            @NonNull Handler handler, @NonNull RawAnswerListener listener) throws ErrnoException {
+        final FileDescriptor queryfd = resNetworkSend((network != null
+                ? network.netId : NETID_UNSET), query, query.length, flags);
+        registerFDListener(handler.getLooper().getQueue(), queryfd,
+                answerbuf -> listener.onAnswer(answerbuf));
+    }
+
+    /**
+     * Pass in a domain name and corresponding setting,
+     * get a blob back asynchronously with the entire raw answer.
+     *
+     * @param network {@link Network} specifying which network for querying.
+     *         {@code null} for query on default network.
+     * @param domain domain name for querying
+     * @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 handler {@link Handler} to specify the thread
+     *         upon which the {@link RawAnswerListener} will be invoked.
+     * @param listener a {@link RawAnswerListener} which will be called to notify the caller
+     *         of the result of dns query.
+     */
+    public void query(@Nullable Network network, @NonNull String domain, @QueryClass int nsClass,
+            @QueryType int nsType, @QueryFlag int flags,
+            @NonNull Handler handler, @NonNull RawAnswerListener listener) throws ErrnoException {
+        final FileDescriptor queryfd = resNetworkQuery((network != null
+                ? network.netId : NETID_UNSET), domain, nsClass, nsType, flags);
+        registerFDListener(handler.getLooper().getQueue(), queryfd,
+                answerbuf -> listener.onAnswer(answerbuf));
+    }
+
+    /**
+     * Pass in a domain name and corresponding setting,
+     * get back a set of InetAddresses asynchronously.
+     *
+     * @param network {@link Network} specifying which network for querying.
+     *         {@code null} for query on default network.
+     * @param domain domain name for querying
+     * @param flags flags as a combination of the FLAGS_* constants
+     * @param handler {@link Handler} to specify the thread
+     *         upon which the {@link InetAddressAnswerListener} will be invoked.
+     * @param listener an {@link InetAddressAnswerListener} 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 Handler handler, @NonNull InetAddressAnswerListener listener)
+            throws ErrnoException {
+        final FileDescriptor v4fd = resNetworkQuery((network != null
+                ? network.netId : NETID_UNSET), domain, CLASS_IN, TYPE_A, flags);
+        final FileDescriptor v6fd = resNetworkQuery((network != null
+                ? network.netId : NETID_UNSET), domain, CLASS_IN, TYPE_AAAA, flags);
+
+        final InetAddressAnswerAccumulator accmulator =
+                new InetAddressAnswerAccumulator(2, listener);
+        final Consumer<byte[]> consumer = answerbuf ->
+                accmulator.accumulate(parseAnswers(answerbuf));
+
+        registerFDListener(handler.getLooper().getQueue(), v4fd, consumer);
+        registerFDListener(handler.getLooper().getQueue(), v6fd, consumer);
+    }
+
+    private void registerFDListener(@NonNull MessageQueue queue,
+            @NonNull FileDescriptor queryfd, @NonNull Consumer<byte[]> answerConsumer) {
+        queue.addOnFileDescriptorEventListener(
+                queryfd,
+                FD_EVENTS,
+                (fd, events) -> {
+                    byte[] answerbuf = null;
+                    try {
+                    // TODO: Implement result function in Java side instead of using JNI
+                    //       Because JNI method close fd prior than unregistering fd on
+                    //       event listener.
+                        answerbuf = resNetworkResult(fd);
+                    } catch (ErrnoException e) {
+                        Log.e(TAG, "resNetworkResult:" + e.toString());
+                    }
+                    answerConsumer.accept(answerbuf);
+
+                    // Unregister this fd listener
+                    return 0;
+                });
+    }
+
+    private 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.rcode != 0) {
+                throw new ParseException("Response error, rcode:" + mHeader.rcode);
+            }
+            if (mHeader.getSectionCount(ANSECTION) == 0) {
+                throw new ParseException("No available answer");
+            }
+            if (mHeader.getSectionCount(QDSECTION) == 0) {
+                throw new ParseException("No question found");
+            }
+            // Assume only one question per answer packet. (RFC1035)
+            mQueryType = mSections[QDSECTION].get(0).nsType;
+        }
+
+        public @NonNull List<InetAddress> getAddresses() {
+            final List<InetAddress> results = new ArrayList<InetAddress>();
+            for (final DnsSection ansSec : mSections[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;
+        }
+    }
+
+    private @Nullable List<InetAddress> parseAnswers(@Nullable byte[] data) {
+        try {
+            return (data == null) ? null : new DnsAddressAnswer(data).getAddresses();
+        } catch (DnsPacket.ParseException e) {
+            Log.e(TAG, "Parse answer fail " + e.getMessage());
+            return null;
+        }
+    }
+
+    private class InetAddressAnswerAccumulator {
+        private final List<InetAddress> mAllAnswers;
+        private final InetAddressAnswerListener mAnswerListener;
+        private final int mTargetAnswerCount;
+        private int mReceivedAnswerCount = 0;
+
+        InetAddressAnswerAccumulator(int size, @NonNull InetAddressAnswerListener listener) {
+            mTargetAnswerCount = size;
+            mAllAnswers = new ArrayList<>();
+            mAnswerListener = listener;
+        }
+
+        public void accumulate(@Nullable List<InetAddress> answer) {
+            if (null != answer) {
+                mAllAnswers.addAll(answer);
+            }
+            if (++mReceivedAnswerCount == mTargetAnswerCount) {
+                mAnswerListener.onAnswer(mAllAnswers);
+            }
+        }
+    }
+}
diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl
index 3d34574..e97060a 100644
--- a/core/java/android/net/IConnectivityManager.aidl
+++ b/core/java/android/net/IConnectivityManager.aidl
@@ -165,6 +165,7 @@
     void setAvoidUnvalidated(in Network network);
     void startCaptivePortalApp(in Network network);
 
+    boolean getAvoidBadWifi();
     int getMultipathPreference(in Network Network);
 
     NetworkRequest getDefaultRequest();
@@ -180,6 +181,10 @@
     void startNattKeepalive(in Network network, int intervalSeconds, in Messenger messenger,
             in IBinder binder, String srcAddr, int srcPort, String dstAddr);
 
+    void startNattKeepaliveWithFd(in Network network, in FileDescriptor fd, int resourceId,
+            int intervalSeconds, in Messenger messenger, in IBinder binder, String srcAddr,
+            String dstAddr);
+
     void stopKeepalive(in Network network, int slot);
 
     String getCaptivePortalServerUrl();
diff --git a/core/java/android/net/IpPrefix.java b/core/java/android/net/IpPrefix.java
index 4631c56..b996cda 100644
--- a/core/java/android/net/IpPrefix.java
+++ b/core/java/android/net/IpPrefix.java
@@ -16,6 +16,8 @@
 
 package android.net;
 
+import android.annotation.SystemApi;
+import android.annotation.TestApi;
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.util.Pair;
@@ -83,6 +85,8 @@
      * @param prefixLength the prefix length. Must be &gt;= 0 and &lt;= (32 or 128) (IPv4 or IPv6).
      * @hide
      */
+    @SystemApi
+    @TestApi
     public IpPrefix(InetAddress address, 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.
diff --git a/core/java/android/net/LinkAddress.java b/core/java/android/net/LinkAddress.java
index a536d08..fbd602c 100644
--- a/core/java/android/net/LinkAddress.java
+++ b/core/java/android/net/LinkAddress.java
@@ -162,6 +162,8 @@
      *              {@link OsConstants#RT_SCOPE_LINK} or {@link OsConstants#RT_SCOPE_SITE}).
      * @hide
      */
+    @SystemApi
+    @TestApi
     public LinkAddress(InetAddress address, int prefixLength, int flags, int scope) {
         init(address, prefixLength, flags, scope);
     }
diff --git a/core/java/android/net/LinkProperties.java b/core/java/android/net/LinkProperties.java
index 21b6a8e..6628701 100644
--- a/core/java/android/net/LinkProperties.java
+++ b/core/java/android/net/LinkProperties.java
@@ -174,7 +174,8 @@
     /**
      * @hide
      */
-    @UnsupportedAppUsage
+    @SystemApi
+    @TestApi
     public LinkProperties(LinkProperties source) {
         if (source != null) {
             mIfaceName = source.mIfaceName;
@@ -576,6 +577,8 @@
      * @param addresses The {@link Collection} of PCSCF servers to set in this object.
      * @hide
      */
+    @SystemApi
+    @TestApi
     public void setPcscfServers(Collection<InetAddress> pcscfServers) {
         mPcscfs.clear();
         for (InetAddress pcscfServer: pcscfServers) {
@@ -590,6 +593,8 @@
      *         this link.
      * @hide
      */
+    @SystemApi
+    @TestApi
     public List<InetAddress> getPcscfServers() {
         return Collections.unmodifiableList(mPcscfs);
     }
@@ -781,6 +786,8 @@
      * @return the NAT64 prefix.
      * @hide
      */
+    @SystemApi
+    @TestApi
     public @Nullable IpPrefix getNat64Prefix() {
         return mNat64Prefix;
     }
@@ -794,6 +801,8 @@
      * @param prefix the NAT64 prefix.
      * @hide
      */
+    @SystemApi
+    @TestApi
     public void setNat64Prefix(IpPrefix prefix) {
         if (prefix != null && prefix.getPrefixLength() != 96) {
             throw new IllegalArgumentException("Only 96-bit prefixes are supported: " + prefix);
diff --git a/core/java/android/net/NattSocketKeepalive.java b/core/java/android/net/NattSocketKeepalive.java
new file mode 100644
index 0000000..88631ae
--- /dev/null
+++ b/core/java/android/net/NattSocketKeepalive.java
@@ -0,0 +1,75 @@
+/*
+ * 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.Binder;
+import android.os.RemoteException;
+import android.util.Log;
+
+import java.io.FileDescriptor;
+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;
+    @NonNull private final FileDescriptor mFd;
+    private final int mResourceId;
+
+    NattSocketKeepalive(@NonNull IConnectivityManager service,
+            @NonNull Network network,
+            @NonNull FileDescriptor fd,
+            int resourceId,
+            @NonNull InetAddress source,
+            @NonNull InetAddress destination,
+            @NonNull Executor executor,
+            @NonNull Callback callback) {
+        super(service, network, executor, callback);
+        mSource = source;
+        mDestination = destination;
+        mFd = fd;
+        mResourceId = resourceId;
+    }
+
+    @Override
+    void startImpl(int intervalSec) {
+        try {
+            mService.startNattKeepaliveWithFd(mNetwork, mFd, mResourceId, intervalSec, mMessenger,
+                    new Binder(), mSource.getHostAddress(), mDestination.getHostAddress());
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error starting packet keepalive: ", e);
+            stopLooper();
+        }
+    }
+
+    @Override
+    void stopImpl() {
+        try {
+            if (mSlot != null) {
+                mService.stopKeepalive(mNetwork, mSlot);
+            }
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error stopping packet keepalive: ", e);
+            stopLooper();
+        }
+    }
+}
diff --git a/core/java/android/net/NetworkUtils.java b/core/java/android/net/NetworkUtils.java
index c0aa4a6..7f4d8cd 100644
--- a/core/java/android/net/NetworkUtils.java
+++ b/core/java/android/net/NetworkUtils.java
@@ -34,6 +34,8 @@
 import java.util.Locale;
 import java.util.TreeSet;
 
+import android.system.ErrnoException;
+
 /**
  * Native methods for managing network interfaces.
  *
@@ -133,6 +135,32 @@
     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 a byte array containing blob answer
+     */
+    public static native byte[] resNetworkResult(FileDescriptor fd) throws ErrnoException;
+
+    /**
      * Add an entry into the ARP cache.
      */
     public static void addArpEntry(Inet4Address ipv4Addr, MacAddress ethAddr, String ifname,
diff --git a/core/java/android/net/ProxyInfo.java b/core/java/android/net/ProxyInfo.java
index e926fda..ef2269a 100644
--- a/core/java/android/net/ProxyInfo.java
+++ b/core/java/android/net/ProxyInfo.java
@@ -39,12 +39,12 @@
  */
 public class ProxyInfo implements Parcelable {
 
-    private String mHost;
-    private int mPort;
-    private String mExclusionList;
-    private String[] mParsedExclusionList;
+    private final String mHost;
+    private final int mPort;
+    private final String mExclusionList;
+    private final String[] mParsedExclusionList;
+    private final Uri mPacFileUrl;
 
-    private Uri mPacFileUrl;
     /**
      *@hide
      */
@@ -96,7 +96,8 @@
     public ProxyInfo(String host, int port, String exclList) {
         mHost = host;
         mPort = port;
-        setExclusionList(exclList);
+        mExclusionList = exclList;
+        mParsedExclusionList = parseExclusionList(mExclusionList);
         mPacFileUrl = Uri.EMPTY;
     }
 
@@ -107,7 +108,8 @@
     public ProxyInfo(Uri pacFileUrl) {
         mHost = LOCAL_HOST;
         mPort = LOCAL_PORT;
-        setExclusionList(LOCAL_EXCL_LIST);
+        mExclusionList = LOCAL_EXCL_LIST;
+        mParsedExclusionList = parseExclusionList(mExclusionList);
         if (pacFileUrl == null) {
             throw new NullPointerException();
         }
@@ -121,7 +123,8 @@
     public ProxyInfo(String pacFileUrl) {
         mHost = LOCAL_HOST;
         mPort = LOCAL_PORT;
-        setExclusionList(LOCAL_EXCL_LIST);
+        mExclusionList = LOCAL_EXCL_LIST;
+        mParsedExclusionList = parseExclusionList(mExclusionList);
         mPacFileUrl = Uri.parse(pacFileUrl);
     }
 
@@ -132,13 +135,22 @@
     public ProxyInfo(Uri pacFileUrl, int localProxyPort) {
         mHost = LOCAL_HOST;
         mPort = localProxyPort;
-        setExclusionList(LOCAL_EXCL_LIST);
+        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;
@@ -159,6 +171,10 @@
             mExclusionList = source.getExclusionListAsString();
             mParsedExclusionList = source.mParsedExclusionList;
         } else {
+            mHost = null;
+            mPort = 0;
+            mExclusionList = null;
+            mParsedExclusionList = null;
             mPacFileUrl = Uri.EMPTY;
         }
     }
@@ -214,24 +230,14 @@
         return mExclusionList;
     }
 
-    // comma separated
-    private void setExclusionList(String exclusionList) {
-        mExclusionList = exclusionList;
-        if (mExclusionList == null) {
-            mParsedExclusionList = new String[0];
-        } else {
-            mParsedExclusionList = exclusionList.toLowerCase(Locale.ROOT).split(",");
-        }
-    }
-
     /**
      * @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);
+                mPort == 0 ? "" : Integer.toString(mPort),
+                mExclusionList == null ? "" : mExclusionList);
     }
 
     /**
@@ -262,7 +268,7 @@
             sb.append("] ");
             sb.append(Integer.toString(mPort));
             if (mExclusionList != null) {
-                    sb.append(" xl=").append(mExclusionList);
+                sb.append(" xl=").append(mExclusionList);
             }
         } else {
             sb.append("[ProxyProperties.mHost == null]");
@@ -308,8 +314,8 @@
      */
     public int hashCode() {
         return ((null == mHost) ? 0 : mHost.hashCode())
-        + ((null == mExclusionList) ? 0 : mExclusionList.hashCode())
-        + mPort;
+                + ((null == mExclusionList) ? 0 : mExclusionList.hashCode())
+                + mPort;
     }
 
     /**
@@ -352,8 +358,7 @@
                 }
                 String exclList = in.readString();
                 String[] parsedExclList = in.readStringArray();
-                ProxyInfo proxyProperties =
-                        new ProxyInfo(host, port, exclList, parsedExclList);
+                ProxyInfo proxyProperties = new ProxyInfo(host, port, exclList, parsedExclList);
                 return proxyProperties;
             }
 
diff --git a/core/java/android/net/RouteInfo.java b/core/java/android/net/RouteInfo.java
index 6bf2c67..5c0f758 100644
--- a/core/java/android/net/RouteInfo.java
+++ b/core/java/android/net/RouteInfo.java
@@ -110,6 +110,8 @@
      *
      * @hide
      */
+    @SystemApi
+    @TestApi
     public RouteInfo(IpPrefix destination, InetAddress gateway, String iface, int type) {
         switch (type) {
             case RTN_UNICAST:
diff --git a/core/java/android/net/SocketKeepalive.java b/core/java/android/net/SocketKeepalive.java
new file mode 100644
index 0000000..97d50f4
--- /dev/null
+++ b/core/java/android/net/SocketKeepalive.java
@@ -0,0 +1,224 @@
+/*
+ * 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.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.Process;
+import android.util.Log;
+
+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.
+ */
+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 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;
+
+    /** @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;
+
+    @NonNull final IConnectivityManager mService;
+    @NonNull final Network mNetwork;
+    @NonNull private final Executor mExecutor;
+    @NonNull private final SocketKeepalive.Callback mCallback;
+    @NonNull private final Looper mLooper;
+    @NonNull final Messenger mMessenger;
+    @NonNull Integer mSlot;
+
+    SocketKeepalive(@NonNull IConnectivityManager service, @NonNull Network network,
+            @NonNull Executor executor, @NonNull Callback callback) {
+        mService = service;
+        mNetwork = network;
+        mExecutor = executor;
+        mCallback = callback;
+        // TODO: 1. Use other thread modeling instead of create one thread for every instance to
+        //          reduce the memory cost.
+        //       2. support restart.
+        //       3. Fix race condition which caused by rapidly start and stop.
+        HandlerThread thread = new HandlerThread(TAG, Process.THREAD_PRIORITY_BACKGROUND
+                + Process.THREAD_PRIORITY_LESS_FAVORABLE);
+        thread.start();
+        mLooper = thread.getLooper();
+        mMessenger = new Messenger(new Handler(mLooper) {
+            @Override
+            public void handleMessage(Message message) {
+                switch (message.what) {
+                    case NetworkAgent.EVENT_PACKET_KEEPALIVE:
+                        final int status = message.arg2;
+                        try {
+                            if (status == SUCCESS) {
+                                if (mSlot == null) {
+                                    mSlot = message.arg1;
+                                    mExecutor.execute(() -> mCallback.onStarted());
+                                } else {
+                                    mSlot = null;
+                                    stopLooper();
+                                    mExecutor.execute(() -> mCallback.onStopped());
+                                }
+                            } else if (status == DATA_RECEIVED) {
+                                stopLooper();
+                                mExecutor.execute(() -> mCallback.onDataReceived());
+                            } else {
+                                stopLooper();
+                                mExecutor.execute(() -> mCallback.onError(status));
+                            }
+                        } catch (Exception e) {
+                            Log.e(TAG, "Exception in keepalive callback(" + status + ")", e);
+                        }
+                        break;
+                    default:
+                        Log.e(TAG, "Unhandled message " + Integer.toHexString(message.what));
+                        break;
+                }
+            }
+        });
+    }
+
+    /**
+     * Request that keepalive be started with the given {@code intervalSec}. See
+     * {@link SocketKeepalive}.
+     *
+     * @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);
+
+    /** @hide */
+    protected void stopLooper() {
+        // TODO: remove this after changing thread modeling.
+        mLooper.quit();
+    }
+
+    /**
+     * 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();
+        stopLooper();
+    }
+
+    /**
+     * 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. */
+        public void onDataReceived() {}
+    }
+}
diff --git a/services/net/java/android/net/util/MultinetworkPolicyTracker.java b/core/java/android/net/util/MultinetworkPolicyTracker.java
similarity index 100%
rename from services/net/java/android/net/util/MultinetworkPolicyTracker.java
rename to core/java/android/net/util/MultinetworkPolicyTracker.java
diff --git a/core/jni/android_net_NetUtils.cpp b/core/jni/android_net_NetUtils.cpp
index 9b138eb..7eddcfe 100644
--- a/core/jni/android_net_NetUtils.cpp
+++ b/core/jni/android_net_NetUtils.cpp
@@ -16,8 +16,11 @@
 
 #define LOG_TAG "NetUtils"
 
+#include <vector>
+
 #include "jni.h"
 #include <nativehelper/JNIHelp.h>
+#include <nativehelper/ScopedLocalRef.h>
 #include "NetdClient.h"
 #include <utils/misc.h>
 #include <android_runtime/AndroidRuntime.h>
@@ -55,6 +58,31 @@
 static const uint32_t kUDPDstPortIndirectOffset = kEtherHeaderLen + offsetof(udphdr, dest);
 static const uint16_t kDhcpClientPort = 68;
 
+constexpr int MAXPACKETSIZE = 8 * 1024;
+// FrameworkListener limits the size of commands to 1024 bytes. TODO: fix this.
+constexpr int MAXCMDSIZE = 1024;
+
+static void throwErrnoException(JNIEnv* env, const char* functionName, int error) {
+    ScopedLocalRef<jstring> detailMessage(env, env->NewStringUTF(functionName));
+    if (detailMessage.get() == NULL) {
+        // Not really much we can do here. We're probably dead in the water,
+        // but let's try to stumble on...
+        env->ExceptionClear();
+    }
+    static jclass errnoExceptionClass =
+            MakeGlobalRefOrDie(env, FindClassOrDie(env, "android/system/ErrnoException"));
+
+    static jmethodID errnoExceptionCtor =
+            GetMethodIDOrDie(env, errnoExceptionClass,
+            "<init>", "(Ljava/lang/String;I)V");
+
+    jobject exception = env->NewObject(errnoExceptionClass,
+                                       errnoExceptionCtor,
+                                       detailMessage.get(),
+                                       error);
+    env->Throw(reinterpret_cast<jthrowable>(exception));
+}
+
 static void android_net_utils_attachDhcpFilter(JNIEnv *env, jobject clazz, jobject javaFd)
 {
     struct sock_filter filter_code[] = {
@@ -372,6 +400,63 @@
     }
 }
 
+static jobject android_net_utils_resNetworkQuery(JNIEnv *env, jobject thiz, jint netId,
+        jstring dname, jint ns_class, jint ns_type, jint flags) {
+    const jsize javaCharsCount = env->GetStringLength(dname);
+    const jsize byteCountUTF8 = env->GetStringUTFLength(dname);
+
+    // Only allow dname which could be simply formatted to UTF8.
+    // In native layer, res_mkquery would re-format the input char array to packet.
+    std::vector<char> queryname(byteCountUTF8 + 1, 0);
+
+    env->GetStringUTFRegion(dname, 0, javaCharsCount, queryname.data());
+    int fd = resNetworkQuery(netId, queryname.data(), ns_class, ns_type, flags);
+
+    if (fd < 0) {
+        throwErrnoException(env, "resNetworkQuery", -fd);
+        return nullptr;
+    }
+
+    return jniCreateFileDescriptor(env, fd);
+}
+
+static jobject android_net_utils_resNetworkSend(JNIEnv *env, jobject thiz, jint netId,
+        jbyteArray msg, jint msgLen, jint flags) {
+    uint8_t data[MAXCMDSIZE];
+
+    checkLenAndCopy(env, msg, msgLen, data);
+    int fd = resNetworkSend(netId, data, msgLen, flags);
+
+    if (fd < 0) {
+        throwErrnoException(env, "resNetworkSend", -fd);
+        return nullptr;
+    }
+
+    return jniCreateFileDescriptor(env, fd);
+}
+
+static jbyteArray android_net_utils_resNetworkResult(JNIEnv *env, jobject thiz, jobject javaFd) {
+    int fd = jniGetFDFromFileDescriptor(env, javaFd);
+    int rcode;
+    std::vector<uint8_t> buf(MAXPACKETSIZE, 0);
+
+    int res = resNetworkResult(fd, &rcode, buf.data(), MAXPACKETSIZE);
+    if (res < 0) {
+        throwErrnoException(env, "resNetworkResult", -res);
+        return nullptr;
+    }
+
+    jbyteArray answer = env->NewByteArray(res);
+    if (answer == nullptr) {
+        throwErrnoException(env, "resNetworkResult", ENOMEM);
+        return nullptr;
+    } else {
+        env->SetByteArrayRegion(answer, 0, res,
+                reinterpret_cast<jbyte*>(buf.data()));
+    }
+
+    return answer;
+}
 
 // ----------------------------------------------------------------------------
 
@@ -391,6 +476,9 @@
     { "attachRaFilter", "(Ljava/io/FileDescriptor;I)V", (void*) android_net_utils_attachRaFilter },
     { "attachControlPacketFilter", "(Ljava/io/FileDescriptor;I)V", (void*) android_net_utils_attachControlPacketFilter },
     { "setupRaSocket", "(Ljava/io/FileDescriptor;I)V", (void*) android_net_utils_setupRaSocket },
+    { "resNetworkSend", "(I[BII)Ljava/io/FileDescriptor;", (void*) android_net_utils_resNetworkSend },
+    { "resNetworkQuery", "(ILjava/lang/String;III)Ljava/io/FileDescriptor;", (void*) android_net_utils_resNetworkQuery },
+    { "resNetworkResult", "(Ljava/io/FileDescriptor;)[B", (void*) android_net_utils_resNetworkResult },
 };
 
 int register_android_net_NetworkUtils(JNIEnv* env)
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index 919a5ab..14e2354 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -73,6 +73,7 @@
 import android.net.LinkProperties;
 import android.net.LinkProperties.CompareResult;
 import android.net.MatchAllNetworkSpecifier;
+import android.net.NattSocketKeepalive;
 import android.net.Network;
 import android.net.NetworkAgent;
 import android.net.NetworkCapabilities;
@@ -97,10 +98,10 @@
 import android.net.metrics.IpConnectivityLog;
 import android.net.metrics.NetworkEvent;
 import android.net.netlink.InetDiagMessage;
+import android.net.shared.NetdService;
 import android.net.shared.NetworkMonitorUtils;
 import android.net.shared.PrivateDnsConfig;
 import android.net.util.MultinetworkPolicyTracker;
-import android.net.util.NetdService;
 import android.os.Binder;
 import android.os.Build;
 import android.os.Bundle;
@@ -237,6 +238,9 @@
     // connect anyway?" dialog after the user selects a network that doesn't validate.
     private static final int PROMPT_UNVALIDATED_DELAY_MS = 8 * 1000;
 
+    // How long to dismiss network notification.
+    private static final int TIMEOUT_NOTIFICATION_DELAY_MS = 20 * 1000;
+
     // Default to 30s linger time-out. Modifiable only for testing.
     private static final String LINGER_DELAY_PROPERTY = "persist.netmon.linger";
     private static final int DEFAULT_LINGER_DELAY_MS = 30_000;
@@ -473,6 +477,11 @@
     public static final int EVENT_PROVISIONING_NOTIFICATION = 43;
 
     /**
+     * This event can handle dismissing notification by given network id.
+     */
+    public static final int EVENT_TIMEOUT_NOTIFICATION = 44;
+
+    /**
      * Argument for {@link #EVENT_PROVISIONING_NOTIFICATION} to indicate that the notification
      * should be shown.
      */
@@ -506,7 +515,8 @@
 
     // A helper object to track the current default HTTP proxy. ConnectivityService needs to tell
     // the world when it changes.
-    private final ProxyTracker mProxyTracker;
+    @VisibleForTesting
+    protected final ProxyTracker mProxyTracker;
 
     final private SettingsObserver mSettingsObserver;
 
@@ -815,7 +825,7 @@
         mPolicyManagerInternal = checkNotNull(
                 LocalServices.getService(NetworkPolicyManagerInternal.class),
                 "missing NetworkPolicyManagerInternal");
-        mProxyTracker = new ProxyTracker(context, mHandler, EVENT_PROXY_HAS_CHANGED);
+        mProxyTracker = makeProxyTracker();
 
         mNetd = NetdService.getInstance();
         mKeyStore = KeyStore.getInstance();
@@ -981,6 +991,11 @@
                 deps);
     }
 
+    @VisibleForTesting
+    protected ProxyTracker makeProxyTracker() {
+        return new ProxyTracker(mContext, mHandler, EVENT_PROXY_HAS_CHANGED);
+    }
+
     private static NetworkCapabilities createDefaultNetworkCapabilitiesForUid(int uid) {
         final NetworkCapabilities netCap = new NetworkCapabilities();
         netCap.addCapability(NET_CAPABILITY_INTERNET);
@@ -2476,6 +2491,11 @@
                     final boolean valid = (msg.arg1 == NETWORK_TEST_RESULT_VALID);
                     final boolean wasValidated = nai.lastValidated;
                     final boolean wasDefault = isDefaultNetwork(nai);
+                    if (nai.everCaptivePortalDetected && !nai.captivePortalLoginNotified
+                            && valid) {
+                        nai.captivePortalLoginNotified = true;
+                        showNetworkNotification(nai, NotificationType.LOGGED_IN);
+                    }
 
                     final String redirectUrl = (msg.obj instanceof String) ? (String) msg.obj : "";
 
@@ -2496,7 +2516,15 @@
                         updateCapabilities(oldScore, nai, nai.networkCapabilities);
                         // If score has changed, rebroadcast to NetworkFactories. b/17726566
                         if (oldScore != nai.getCurrentScore()) sendUpdatedScoreToFactories(nai);
-                        if (valid) handleFreshlyValidatedNetwork(nai);
+                        if (valid) {
+                            handleFreshlyValidatedNetwork(nai);
+                            // Clear NO_INTERNET and LOST_INTERNET notifications if network becomes
+                            // valid.
+                            mNotifier.clearNotification(nai.network.netId,
+                                    NotificationType.NO_INTERNET);
+                            mNotifier.clearNotification(nai.network.netId,
+                                    NotificationType.LOST_INTERNET);
+                        }
                     }
                     updateInetCondition(nai);
                     // Let the NetworkAgent know the state of its network
@@ -2520,6 +2548,9 @@
                         final int oldScore = nai.getCurrentScore();
                         nai.lastCaptivePortalDetected = visible;
                         nai.everCaptivePortalDetected |= visible;
+                        if (visible) {
+                            nai.captivePortalLoginNotified = false;
+                        }
                         if (nai.lastCaptivePortalDetected &&
                             Settings.Global.CAPTIVE_PORTAL_MODE_AVOID == getCaptivePortalMode()) {
                             if (DBG) log("Avoiding captive portal network: " + nai.name());
@@ -2531,7 +2562,10 @@
                         updateCapabilities(oldScore, nai, nai.networkCapabilities);
                     }
                     if (!visible) {
-                        mNotifier.clearNotification(netId);
+                        // Only clear SIGN_IN and NETWORK_SWITCH notifications here, or else other
+                        // notifications belong to the same network may be cleared unexpected.
+                        mNotifier.clearNotification(netId, NotificationType.SIGN_IN);
+                        mNotifier.clearNotification(netId, NotificationType.NETWORK_SWITCH);
                     } else {
                         if (nai == null) {
                             loge("EVENT_PROVISIONING_NOTIFICATION from unknown NetworkMonitor");
@@ -2640,8 +2674,7 @@
     }
 
     private boolean networkRequiresValidation(NetworkAgentInfo nai) {
-        return isValidationRequired(
-                mDefaultRequest.networkCapabilities, nai.networkCapabilities);
+        return isValidationRequired(nai.networkCapabilities);
     }
 
     private void handleFreshlyValidatedNetwork(NetworkAgentInfo nai) {
@@ -3183,6 +3216,15 @@
         return mMultinetworkPolicyTracker.getAvoidBadWifi();
     }
 
+    @Override
+    public boolean getAvoidBadWifi() {
+        if (!checkNetworkStackPermission()) {
+            throw new SecurityException("avoidBadWifi requires NETWORK_STACK permission");
+        }
+        return avoidBadWifi();
+    }
+
+
     private void rematchForAvoidBadWifiUpdate() {
         rematchAllNetworksAndRequests(null, 0);
         for (NetworkAgentInfo nai: mNetworkAgentInfos.values()) {
@@ -3229,9 +3271,15 @@
         pw.decreaseIndent();
     }
 
-    private void showValidationNotification(NetworkAgentInfo nai, NotificationType type) {
+    private void showNetworkNotification(NetworkAgentInfo nai, NotificationType type) {
         final String action;
         switch (type) {
+            case LOGGED_IN:
+                action = Settings.ACTION_WIFI_SETTINGS;
+                mHandler.removeMessages(EVENT_TIMEOUT_NOTIFICATION);
+                mHandler.sendMessageDelayed(mHandler.obtainMessage(EVENT_TIMEOUT_NOTIFICATION,
+                        nai.network.netId, 0), TIMEOUT_NOTIFICATION_DELAY_MS);
+                break;
             case NO_INTERNET:
                 action = ConnectivityManager.ACTION_PROMPT_UNVALIDATED;
                 break;
@@ -3244,10 +3292,12 @@
         }
 
         Intent intent = new Intent(action);
-        intent.setData(Uri.fromParts("netId", Integer.toString(nai.network.netId), null));
-        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
-        intent.setClassName("com.android.settings",
-                "com.android.settings.wifi.WifiNoInternetDialog");
+        if (type != NotificationType.LOGGED_IN) {
+            intent.setData(Uri.fromParts("netId", Integer.toString(nai.network.netId), null));
+            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+            intent.setClassName("com.android.settings",
+                    "com.android.settings.wifi.WifiNoInternetDialog");
+        }
 
         PendingIntent pendingIntent = PendingIntent.getActivityAsUser(
                 mContext, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT, null, UserHandle.CURRENT);
@@ -3265,7 +3315,7 @@
                 !nai.networkMisc.explicitlySelected || nai.networkMisc.acceptUnvalidated) {
             return;
         }
-        showValidationNotification(nai, NotificationType.NO_INTERNET);
+        showNetworkNotification(nai, NotificationType.NO_INTERNET);
     }
 
     private void handleNetworkUnvalidated(NetworkAgentInfo nai) {
@@ -3274,7 +3324,7 @@
 
         if (nc.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) &&
             mMultinetworkPolicyTracker.shouldNotifyWifiUnvalidated()) {
-            showValidationNotification(nai, NotificationType.LOST_INTERNET);
+            showNetworkNotification(nai, NotificationType.LOST_INTERNET);
         }
     }
 
@@ -3420,6 +3470,9 @@
                 case EVENT_DATA_SAVER_CHANGED:
                     handleRestrictBackgroundChanged(toBool(msg.arg1));
                     break;
+                case EVENT_TIMEOUT_NOTIFICATION:
+                    mNotifier.clearNotification(msg.arg1, NotificationType.LOGGED_IN);
+                    break;
             }
         }
     }
@@ -3677,20 +3730,46 @@
         }
     }
 
+    /**
+     * Returns information about the proxy a certain network is using. If given a null network, it
+     * it will return the proxy for the bound network for the caller app or the default proxy if
+     * none.
+     *
+     * @param network the network we want to get the proxy information for.
+     * @return Proxy information if a network has a proxy configured, or otherwise null.
+     */
     @Override
     public ProxyInfo getProxyForNetwork(Network network) {
-        if (network == null) return mProxyTracker.getDefaultProxy();
         final ProxyInfo globalProxy = mProxyTracker.getGlobalProxy();
         if (globalProxy != null) return globalProxy;
-        if (!NetworkUtils.queryUserAccess(Binder.getCallingUid(), network.netId)) return null;
-        // Don't call getLinkProperties() as it requires ACCESS_NETWORK_STATE permission, which
-        // caller may not have.
+        if (network == null) {
+            // Get the network associated with the calling UID.
+            final Network activeNetwork = getActiveNetworkForUidInternal(Binder.getCallingUid(),
+                    true);
+            if (activeNetwork == null) {
+                return null;
+            }
+            return getLinkPropertiesProxyInfo(activeNetwork);
+        } else if (queryUserAccess(Binder.getCallingUid(), network.netId)) {
+            // Don't call getLinkProperties() as it requires ACCESS_NETWORK_STATE permission, which
+            // caller may not have.
+            return getLinkPropertiesProxyInfo(network);
+        }
+        // No proxy info available if the calling UID does not have network access.
+        return null;
+    }
+
+    @VisibleForTesting
+    protected boolean queryUserAccess(int uid, int netId) {
+        return NetworkUtils.queryUserAccess(uid, netId);
+    }
+
+    private ProxyInfo getLinkPropertiesProxyInfo(Network network) {
         final NetworkAgentInfo nai = getNetworkAgentInfoForNetwork(network);
         if (nai == null) return null;
         synchronized (nai) {
-            final ProxyInfo proxyInfo = nai.linkProperties.getHttpProxy();
-            if (proxyInfo == null) return null;
-            return new ProxyInfo(proxyInfo);
+            final ProxyInfo linkHttpProxy = nai.linkProperties.getHttpProxy();
+            return linkHttpProxy == null ? null : new ProxyInfo(linkHttpProxy);
         }
     }
 
@@ -3714,11 +3793,10 @@
         mProxyTracker.setDefaultProxy(proxy);
     }
 
-    // If the proxy has changed from oldLp to newLp, resend proxy broadcast with default proxy.
-    // This method gets called when any network changes proxy, but the broadcast only ever contains
-    // the default proxy (even if it hasn't changed).
-    // TODO: Deprecate the broadcast extras as they aren't necessarily applicable in a multi-network
-    // world where an app might be bound to a non-default network.
+    // If the proxy has changed from oldLp to newLp, resend proxy broadcast. This method gets called
+    // when any network changes proxy.
+    // TODO: Remove usage of broadcast extras as they are deprecated and not applicable in a
+    // multi-network world where an app might be bound to a non-default network.
     private void updateProxy(LinkProperties newLp, LinkProperties oldLp) {
         ProxyInfo newProxyInfo = newLp == null ? null : newLp.getHttpProxy();
         ProxyInfo oldProxyInfo = oldLp == null ? null : oldLp.getHttpProxy();
@@ -5885,12 +5963,6 @@
             }
             scheduleUnvalidatedPrompt(networkAgent);
 
-            if (networkAgent.isVPN()) {
-                // Temporarily disable the default proxy (not global).
-                mProxyTracker.setDefaultProxyEnabled(false);
-                // TODO: support proxy per network.
-            }
-
             // Whether a particular NetworkRequest listen should cause signal strength thresholds to
             // be communicated to a particular NetworkAgent depends only on the network's immutable,
             // capabilities, so it only needs to be done once on initial connect, not every time the
@@ -5909,10 +5981,16 @@
         } else if (state == NetworkInfo.State.DISCONNECTED) {
             networkAgent.asyncChannel.disconnect();
             if (networkAgent.isVPN()) {
-                mProxyTracker.setDefaultProxyEnabled(true);
                 updateUids(networkAgent, networkAgent.networkCapabilities, null);
             }
             disconnectAndDestroyNetwork(networkAgent);
+            if (networkAgent.isVPN()) {
+                // As the active or bound network changes for apps, broadcast the default proxy, as
+                // apps may need to update their proxy data. This is called after disconnecting from
+                // VPN to make sure we do not broadcast the old proxy data.
+                // TODO(b/122649188): send the broadcast only to VPN users.
+                mProxyTracker.sendProxyBroadcast();
+            }
         } else if ((oldInfo != null && oldInfo.getState() == NetworkInfo.State.SUSPENDED) ||
                 state == NetworkInfo.State.SUSPENDED) {
             // going into or coming out of SUSPEND: re-score and notify
@@ -6177,6 +6255,17 @@
     }
 
     @Override
+    public void startNattKeepaliveWithFd(Network network, FileDescriptor fd, int resourceId,
+            int intervalSeconds, Messenger messenger, IBinder binder, String srcAddr,
+            String dstAddr) {
+        enforceKeepalivePermission();
+        mKeepaliveTracker.startNattKeepalive(
+                getNetworkAgentInfoForNetwork(network), fd, resourceId,
+                intervalSeconds, messenger, binder,
+                srcAddr, dstAddr, NattSocketKeepalive.NATT_PORT);
+    }
+
+    @Override
     public void stopKeepalive(Network network, int slot) {
         mHandler.sendMessage(mHandler.obtainMessage(
                 NetworkAgent.CMD_STOP_PACKET_KEEPALIVE, slot, PacketKeepalive.SUCCESS, network));
diff --git a/services/core/java/com/android/server/connectivity/KeepaliveTracker.java b/services/core/java/com/android/server/connectivity/KeepaliveTracker.java
index 8a3cdca..1559ba8 100644
--- a/services/core/java/com/android/server/connectivity/KeepaliveTracker.java
+++ b/services/core/java/com/android/server/connectivity/KeepaliveTracker.java
@@ -16,40 +16,49 @@
 
 package com.android.server.connectivity;
 
-import com.android.internal.util.HexDump;
-import com.android.internal.util.IndentingPrintWriter;
-import com.android.server.connectivity.NetworkAgentInfo;
-import android.net.ConnectivityManager;
+// TODO: Clean up imports and remove references of PacketKeepalive constants.
+
+import static android.net.ConnectivityManager.PacketKeepalive.ERROR_INVALID_INTERVAL;
+import static android.net.ConnectivityManager.PacketKeepalive.ERROR_INVALID_IP_ADDRESS;
+import static android.net.ConnectivityManager.PacketKeepalive.ERROR_INVALID_NETWORK;
+import static android.net.ConnectivityManager.PacketKeepalive.MIN_INTERVAL;
+import static android.net.ConnectivityManager.PacketKeepalive.NATT_PORT;
+import static android.net.ConnectivityManager.PacketKeepalive.NO_KEEPALIVE;
+import static android.net.ConnectivityManager.PacketKeepalive.SUCCESS;
+import static android.net.NetworkAgent.CMD_START_PACKET_KEEPALIVE;
+import static android.net.NetworkAgent.CMD_STOP_PACKET_KEEPALIVE;
+import static android.net.NetworkAgent.EVENT_PACKET_KEEPALIVE;
+import static android.net.SocketKeepalive.ERROR_INVALID_SOCKET;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.net.ConnectivityManager.PacketKeepalive;
 import android.net.KeepalivePacketData;
-import android.net.LinkAddress;
 import android.net.NetworkAgent;
 import android.net.NetworkUtils;
 import android.net.util.IpUtils;
 import android.os.Binder;
-import android.os.IBinder;
 import android.os.Handler;
+import android.os.IBinder;
 import android.os.Message;
 import android.os.Messenger;
 import android.os.Process;
 import android.os.RemoteException;
-import android.system.OsConstants;
+import android.system.ErrnoException;
+import android.system.Os;
 import android.util.Log;
 import android.util.Pair;
 
-import java.nio.ByteBuffer;
-import java.nio.ByteOrder;
-import java.net.Inet4Address;
-import java.net.Inet6Address;
+import com.android.internal.util.HexDump;
+import com.android.internal.util.IndentingPrintWriter;
+
+import java.io.FileDescriptor;
 import java.net.InetAddress;
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
 import java.util.ArrayList;
 import java.util.HashMap;
 
-import static android.net.ConnectivityManager.PacketKeepalive.*;
-import static android.net.NetworkAgent.CMD_START_PACKET_KEEPALIVE;
-import static android.net.NetworkAgent.CMD_STOP_PACKET_KEEPALIVE;
-import static android.net.NetworkAgent.EVENT_PACKET_KEEPALIVE;
-
 /**
  * Manages packet keepalive requests.
  *
@@ -300,7 +309,9 @@
         }
     }
 
-    public void handleEventPacketKeepalive(NetworkAgentInfo nai, Message message) {
+    /** Handle keepalive events from lower layer. */
+    public void handleEventPacketKeepalive(@NonNull NetworkAgentInfo nai,
+            @NonNull Message message) {
         int slot = message.arg1;
         int reason = message.arg2;
 
@@ -330,8 +341,18 @@
         }
     }
 
-    public void startNattKeepalive(NetworkAgentInfo nai, int intervalSeconds, Messenger messenger,
-            IBinder binder, String srcAddrString, int srcPort, String dstAddrString, int dstPort) {
+    /**
+     * Called when requesting that keepalives be started on a IPsec NAT-T socket. See
+     * {@link android.net.SocketKeepalive}.
+     **/
+    public void startNattKeepalive(@Nullable NetworkAgentInfo nai,
+            int intervalSeconds,
+            @NonNull Messenger messenger,
+            @NonNull IBinder binder,
+            @NonNull String srcAddrString,
+            int srcPort,
+            @NonNull String dstAddrString,
+            int dstPort) {
         if (nai == null) {
             notifyMessenger(messenger, NO_KEEPALIVE, ERROR_INVALID_NETWORK);
             return;
@@ -360,6 +381,56 @@
                 NetworkAgent.CMD_START_PACKET_KEEPALIVE, ki).sendToTarget();
     }
 
+   /**
+    * Called when requesting that keepalives be started on a IPsec NAT-T socket. This function is
+    * identical to {@link #startNattKeepalive}, but also takes a {@code resourceId}, which is the
+    * resource index bound to the {@link UdpEncapsulationSocket} when creating by
+    * {@link com.android.server.IpSecService} to verify whether the given
+    * {@link UdpEncapsulationSocket} is legitimate.
+    **/
+    public void startNattKeepalive(@Nullable NetworkAgentInfo nai,
+            @Nullable FileDescriptor fd,
+            int resourceId,
+            int intervalSeconds,
+            @NonNull Messenger messenger,
+            @NonNull IBinder binder,
+            @NonNull String srcAddrString,
+            @NonNull String dstAddrString,
+            int dstPort) {
+        // Ensure that the socket is created by IpSecService.
+        if (!isNattKeepaliveSocketValid(fd, resourceId)) {
+            notifyMessenger(messenger, NO_KEEPALIVE, ERROR_INVALID_SOCKET);
+        }
+
+        // Get src port to adopt old API.
+        int srcPort = 0;
+        try {
+            final SocketAddress srcSockAddr = Os.getsockname(fd);
+            srcPort = ((InetSocketAddress) srcSockAddr).getPort();
+        } catch (ErrnoException e) {
+            notifyMessenger(messenger, NO_KEEPALIVE, ERROR_INVALID_SOCKET);
+        }
+
+        // Forward request to old API.
+        startNattKeepalive(nai, intervalSeconds, messenger, binder, srcAddrString, srcPort,
+                dstAddrString, dstPort);
+    }
+
+    /**
+     * Verify if the IPsec NAT-T file descriptor and resource Id hold for IPsec keepalive is valid.
+     **/
+    public static boolean isNattKeepaliveSocketValid(@Nullable FileDescriptor fd, int resourceId) {
+        // TODO: 1. confirm whether the fd is called from system api or created by IpSecService.
+        //       2. If the fd is created from the system api, check that it's bounded. And
+        //          call dup to keep the fd open.
+        //       3. If the fd is created from IpSecService, check if the resource ID is valid. And
+        //          hold the resource needed in IpSecService.
+        if (null == fd) {
+            return false;
+        }
+        return true;
+    }
+
     public void dump(IndentingPrintWriter pw) {
         pw.println("Packet keepalives:");
         pw.increaseIndent();
diff --git a/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java b/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
index 9ea73fb..d0cff25 100644
--- a/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
+++ b/services/core/java/com/android/server/connectivity/NetworkAgentInfo.java
@@ -152,6 +152,10 @@
     // Whether a captive portal was found during the last network validation attempt.
     public boolean lastCaptivePortalDetected;
 
+    // Indicates the user was notified of a successful captive portal login since a portal was
+    // last detected.
+    public boolean captivePortalLoginNotified;
+
     // Networks are lingered when they become unneeded as a result of their NetworkRequests being
     // satisfied by a higher-scoring network. so as to allow communication to wrap up before the
     // network is taken down.  This usually only happens to the default network. Lingering ends with
@@ -618,18 +622,19 @@
     }
 
     public String toString() {
-        return "NetworkAgentInfo{ ni{" + networkInfo + "}  " +
-                "network{" + network + "}  nethandle{" + network.getNetworkHandle() + "}  " +
-                "lp{" + linkProperties + "}  " +
-                "nc{" + networkCapabilities + "}  Score{" + getCurrentScore() + "}  " +
-                "everValidated{" + everValidated + "}  lastValidated{" + lastValidated + "}  " +
-                "created{" + created + "} lingering{" + isLingering() + "} " +
-                "explicitlySelected{" + networkMisc.explicitlySelected + "} " +
-                "acceptUnvalidated{" + networkMisc.acceptUnvalidated + "} " +
-                "everCaptivePortalDetected{" + everCaptivePortalDetected + "} " +
-                "lastCaptivePortalDetected{" + lastCaptivePortalDetected + "} " +
-                "clat{" + clatd + "} " +
-                "}";
+        return "NetworkAgentInfo{ ni{" + networkInfo + "}  "
+                + "network{" + network + "}  nethandle{" + network.getNetworkHandle() + "}  "
+                + "lp{" + linkProperties + "}  "
+                + "nc{" + networkCapabilities + "}  Score{" + getCurrentScore() + "}  "
+                + "everValidated{" + everValidated + "}  lastValidated{" + lastValidated + "}  "
+                + "created{" + created + "} lingering{" + isLingering() + "} "
+                + "explicitlySelected{" + networkMisc.explicitlySelected + "} "
+                + "acceptUnvalidated{" + networkMisc.acceptUnvalidated + "} "
+                + "everCaptivePortalDetected{" + everCaptivePortalDetected + "} "
+                + "lastCaptivePortalDetected{" + lastCaptivePortalDetected + "} "
+                + "captivePortalLoginNotified{" + captivePortalLoginNotified + "} "
+                + "clat{" + clatd + "} "
+                + "}";
     }
 
     public String name() {
diff --git a/services/core/java/com/android/server/connectivity/NetworkNotificationManager.java b/services/core/java/com/android/server/connectivity/NetworkNotificationManager.java
index 36a2476..b50477b 100644
--- a/services/core/java/com/android/server/connectivity/NetworkNotificationManager.java
+++ b/services/core/java/com/android/server/connectivity/NetworkNotificationManager.java
@@ -16,13 +16,16 @@
 
 package com.android.server.connectivity;
 
+import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
+import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
+import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
+
 import android.app.Notification;
 import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.content.Context;
 import android.content.Intent;
 import android.content.res.Resources;
-import android.net.NetworkCapabilities;
 import android.net.wifi.WifiInfo;
 import android.os.UserHandle;
 import android.telephony.TelephonyManager;
@@ -31,15 +34,12 @@
 import android.util.SparseArray;
 import android.util.SparseIntArray;
 import android.widget.Toast;
+
 import com.android.internal.R;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.messages.nano.SystemMessageProto.SystemMessage;
 import com.android.internal.notification.SystemNotificationChannels;
 
-import static android.net.NetworkCapabilities.NET_CAPABILITY_INTERNET;
-import static android.net.NetworkCapabilities.TRANSPORT_CELLULAR;
-import static android.net.NetworkCapabilities.TRANSPORT_WIFI;
-
 public class NetworkNotificationManager {
 
 
@@ -47,7 +47,8 @@
         LOST_INTERNET(SystemMessage.NOTE_NETWORK_LOST_INTERNET),
         NETWORK_SWITCH(SystemMessage.NOTE_NETWORK_SWITCH),
         NO_INTERNET(SystemMessage.NOTE_NETWORK_NO_INTERNET),
-        SIGN_IN(SystemMessage.NOTE_NETWORK_SIGN_IN);
+        SIGN_IN(SystemMessage.NOTE_NETWORK_SIGN_IN),
+        LOGGED_IN(SystemMessage.NOTE_NETWORK_LOGGED_IN);
 
         public final int eventId;
 
@@ -192,6 +193,9 @@
                     details = r.getString(R.string.network_available_sign_in_detailed, name);
                     break;
             }
+        } else if (notifyType == NotificationType.LOGGED_IN) {
+            title = WifiInfo.removeDoubleQuotes(nai.networkCapabilities.getSSID());
+            details = r.getString(R.string.captive_portal_logged_in_detailed);
         } else if (notifyType == NotificationType.NETWORK_SWITCH) {
             String fromTransport = getTransportName(transportType);
             String toTransport = getTransportName(getFirstTransportType(switchToNai));
@@ -239,6 +243,18 @@
         }
     }
 
+    /**
+     * Clear the notification with the given id, only if it matches the given type.
+     */
+    public void clearNotification(int id, NotificationType notifyType) {
+        final int previousEventId = mNotificationTypeMap.get(id);
+        final NotificationType previousNotifyType = NotificationType.getFromId(previousEventId);
+        if (notifyType != previousNotifyType) {
+            return;
+        }
+        clearNotification(id);
+    }
+
     public void clearNotification(int id) {
         if (mNotificationTypeMap.indexOfKey(id) < 0) {
             return;
@@ -290,6 +306,10 @@
         return (t != null) ? t.name() : "UNKNOWN";
     }
 
+    /**
+     * A notification with a higher number will take priority over a notification with a lower
+     * number.
+     */
     private static int priority(NotificationType t) {
         if (t == null) {
             return 0;
@@ -302,6 +322,7 @@
             case NETWORK_SWITCH:
                 return 2;
             case LOST_INTERNET:
+            case LOGGED_IN:
                 return 1;
             default:
                 return 0;
diff --git a/services/core/java/com/android/server/connectivity/ProxyTracker.java b/services/core/java/com/android/server/connectivity/ProxyTracker.java
index fdddccd..a671287 100644
--- a/services/core/java/com/android/server/connectivity/ProxyTracker.java
+++ b/services/core/java/com/android/server/connectivity/ProxyTracker.java
@@ -309,22 +309,4 @@
             }
         }
     }
-
-    /**
-     * Enable or disable the default proxy.
-     *
-     * This sets the flag for enabling/disabling the default proxy and sends the broadcast
-     * if applicable.
-     * @param enabled whether the default proxy should be enabled.
-     */
-    public void setDefaultProxyEnabled(final boolean enabled) {
-        synchronized (mProxyLock) {
-            if (mDefaultProxyEnabled != enabled) {
-                mDefaultProxyEnabled = enabled;
-                if (mGlobalProxy == null && mDefaultProxy != null) {
-                    sendProxyBroadcast();
-                }
-            }
-        }
-    }
 }
diff --git a/tests/net/java/com/android/server/ConnectivityServiceTest.java b/tests/net/java/com/android/server/ConnectivityServiceTest.java
index 1c26418..923c7dd 100644
--- a/tests/net/java/com/android/server/ConnectivityServiceTest.java
+++ b/tests/net/java/com/android/server/ConnectivityServiceTest.java
@@ -107,6 +107,8 @@
 import android.net.INetworkStatsService;
 import android.net.InterfaceConfiguration;
 import android.net.IpPrefix;
+import android.net.IpSecManager;
+import android.net.IpSecManager.UdpEncapsulationSocket;
 import android.net.LinkAddress;
 import android.net.LinkProperties;
 import android.net.MatchAllNetworkSpecifier;
@@ -121,7 +123,9 @@
 import android.net.NetworkSpecifier;
 import android.net.NetworkStack;
 import android.net.NetworkUtils;
+import android.net.ProxyInfo;
 import android.net.RouteInfo;
+import android.net.SocketKeepalive;
 import android.net.UidRange;
 import android.net.metrics.IpConnectivityLog;
 import android.net.shared.NetworkMonitorUtils;
@@ -158,6 +162,7 @@
 import com.android.server.connectivity.IpConnectivityMetrics;
 import com.android.server.connectivity.MockableSystemProperties;
 import com.android.server.connectivity.Nat464Xlat;
+import com.android.server.connectivity.ProxyTracker;
 import com.android.server.connectivity.Tethering;
 import com.android.server.connectivity.Vpn;
 import com.android.server.net.NetworkPinner;
@@ -186,6 +191,8 @@
 import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
 import java.util.concurrent.LinkedBlockingQueue;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -402,8 +409,8 @@
         private final ConditionVariable mPreventReconnectReceived = new ConditionVariable();
         private int mScore;
         private NetworkAgent mNetworkAgent;
-        private int mStartKeepaliveError = PacketKeepalive.ERROR_HARDWARE_UNSUPPORTED;
-        private int mStopKeepaliveError = PacketKeepalive.NO_KEEPALIVE;
+        private int mStartKeepaliveError = SocketKeepalive.ERROR_HARDWARE_UNSUPPORTED;
+        private int mStopKeepaliveError = SocketKeepalive.NO_KEEPALIVE;
         private Integer mExpectedKeepaliveSlot = null;
         // Contains the redirectUrl from networkStatus(). Before reading, wait for
         // mNetworkStatusReceived.
@@ -1002,6 +1009,11 @@
         }
 
         @Override
+        protected ProxyTracker makeProxyTracker() {
+            return mock(ProxyTracker.class);
+        }
+
+        @Override
         protected int reserveNetId() {
             while (true) {
                 final int netId = super.reserveNetId();
@@ -1023,6 +1035,11 @@
             }
         }
 
+        @Override
+        protected boolean queryUserAccess(int uid, int netId) {
+            return true;
+        }
+
         public Nat464Xlat getNat464Xlat(MockNetworkAgent mna) {
             return getNetworkAgentInfoForNetwork(mna.getNetwork()).clatd;
         }
@@ -1508,6 +1525,12 @@
         verifyActiveNetwork(TRANSPORT_WIFI);
     }
 
+    @Test
+    public void testRequiresValidation() {
+        assertTrue(NetworkMonitorUtils.isValidationRequired(
+                mCm.getDefaultRequest().networkCapabilities));
+    }
+
     enum CallbackState {
         NONE,
         AVAILABLE,
@@ -3542,6 +3565,80 @@
         }
     }
 
+    private static class TestSocketKeepaliveCallback extends SocketKeepalive.Callback {
+
+        public enum CallbackType { ON_STARTED, ON_STOPPED, ON_ERROR };
+
+        private class CallbackValue {
+            public CallbackType callbackType;
+            public int error;
+
+            CallbackValue(CallbackType type) {
+                this.callbackType = type;
+                this.error = SocketKeepalive.SUCCESS;
+                assertTrue("onError callback must have error", type != CallbackType.ON_ERROR);
+            }
+
+            CallbackValue(CallbackType type, int error) {
+                this.callbackType = type;
+                this.error = error;
+                assertEquals("error can only be set for onError", type, CallbackType.ON_ERROR);
+            }
+
+            @Override
+            public boolean equals(Object o) {
+                return o instanceof CallbackValue
+                        && this.callbackType == ((CallbackValue) o).callbackType
+                        && this.error == ((CallbackValue) o).error;
+            }
+
+            @Override
+            public String toString() {
+                return String.format("%s(%s, %d)", getClass().getSimpleName(), callbackType,
+                        error);
+            }
+        }
+
+        private LinkedBlockingQueue<CallbackValue> mCallbacks = new LinkedBlockingQueue<>();
+
+        @Override
+        public void onStarted() {
+            mCallbacks.add(new CallbackValue(CallbackType.ON_STARTED));
+        }
+
+        @Override
+        public void onStopped() {
+            mCallbacks.add(new CallbackValue(CallbackType.ON_STOPPED));
+        }
+
+        @Override
+        public void onError(int error) {
+            mCallbacks.add(new CallbackValue(CallbackType.ON_ERROR, error));
+        }
+
+        private void expectCallback(CallbackValue callbackValue) {
+            try {
+                assertEquals(
+                        callbackValue,
+                        mCallbacks.poll(TIMEOUT_MS, TimeUnit.MILLISECONDS));
+            } catch (InterruptedException e) {
+                fail(callbackValue.callbackType + " callback not seen after " + TIMEOUT_MS + " ms");
+            }
+        }
+
+        public void expectStarted() {
+            expectCallback(new CallbackValue(CallbackType.ON_STARTED));
+        }
+
+        public void expectStopped() {
+            expectCallback(new CallbackValue(CallbackType.ON_STOPPED));
+        }
+
+        public void expectError(int error) {
+            expectCallback(new CallbackValue(CallbackType.ON_ERROR, error));
+        }
+    }
+
     private Network connectKeepaliveNetwork(LinkProperties lp) {
         // Ensure the network is disconnected before we do anything.
         if (mWiFiNetworkAgent != null) {
@@ -3689,6 +3786,145 @@
     }
 
     @Test
+    public void testNattSocketKeepalives() throws Exception {
+        // TODO: 1. Move this outside of ConnectivityServiceTest.
+        //       2. Add helper function to test against newSingleThreadExecutor as well as inline
+        //          executor.
+        //       3. Make test to verify that Nat-T keepalive socket is created by IpSecService.
+        final int srcPort = 12345;
+        final InetAddress myIPv4 = InetAddress.getByName("192.0.2.129");
+        final InetAddress notMyIPv4 = InetAddress.getByName("192.0.2.35");
+        final InetAddress myIPv6 = InetAddress.getByName("2001:db8::1");
+        final InetAddress dstIPv4 = InetAddress.getByName("8.8.8.8");
+        final InetAddress dstIPv6 = InetAddress.getByName("2001:4860:4860::8888");
+
+        final int validKaInterval = 15;
+        final int invalidKaInterval = 9;
+
+        final IpSecManager mIpSec = (IpSecManager) mContext.getSystemService(Context.IPSEC_SERVICE);
+        final UdpEncapsulationSocket testSocket = mIpSec.openUdpEncapsulationSocket(srcPort);
+
+        final Executor executor = Executors.newSingleThreadExecutor();
+
+        LinkProperties lp = new LinkProperties();
+        lp.setInterfaceName("wlan12");
+        lp.addLinkAddress(new LinkAddress(myIPv6, 64));
+        lp.addLinkAddress(new LinkAddress(myIPv4, 25));
+        lp.addRoute(new RouteInfo(InetAddress.getByName("fe80::1234")));
+        lp.addRoute(new RouteInfo(InetAddress.getByName("192.0.2.254")));
+
+        Network notMyNet = new Network(61234);
+        Network myNet = connectKeepaliveNetwork(lp);
+
+        TestSocketKeepaliveCallback callback = new TestSocketKeepaliveCallback();
+        SocketKeepalive ka;
+
+        // Attempt to start keepalives with invalid parameters and check for errors.
+        // Invalid network.
+        ka = mCm.createSocketKeepalive(notMyNet, testSocket, myIPv4, dstIPv4, executor, callback);
+        ka.start(validKaInterval);
+        callback.expectError(SocketKeepalive.ERROR_INVALID_NETWORK);
+
+        // Invalid interval.
+        ka = mCm.createSocketKeepalive(myNet, testSocket, myIPv4, dstIPv4, executor, callback);
+        ka.start(invalidKaInterval);
+        callback.expectError(SocketKeepalive.ERROR_INVALID_INTERVAL);
+
+        // Invalid destination.
+        ka = mCm.createSocketKeepalive(myNet, testSocket, myIPv4, dstIPv6, executor, callback);
+        ka.start(validKaInterval);
+        callback.expectError(SocketKeepalive.ERROR_INVALID_IP_ADDRESS);
+
+        // Invalid source;
+        ka = mCm.createSocketKeepalive(myNet, testSocket, myIPv6, dstIPv4, executor, callback);
+        ka.start(validKaInterval);
+        callback.expectError(SocketKeepalive.ERROR_INVALID_IP_ADDRESS);
+
+        // NAT-T is only supported for IPv4.
+        ka = mCm.createSocketKeepalive(myNet, testSocket, myIPv6, dstIPv6, executor, callback);
+        ka.start(validKaInterval);
+        callback.expectError(SocketKeepalive.ERROR_INVALID_IP_ADDRESS);
+
+        // Sanity check before testing started keepalive.
+        ka = mCm.createSocketKeepalive(myNet, testSocket, myIPv4, dstIPv4, executor, callback);
+        ka.start(validKaInterval);
+        callback.expectError(SocketKeepalive.ERROR_HARDWARE_UNSUPPORTED);
+
+        // Check that a started keepalive can be stopped.
+        mWiFiNetworkAgent.setStartKeepaliveError(SocketKeepalive.SUCCESS);
+        ka = mCm.createSocketKeepalive(myNet, testSocket, myIPv4, dstIPv4, executor, callback);
+        ka.start(validKaInterval);
+        callback.expectStarted();
+        mWiFiNetworkAgent.setStopKeepaliveError(SocketKeepalive.SUCCESS);
+        ka.stop();
+        callback.expectStopped();
+
+        // Check that deleting the IP address stops the keepalive.
+        LinkProperties bogusLp = new LinkProperties(lp);
+        ka = mCm.createSocketKeepalive(myNet, testSocket, myIPv4, dstIPv4, executor, callback);
+        ka.start(validKaInterval);
+        callback.expectStarted();
+        bogusLp.removeLinkAddress(new LinkAddress(myIPv4, 25));
+        bogusLp.addLinkAddress(new LinkAddress(notMyIPv4, 25));
+        mWiFiNetworkAgent.sendLinkProperties(bogusLp);
+        callback.expectError(SocketKeepalive.ERROR_INVALID_IP_ADDRESS);
+        mWiFiNetworkAgent.sendLinkProperties(lp);
+
+        // Check that a started keepalive is stopped correctly when the network disconnects.
+        ka = mCm.createSocketKeepalive(myNet, testSocket, myIPv4, dstIPv4, executor, callback);
+        ka.start(validKaInterval);
+        callback.expectStarted();
+        mWiFiNetworkAgent.disconnect();
+        waitFor(mWiFiNetworkAgent.getDisconnectedCV());
+        callback.expectError(SocketKeepalive.ERROR_INVALID_NETWORK);
+
+        // ... and that stopping it after that has no adverse effects.
+        waitForIdle();
+        final Network myNetAlias = myNet;
+        assertNull(mCm.getNetworkCapabilities(myNetAlias));
+        ka.stop();
+
+        // Reconnect.
+        myNet = connectKeepaliveNetwork(lp);
+        mWiFiNetworkAgent.setStartKeepaliveError(SocketKeepalive.SUCCESS);
+
+        // Check things work as expected when the keepalive is stopped and the network disconnects.
+        ka = mCm.createSocketKeepalive(myNet, testSocket, myIPv4, dstIPv4, executor, callback);
+        ka.start(validKaInterval);
+        callback.expectStarted();
+        ka.stop();
+        mWiFiNetworkAgent.disconnect();
+        waitFor(mWiFiNetworkAgent.getDisconnectedCV());
+        waitForIdle();
+        callback.expectStopped();
+
+        // Reconnect.
+        myNet = connectKeepaliveNetwork(lp);
+        mWiFiNetworkAgent.setStartKeepaliveError(SocketKeepalive.SUCCESS);
+
+        // Check that keepalive slots start from 1 and increment. The first one gets slot 1.
+        mWiFiNetworkAgent.setExpectedKeepaliveSlot(1);
+        ka = mCm.createSocketKeepalive(myNet, testSocket, myIPv4, dstIPv4, executor, callback);
+        ka.start(validKaInterval);
+        callback.expectStarted();
+
+        // The second one gets slot 2.
+        mWiFiNetworkAgent.setExpectedKeepaliveSlot(2);
+        final UdpEncapsulationSocket testSocket2 = mIpSec.openUdpEncapsulationSocket(6789);
+        TestSocketKeepaliveCallback callback2 = new TestSocketKeepaliveCallback();
+        SocketKeepalive ka2 =
+                mCm.createSocketKeepalive(myNet, testSocket2, myIPv4, dstIPv4, executor, callback2);
+        ka2.start(validKaInterval);
+        callback2.expectStarted();
+
+        ka.stop();
+        callback.expectStopped();
+
+        ka2.stop();
+        callback2.expectStopped();
+    }
+
+    @Test
     public void testGetCaptivePortalServerUrl() throws Exception {
         String url = mCm.getCaptivePortalServerUrl();
         assertEquals("http://connectivitycheck.gstatic.com/generate_204", url);
@@ -4404,8 +4640,7 @@
         mMockVpn.setUids(ranges);
         // VPN networks do not satisfy the default request and are automatically validated
         // by NetworkMonitor
-        assertFalse(NetworkMonitorUtils.isValidationRequired(
-                mCm.getDefaultRequest().networkCapabilities, vpnNetworkAgent.mNetworkCapabilities));
+        assertFalse(NetworkMonitorUtils.isValidationRequired(vpnNetworkAgent.mNetworkCapabilities));
         vpnNetworkAgent.setNetworkValid();
 
         vpnNetworkAgent.connect(false);
@@ -4909,4 +5144,84 @@
         mCellNetworkAgent.sendLinkProperties(lp);
         verifyTcpBufferSizeChange(TEST_TCP_BUFFER_SIZES);
     }
+
+    @Test
+    public void testGetGlobalProxyForNetwork() {
+        final ProxyInfo testProxyInfo = ProxyInfo.buildDirectProxy("test", 8888);
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        final Network wifiNetwork = mWiFiNetworkAgent.getNetwork();
+        when(mService.mProxyTracker.getGlobalProxy()).thenReturn(testProxyInfo);
+        assertEquals(testProxyInfo, mService.getProxyForNetwork(wifiNetwork));
+    }
+
+    @Test
+    public void testGetProxyForActiveNetwork() {
+        final ProxyInfo testProxyInfo = ProxyInfo.buildDirectProxy("test", 8888);
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(true);
+        waitForIdle();
+        assertNull(mService.getProxyForNetwork(null));
+
+        final LinkProperties testLinkProperties = new LinkProperties();
+        testLinkProperties.setHttpProxy(testProxyInfo);
+
+        mWiFiNetworkAgent.sendLinkProperties(testLinkProperties);
+        waitForIdle();
+
+        assertEquals(testProxyInfo, mService.getProxyForNetwork(null));
+    }
+
+    @Test
+    public void testGetProxyForVPN() {
+        final ProxyInfo testProxyInfo = ProxyInfo.buildDirectProxy("test", 8888);
+
+        // Set up a WiFi network with no proxy
+        mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI);
+        mWiFiNetworkAgent.connect(true);
+        waitForIdle();
+        assertNull(mService.getProxyForNetwork(null));
+
+        // Set up a VPN network with a proxy
+        final int uid = Process.myUid();
+        final MockNetworkAgent vpnNetworkAgent = new MockNetworkAgent(TRANSPORT_VPN);
+        final ArraySet<UidRange> ranges = new ArraySet<>();
+        ranges.add(new UidRange(uid, uid));
+        mMockVpn.setUids(ranges);
+        LinkProperties testLinkProperties = new LinkProperties();
+        testLinkProperties.setHttpProxy(testProxyInfo);
+        vpnNetworkAgent.sendLinkProperties(testLinkProperties);
+        waitForIdle();
+
+        // Connect to VPN with proxy
+        mMockVpn.setNetworkAgent(vpnNetworkAgent);
+        vpnNetworkAgent.connect(true);
+        mMockVpn.connect();
+        waitForIdle();
+
+        // Test that the VPN network returns a proxy, and the WiFi does not.
+        assertEquals(testProxyInfo, mService.getProxyForNetwork(vpnNetworkAgent.getNetwork()));
+        assertEquals(testProxyInfo, mService.getProxyForNetwork(null));
+        assertNull(mService.getProxyForNetwork(mWiFiNetworkAgent.getNetwork()));
+
+        // Test that the VPN network returns no proxy when it is set to null.
+        testLinkProperties.setHttpProxy(null);
+        vpnNetworkAgent.sendLinkProperties(testLinkProperties);
+        waitForIdle();
+        assertNull(mService.getProxyForNetwork(vpnNetworkAgent.getNetwork()));
+        assertNull(mService.getProxyForNetwork(null));
+
+        // Set WiFi proxy and check that the vpn proxy is still null.
+        testLinkProperties.setHttpProxy(testProxyInfo);
+        mWiFiNetworkAgent.sendLinkProperties(testLinkProperties);
+        waitForIdle();
+        assertNull(mService.getProxyForNetwork(null));
+
+        // Disconnect from VPN and check that the active network, which is now the WiFi, has the
+        // correct proxy setting.
+        vpnNetworkAgent.disconnect();
+        waitForIdle();
+        assertEquals(mWiFiNetworkAgent.getNetwork(), mCm.getActiveNetwork());
+        assertEquals(testProxyInfo, mService.getProxyForNetwork(mWiFiNetworkAgent.getNetwork()));
+        assertEquals(testProxyInfo, mService.getProxyForNetwork(null));
+    }
 }
diff --git a/tests/net/java/com/android/server/connectivity/NetworkNotificationManagerTest.java b/tests/net/java/com/android/server/connectivity/NetworkNotificationManagerTest.java
index 125fe72..273b8fc 100644
--- a/tests/net/java/com/android/server/connectivity/NetworkNotificationManagerTest.java
+++ b/tests/net/java/com/android/server/connectivity/NetworkNotificationManagerTest.java
@@ -17,6 +17,7 @@
 package com.android.server.connectivity;
 
 import static com.android.server.connectivity.NetworkNotificationManager.NotificationType.*;
+
 import static org.mockito.Mockito.any;
 import static org.mockito.Mockito.anyInt;
 import static org.mockito.Mockito.eq;
@@ -34,26 +35,24 @@
 import android.content.res.Resources;
 import android.net.NetworkCapabilities;
 import android.net.NetworkInfo;
-import android.support.test.runner.AndroidJUnit4;
 import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
 import android.telephony.TelephonyManager;
 
 import com.android.server.connectivity.NetworkNotificationManager.NotificationType;
 
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
 
-import org.junit.runner.RunWith;
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
-import org.mockito.Mockito;
-import org.mockito.MockitoAnnotations;
-
-
 @RunWith(AndroidJUnit4.class)
 @SmallTest
 public class NetworkNotificationManagerTest {
@@ -194,4 +193,54 @@
         mManager.clearNotification(id);
         verify(mNotificationManager, times(1)).cancelAsUser(eq(tag), eq(SIGN_IN.eventId), any());
     }
+
+    @Test
+    public void testSameLevelNotifications() {
+        final int id = 101;
+        final String tag = NetworkNotificationManager.tagFor(id);
+
+        mManager.showNotification(id, LOGGED_IN, mWifiNai, mCellNai, null, false);
+        verify(mNotificationManager, times(1))
+                .notifyAsUser(eq(tag), eq(LOGGED_IN.eventId), any(), any());
+
+        mManager.showNotification(id, LOST_INTERNET, mWifiNai, mCellNai, null, false);
+        verify(mNotificationManager, times(1))
+                .notifyAsUser(eq(tag), eq(LOST_INTERNET.eventId), any(), any());
+    }
+
+    @Test
+    public void testClearNotificationByType() {
+        final int id = 101;
+        final String tag = NetworkNotificationManager.tagFor(id);
+
+        // clearNotification(int id, NotificationType notifyType) will check if given type is equal
+        // to previous type or not. If they are equal then clear the notification; if they are not
+        // equal then return.
+
+        mManager.showNotification(id, LOGGED_IN, mWifiNai, mCellNai, null, false);
+        verify(mNotificationManager, times(1))
+                .notifyAsUser(eq(tag), eq(LOGGED_IN.eventId), any(), any());
+
+        // Previous notification is LOGGED_IN and given type is LOGGED_IN too. The notification
+        // should be cleared.
+        mManager.clearNotification(id, LOGGED_IN);
+        verify(mNotificationManager, times(1))
+                .cancelAsUser(eq(tag), eq(LOGGED_IN.eventId), any());
+
+        mManager.showNotification(id, LOGGED_IN, mWifiNai, mCellNai, null, false);
+        verify(mNotificationManager, times(2))
+                .notifyAsUser(eq(tag), eq(LOGGED_IN.eventId), any(), any());
+
+        // LOST_INTERNET notification popup after LOGGED_IN notification.
+        mManager.showNotification(id, LOST_INTERNET, mWifiNai, mCellNai, null, false);
+        verify(mNotificationManager, times(1))
+                .notifyAsUser(eq(tag), eq(LOST_INTERNET.eventId), any(), any());
+
+        // Previous notification is LOST_INTERNET and given type is LOGGED_IN. The notification
+        // shouldn't be cleared.
+        mManager.clearNotification(id, LOGGED_IN);
+        // LOST_INTERNET shouldn't be cleared.
+        verify(mNotificationManager, never())
+                .cancelAsUser(eq(tag), eq(LOST_INTERNET.eventId), any());
+    }
 }
diff --git a/tests/net/java/com/android/server/net/ipmemorystore/NetworkAttributesTest.java b/tests/net/java/com/android/server/net/ipmemorystore/NetworkAttributesTest.java
new file mode 100644
index 0000000..fe19eee
--- /dev/null
+++ b/tests/net/java/com/android/server/net/ipmemorystore/NetworkAttributesTest.java
@@ -0,0 +1,65 @@
+/*
+ * 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 com.android.server.net.ipmemorystore;
+
+import static org.junit.Assert.assertEquals;
+
+import android.net.ipmemorystore.NetworkAttributes;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.lang.reflect.Field;
+import java.net.Inet4Address;
+import java.net.UnknownHostException;
+import java.util.Arrays;
+
+/** Unit tests for {@link NetworkAttributes}. */
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class NetworkAttributesTest {
+    private static final String WEIGHT_FIELD_NAME_PREFIX = "WEIGHT_";
+    private static final float EPSILON = 0.0001f;
+
+    // This is running two tests to make sure the total weight is the sum of all weights. To be
+    // sure this is not fireproof, but you'd kind of need to do it on purpose to pass.
+    @Test
+    public void testTotalWeight() throws IllegalAccessException, UnknownHostException {
+        // Make sure that TOTAL_WEIGHT is equal to the sum of the fields starting with WEIGHT_
+        float sum = 0f;
+        final Field[] fieldList = NetworkAttributes.class.getDeclaredFields();
+        for (final Field field : fieldList) {
+            if (!field.getName().startsWith(WEIGHT_FIELD_NAME_PREFIX)) continue;
+            field.setAccessible(true);
+            sum += (float) field.get(null);
+        }
+        assertEquals(sum, NetworkAttributes.TOTAL_WEIGHT, EPSILON);
+
+        // Use directly the constructor with all attributes, and make sure that when compared
+        // to itself the score is a clean 1.0f.
+        final NetworkAttributes na =
+                new NetworkAttributes(
+                        (Inet4Address) Inet4Address.getByAddress(new byte[] {1, 2, 3, 4}),
+                        "some hint",
+                        Arrays.asList(Inet4Address.getByAddress(new byte[] {5, 6, 7, 8}),
+                                Inet4Address.getByAddress(new byte[] {9, 0, 1, 2})),
+                        98);
+        assertEquals(1.0f, na.getNetworkGroupSamenessConfidence(na), EPSILON);
+    }
+}