ConnectivityManager API for for packet keepalives.

Bug: 21405946
Change-Id: Ie1f8f8bee684fe2bb1092a9f1bc9f5dc29b1defc
diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java
index dc8ff8f..86a50c6 100644
--- a/core/java/android/net/ConnectivityManager.java
+++ b/core/java/android/net/ConnectivityManager.java
@@ -1206,6 +1206,142 @@
         return true;
     }
 
+    /** @hide */
+    public static class PacketKeepaliveCallback {
+        /** The requested keepalive was successfully started. */
+        public void onStarted() {}
+        /** The keepalive was successfully stopped. */
+        public void onStopped() {}
+        /** An error occurred. */
+        public void onError(int error) {}
+    }
+
+    /**
+     * Allows applications to request that the system periodically send specific packets on their
+     * behalf, using hardware offload to save battery power.
+     *
+     * To request that the system send keepalives, call one of the methods that return a
+     * {@link ConnectivityManager.PacketKeepalive} object, such as {@link #startNattKeepalive},
+     * passing in a non-null callback. If the callback is successfully started, the callback's
+     * {@code onStarted} method will be called. If an error occurs, {@code onError} will be called,
+     * specifying one of the {@code ERROR_*} constants in this class.
+     *
+     * To stop an existing keepalive, call {@link stop}. The system will call {@code onStopped} if
+     * the operation was successfull or {@code onError} if an error occurred.
+     *
+     * @hide
+     */
+    public class PacketKeepalive {
+
+        private static final String TAG = "PacketKeepalive";
+
+        /** @hide */
+        public static final int SUCCESS = 0;
+
+        /** @hide */
+        public static final int NO_KEEPALIVE = -1;
+
+        /** @hide */
+        public static final int BINDER_DIED = -10;
+
+        /** The specified {@code Network} is not connected. */
+        public static final int ERROR_INVALID_NETWORK = -20;
+        /** The specified IP addresses are invalid. For example, the specified source IP address is
+          * not configured on the specified {@code Network}. */
+        public static final int ERROR_INVALID_IP_ADDRESS = -21;
+        /** The requested port is invalid. */
+        public static final int ERROR_INVALID_PORT = -22;
+        /** The packet length is invalid (e.g., too long). */
+        public static final int ERROR_INVALID_LENGTH = -23;
+        /** The packet transmission interval is invalid (e.g., too short). */
+        public static final int ERROR_INVALID_INTERVAL = -24;
+
+        /** The hardware does not support this request. */
+        public static final int ERROR_HARDWARE_UNSUPPORTED = -30;
+
+        public static final int NATT_PORT = 4500;
+
+        private final Network mNetwork;
+        private final PacketKeepaliveCallback mCallback;
+        private final Looper mLooper;
+        private final Messenger mMessenger;
+
+        private volatile Integer mSlot;
+
+        void stopLooper() {
+            mLooper.quit();
+        }
+
+        public void stop() {
+            try {
+                mService.stopKeepalive(mNetwork, mSlot);
+            } catch (RemoteException e) {
+                Log.e(TAG, "Error stopping packet keepalive: ", e);
+                stopLooper();
+            }
+        }
+
+        private PacketKeepalive(Network network, PacketKeepaliveCallback callback) {
+            checkNotNull(network, "network cannot be null");
+            checkNotNull(callback, "callback cannot be null");
+            mNetwork = network;
+            mCallback = callback;
+            HandlerThread thread = new HandlerThread(TAG);
+            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:
+                            int error = message.arg2;
+                            try {
+                                if (error == SUCCESS) {
+                                    if (mSlot == null) {
+                                        mSlot = message.arg1;
+                                        mCallback.onStarted();
+                                    } else {
+                                        mSlot = null;
+                                        stopLooper();
+                                        mCallback.onStopped();
+                                    }
+                                } else {
+                                    stopLooper();
+                                    mCallback.onError(error);
+                                }
+                            } catch (Exception e) {
+                                Log.e(TAG, "Exception in keepalive callback(" + error + ")", e);
+                            }
+                            break;
+                        default:
+                            Log.e(TAG, "Unhandled message " + Integer.toHexString(message.what));
+                            break;
+                    }
+                }
+            });
+        }
+    }
+
+    /**
+     * Starts an IPsec NAT-T keepalive packet with the specified parameters.
+     *
+     * @hide
+     */
+    public PacketKeepalive startNattKeepalive(
+            Network network, int intervalSeconds, PacketKeepaliveCallback callback,
+            InetAddress srcAddr, int srcPort, InetAddress dstAddr) {
+        final PacketKeepalive k = new PacketKeepalive(network, callback);
+        try {
+            mService.startNattKeepalive(network, intervalSeconds, k.mMessenger, new Binder(),
+                    srcAddr.getHostAddress(), srcPort, dstAddr.getHostAddress());
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error starting packet keepalive: ", e);
+            k.stopLooper();
+            return null;
+        }
+        return k;
+    }
+
     /**
      * 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
diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl
index 46c28a6..d4dd669 100644
--- a/core/java/android/net/IConnectivityManager.aidl
+++ b/core/java/android/net/IConnectivityManager.aidl
@@ -160,4 +160,9 @@
     boolean setUnderlyingNetworksForVpn(in Network[] networks);
 
     void factoryReset();
+
+    void startNattKeepalive(in Network network, int intervalSeconds, in Messenger messenger,
+            in IBinder binder, String srcAddr, int srcPort, String dstAddr);
+
+    void stopKeepalive(in Network network, int slot);
 }
diff --git a/core/java/android/net/NetworkAgent.java b/core/java/android/net/NetworkAgent.java
index e6fc1ea..85a584a 100644
--- a/core/java/android/net/NetworkAgent.java
+++ b/core/java/android/net/NetworkAgent.java
@@ -25,6 +25,7 @@
 
 import com.android.internal.util.AsyncChannel;
 import com.android.internal.util.Protocol;
+import android.net.ConnectivityManager.PacketKeepalive;
 
 import java.util.ArrayList;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -143,11 +144,46 @@
      */
     public static final int CMD_SAVE_ACCEPT_UNVALIDATED = BASE + 9;
 
-    /** Sent by ConnectivityService to the NetworkAgent to inform the agent to pull
+    /**
+     * Sent by ConnectivityService to the NetworkAgent to inform the agent to pull
      * the underlying network connection for updated bandwidth information.
      */
     public static final int CMD_REQUEST_BANDWIDTH_UPDATE = BASE + 10;
 
+    /**
+     * Sent by ConnectivityService to the NetworkAgent to request that the specified packet be sent
+     * periodically on the given interval.
+     *
+     *   arg1 = the slot number of the keepalive to start
+     *   arg2 = interval in seconds
+     *   obj = KeepalivePacketData object describing the data to be sent
+     *
+     * Also used internally by ConnectivityService / KeepaliveTracker, with different semantics.
+     */
+    public static final int CMD_START_PACKET_KEEPALIVE = BASE + 11;
+
+    /**
+     * Requests that the specified keepalive packet be stopped.
+     *
+     * arg1 = slot number of the keepalive to stop.
+     *
+     * Also used internally by ConnectivityService / KeepaliveTracker, with different semantics.
+     */
+    public static final int CMD_STOP_PACKET_KEEPALIVE = BASE + 12;
+
+    /**
+     * Sent by the NetworkAgent to ConnectivityService to provide status on a packet keepalive
+     * request. This may either be the reply to a CMD_START_PACKET_KEEPALIVE, or an asynchronous
+     * error notification.
+     *
+     * This is also sent by KeepaliveTracker to the app's ConnectivityManager.PacketKeepalive to
+     * so that the app's PacketKeepaliveCallback methods can be called.
+     *
+     * arg1 = slot number of the keepalive
+     * arg2 = error code
+     */
+    public static final int EVENT_PACKET_KEEPALIVE = BASE + 13;
+
     public NetworkAgent(Looper looper, Context context, String logTag, NetworkInfo ni,
             NetworkCapabilities nc, LinkProperties lp, int score) {
         this(looper, context, logTag, ni, nc, lp, score, null);
@@ -240,18 +276,41 @@
             }
             case CMD_SAVE_ACCEPT_UNVALIDATED: {
                 saveAcceptUnvalidated(msg.arg1 != 0);
+                break;
+            }
+            case CMD_START_PACKET_KEEPALIVE: {
+                startPacketKeepalive(msg);
+                break;
+            }
+            case CMD_STOP_PACKET_KEEPALIVE: {
+                stopPacketKeepalive(msg);
+                break;
             }
         }
     }
 
     private void queueOrSendMessage(int what, Object obj) {
+        queueOrSendMessage(what, 0, 0, obj);
+    }
+
+    private void queueOrSendMessage(int what, int arg1, int arg2) {
+        queueOrSendMessage(what, arg1, arg2, null);
+    }
+
+    private void queueOrSendMessage(int what, int arg1, int arg2, Object obj) {
+        Message msg = Message.obtain();
+        msg.what = what;
+        msg.arg1 = arg1;
+        msg.arg2 = arg2;
+        msg.obj = obj;
+        queueOrSendMessage(msg);
+    }
+
+    private void queueOrSendMessage(Message msg) {
         synchronized (mPreConnectedQueue) {
             if (mAsyncChannel != null) {
-                mAsyncChannel.sendMessage(what, obj);
+                mAsyncChannel.sendMessage(msg);
             } else {
-                Message msg = Message.obtain();
-                msg.what = what;
-                msg.obj = obj;
                 mPreConnectedQueue.add(msg);
             }
         }
@@ -365,6 +424,27 @@
     protected void saveAcceptUnvalidated(boolean accept) {
     }
 
+    /**
+     * Requests that the network hardware send the specified packet at the specified interval.
+     */
+    protected void startPacketKeepalive(Message msg) {
+        onPacketKeepaliveEvent(msg.arg1, PacketKeepalive.ERROR_HARDWARE_UNSUPPORTED);
+    }
+
+    /**
+     * Requests that the network hardware send the specified packet at the specified interval.
+     */
+    protected void stopPacketKeepalive(Message msg) {
+        onPacketKeepaliveEvent(msg.arg1, PacketKeepalive.ERROR_HARDWARE_UNSUPPORTED);
+    }
+
+    /**
+     * Called by the network when a packet keepalive event occurs.
+     */
+    public void onPacketKeepaliveEvent(int slot, int reason) {
+        queueOrSendMessage(EVENT_PACKET_KEEPALIVE, slot, reason);
+    }
+
     protected void log(String s) {
         Log.d(LOG_TAG, "NetworkAgent: " + s);
     }
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index 9c767a5..08754c2 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -47,6 +47,7 @@
 import android.content.res.Resources;
 import android.database.ContentObserver;
 import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.PacketKeepalive;
 import android.net.IConnectivityManager;
 import android.net.INetworkManagementEventObserver;
 import android.net.INetworkPolicyListener;
@@ -117,6 +118,7 @@
 import com.android.internal.util.XmlUtils;
 import com.android.server.am.BatteryStatsService;
 import com.android.server.connectivity.DataConnectionStats;
+import com.android.server.connectivity.KeepaliveTracker;
 import com.android.server.connectivity.NetworkDiagnostics;
 import com.android.server.connectivity.Nat464Xlat;
 import com.android.server.connectivity.NetworkAgentInfo;
@@ -399,6 +401,8 @@
 
     TelephonyManager mTelephonyManager;
 
+    private KeepaliveTracker mKeepaliveTracker;
+
     // sequence number for Networks; keep in sync with system/netd/NetworkController.cpp
     private final static int MIN_NET_ID = 100; // some reserved marks
     private final static int MAX_NET_ID = 65535;
@@ -764,6 +768,8 @@
         mPacManager = new PacManager(mContext, mHandler, EVENT_PROXY_HAS_CHANGED);
 
         mUserManager = (UserManager) context.getSystemService(Context.USER_SERVICE);
+
+        mKeepaliveTracker = new KeepaliveTracker(mHandler);
     }
 
     private NetworkRequest createInternetRequestForTransport(int transportType) {
@@ -1449,6 +1455,10 @@
                 "ConnectivityService");
     }
 
+    private void enforceKeepalivePermission() {
+        mContext.enforceCallingPermission(KeepaliveTracker.PERMISSION, "ConnectivityService");
+    }
+
     public void sendConnectedBroadcast(NetworkInfo info) {
         enforceConnectivityInternalPermission();
         sendGeneralBroadcast(info, CONNECTIVITY_ACTION);
@@ -1840,10 +1850,13 @@
                 pw.println(", last requested never");
             }
         }
-        pw.println();
 
+        pw.println();
         mTethering.dump(fd, pw, args);
 
+        pw.println();
+        mKeepaliveTracker.dump(pw);
+
         if (mInetLog != null && mInetLog.size() > 0) {
             pw.println();
             pw.println("Inet condition reports:");
@@ -2010,6 +2023,15 @@
                     nai.networkMisc.acceptUnvalidated = (boolean) msg.obj;
                     break;
                 }
+                case NetworkAgent.EVENT_PACKET_KEEPALIVE: {
+                    NetworkAgentInfo nai = mNetworkAgentInfos.get(msg.replyTo);
+                    if (nai == null) {
+                        loge("EVENT_PACKET_KEEPALIVE from unknown NetworkAgent");
+                        break;
+                    }
+                    mKeepaliveTracker.handleEventPacketKeepalive(nai, msg);
+                    break;
+                }
                 case NetworkMonitor.EVENT_NETWORK_TESTED: {
                     NetworkAgentInfo nai = (NetworkAgentInfo)msg.obj;
                     if (isLiveNetworkAgent(nai, "EVENT_NETWORK_TESTED")) {
@@ -2148,6 +2170,8 @@
             }
             notifyIfacesChanged();
             notifyNetworkCallbacks(nai, ConnectivityManager.CALLBACK_LOST);
+            mKeepaliveTracker.handleStopAllKeepalives(nai,
+                    ConnectivityManager.PacketKeepalive.ERROR_INVALID_NETWORK);
             nai.networkMonitor.sendMessage(NetworkMonitor.CMD_NETWORK_DISCONNECTED);
             mNetworkAgentInfos.remove(msg.replyTo);
             updateClat(null, nai.linkProperties, nai);
@@ -2509,6 +2533,19 @@
                     handleMobileDataAlwaysOn();
                     break;
                 }
+                // Sent by KeepaliveTracker to process an app request on the state machine thread.
+                case NetworkAgent.CMD_START_PACKET_KEEPALIVE: {
+                    mKeepaliveTracker.handleStartKeepalive(msg);
+                    break;
+                }
+                // Sent by KeepaliveTracker to process an app request on the state machine thread.
+                case NetworkAgent.CMD_STOP_PACKET_KEEPALIVE: {
+                    NetworkAgentInfo nai = getNetworkAgentInfoForNetwork((Network) msg.obj);
+                    int slot = msg.arg1;
+                    int reason = msg.arg2;
+                    mKeepaliveTracker.handleStopKeepalive(nai, slot, reason);
+                    break;
+                }
                 case EVENT_SYSTEM_READY: {
                     for (NetworkAgentInfo nai : mNetworkAgentInfos.values()) {
                         nai.networkMonitor.systemReady = true;
@@ -3863,6 +3900,8 @@
             notifyIfacesChanged();
             notifyNetworkCallbacks(networkAgent, ConnectivityManager.CALLBACK_IP_CHANGED);
         }
+
+        mKeepaliveTracker.handleCheckKeepalivesStillValid(networkAgent);
     }
 
     private void updateClat(LinkProperties newLp, LinkProperties oldLp, NetworkAgentInfo nai) {
@@ -4663,6 +4702,22 @@
     }
 
     @Override
+    public void startNattKeepalive(Network network, int intervalSeconds, Messenger messenger,
+            IBinder binder, String srcAddr, int srcPort, String dstAddr) {
+        enforceKeepalivePermission();
+        mKeepaliveTracker.startNattKeepalive(
+                getNetworkAgentInfoForNetwork(network),
+                intervalSeconds, messenger, binder,
+                srcAddr, srcPort, dstAddr, ConnectivityManager.PacketKeepalive.NATT_PORT);
+    }
+
+    @Override
+    public void stopKeepalive(Network network, int slot) {
+        mHandler.sendMessage(mHandler.obtainMessage(
+                NetworkAgent.CMD_STOP_PACKET_KEEPALIVE, slot, PacketKeepalive.SUCCESS, network));
+    }
+
+    @Override
     public void factoryReset() {
         enforceConnectivityInternalPermission();
 
diff --git a/services/core/java/com/android/server/connectivity/KeepalivePacketData.java b/services/core/java/com/android/server/connectivity/KeepalivePacketData.java
new file mode 100644
index 0000000..64b9399
--- /dev/null
+++ b/services/core/java/com/android/server/connectivity/KeepalivePacketData.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity;
+
+import android.system.OsConstants;
+import android.net.ConnectivityManager;
+import android.net.NetworkUtils;
+import android.net.util.IpUtils;
+
+import java.net.Inet4Address;
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+
+import static android.net.ConnectivityManager.PacketKeepalive.*;
+
+/**
+ * Represents the actual packets that are sent by the
+ * {@link android.net.ConnectivityManager.PacketKeepalive} API.
+ *
+ * @hide
+ */
+public class KeepalivePacketData {
+    /** Protocol of the packet to send; one of the OsConstants.ETH_P_* values. */
+    public final int protocol;
+
+    /** Source IP address */
+    public final InetAddress srcAddress;
+
+    /** Destination IP address */
+    public final InetAddress dstAddress;
+
+    /** Source port */
+    public final int srcPort;
+
+    /** Destination port */
+    public final int dstPort;
+
+    /** Destination MAC address. Can change if routing changes. */
+    public byte[] dstMac;
+
+    /** Packet data. A raw byte string of packet data, not including the link-layer header. */
+    public final byte[] data;
+
+    private static final int IPV4_HEADER_LENGTH = 20;
+    private static final int UDP_HEADER_LENGTH = 8;
+
+    protected KeepalivePacketData(InetAddress srcAddress, int srcPort,
+            InetAddress dstAddress, int dstPort, byte[] data) throws InvalidPacketException {
+        this.srcAddress = srcAddress;
+        this.dstAddress = dstAddress;
+        this.srcPort = srcPort;
+        this.dstPort = dstPort;
+        this.data = data;
+
+        // Check we have two IP addresses of the same family.
+        if (srcAddress == null || dstAddress == null ||
+                !srcAddress.getClass().getName().equals(dstAddress.getClass().getName())) {
+        }
+
+        // Set the protocol.
+        if (this.dstAddress instanceof Inet4Address) {
+            this.protocol = OsConstants.ETH_P_IP;
+        } else if (this.dstAddress instanceof Inet6Address) {
+            this.protocol = OsConstants.ETH_P_IPV6;
+        } else {
+            throw new InvalidPacketException(ERROR_INVALID_IP_ADDRESS);
+        }
+
+        // Check the ports.
+        if (!IpUtils.isValidUdpOrTcpPort(srcPort) || !IpUtils.isValidUdpOrTcpPort(dstPort)) {
+            throw new InvalidPacketException(ERROR_INVALID_PORT);
+        }
+    }
+
+    public static class InvalidPacketException extends Exception {
+        final public int error;
+        public InvalidPacketException(int error) {
+            this.error = error;
+        }
+    }
+
+    /**
+     * Creates an IPsec NAT-T keepalive packet with the specified parameters.
+     */
+    public static KeepalivePacketData nattKeepalivePacket(
+            InetAddress srcAddress, int srcPort,
+            InetAddress dstAddress, int dstPort) throws InvalidPacketException {
+
+        if (!(srcAddress instanceof Inet4Address)) {
+            throw new InvalidPacketException(ERROR_INVALID_IP_ADDRESS);
+        }
+
+        if (dstPort != NATT_PORT) {
+            throw new InvalidPacketException(ERROR_INVALID_PORT);
+        }
+
+        int length = IPV4_HEADER_LENGTH + UDP_HEADER_LENGTH + 1;
+        ByteBuffer buf = ByteBuffer.allocate(length);
+        buf.order(ByteOrder.BIG_ENDIAN);
+        buf.putShort((short) 0x4500);             // IP version and TOS
+        buf.putShort((short) length);
+        buf.putInt(0);                            // ID, flags, offset
+        buf.put((byte) 64);                       // TTL
+        buf.put((byte) OsConstants.IPPROTO_UDP);
+        int ipChecksumOffset = buf.position();
+        buf.putShort((short) 0);                  // IP checksum
+        buf.put(srcAddress.getAddress());
+        buf.put(dstAddress.getAddress());
+        buf.putShort((short) srcPort);
+        buf.putShort((short) dstPort);
+        buf.putShort((short) (length - 20));      // UDP length
+        int udpChecksumOffset = buf.position();
+        buf.putShort((short) 0);                  // UDP checksum
+        buf.put((byte) 0xff);                     // NAT-T keepalive
+        buf.putShort(ipChecksumOffset, IpUtils.ipChecksum(buf, 0));
+        buf.putShort(udpChecksumOffset, IpUtils.udpChecksum(buf, 0, IPV4_HEADER_LENGTH));
+
+        return new KeepalivePacketData(srcAddress, srcPort, dstAddress, dstPort, buf.array());
+    }
+}
diff --git a/services/core/java/com/android/server/connectivity/KeepaliveTracker.java b/services/core/java/com/android/server/connectivity/KeepaliveTracker.java
new file mode 100644
index 0000000..c78f347
--- /dev/null
+++ b/services/core/java/com/android/server/connectivity/KeepaliveTracker.java
@@ -0,0 +1,372 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.connectivity;
+
+import com.android.internal.util.HexDump;
+import com.android.internal.util.IndentingPrintWriter;
+import com.android.server.connectivity.KeepalivePacketData;
+import com.android.server.connectivity.NetworkAgentInfo;
+import android.net.ConnectivityManager;
+import android.net.ConnectivityManager.PacketKeepalive;
+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.Message;
+import android.os.Messenger;
+import android.os.Process;
+import android.os.RemoteException;
+import android.system.OsConstants;
+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 java.net.InetAddress;
+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.
+ *
+ * Provides methods to stop and start keepalive requests, and keeps track of keepalives across all
+ * networks. This class is tightly coupled to ConnectivityService. It is not thread-safe and its
+ * methods must be called only from the ConnectivityService handler thread.
+ */
+public class KeepaliveTracker {
+
+    private static final String TAG = "KeepaliveTracker";
+    private static final boolean DBG = true;
+
+    // TODO: Change this to a system-only permission.
+    public static final String PERMISSION = android.Manifest.permission.CHANGE_NETWORK_STATE;
+
+    /** Keeps track of keepalive requests. */
+    private final HashMap <NetworkAgentInfo, HashMap<Integer, KeepaliveInfo>> mKeepalives =
+            new HashMap<> ();
+    private final Handler mConnectivityServiceHandler;
+
+    public KeepaliveTracker(Handler handler) {
+        mConnectivityServiceHandler = handler;
+    }
+
+    /**
+     * Tracks information about a packet keepalive.
+     *
+     * All information about this keepalive is known at construction time except the slot number,
+     * which is only returned when the hardware has successfully started the keepalive.
+     */
+    class KeepaliveInfo implements IBinder.DeathRecipient {
+        // Bookkeping data.
+        private final Messenger mMessenger;
+        private final IBinder mBinder;
+        private final int mUid;
+        private final int mPid;
+        private final NetworkAgentInfo mNai;
+
+        /** Keepalive slot. A small integer that identifies this keepalive among the ones handled
+          * by this network. */
+        private int mSlot = PacketKeepalive.NO_KEEPALIVE;
+
+        // Packet data.
+        private final KeepalivePacketData mPacket;
+        private final int mInterval;
+
+        // Whether the keepalive is started or not.
+        public boolean isStarted;
+
+        public KeepaliveInfo(Messenger messenger, IBinder binder, NetworkAgentInfo nai,
+                KeepalivePacketData packet, int interval) {
+            mMessenger = messenger;
+            mBinder = binder;
+            mPid = Binder.getCallingPid();
+            mUid = Binder.getCallingUid();
+
+            mNai = nai;
+            mPacket = packet;
+            mInterval = interval;
+
+            try {
+                mBinder.linkToDeath(this, 0);
+            } catch (RemoteException e) {
+                binderDied();
+            }
+        }
+
+        public NetworkAgentInfo getNai() {
+            return mNai;
+        }
+
+        public String toString() {
+            return new StringBuffer("KeepaliveInfo [")
+                    .append(" network=").append(mNai.network)
+                    .append(" isStarted=").append(isStarted)
+                    .append(" ")
+                    .append(IpUtils.addressAndPortToString(mPacket.srcAddress, mPacket.srcPort))
+                    .append("->")
+                    .append(IpUtils.addressAndPortToString(mPacket.dstAddress, mPacket.dstPort))
+                    .append(" interval=" + mInterval)
+                    .append(" data=" + HexDump.toHexString(mPacket.data))
+                    .append(" uid=").append(mUid).append(" pid=").append(mPid)
+                    .append(" ]")
+                    .toString();
+        }
+
+        /** Sends a message back to the application via its PacketKeepalive.Callback. */
+        void notifyMessenger(int slot, int err) {
+            KeepaliveTracker.this.notifyMessenger(mMessenger, slot, err);
+        }
+
+        /** Called when the application process is killed. */
+        public void binderDied() {
+            // Not called from ConnectivityService handler thread, so send it a message.
+            mConnectivityServiceHandler.obtainMessage(
+                    NetworkAgent.CMD_STOP_PACKET_KEEPALIVE,
+                    mSlot, PacketKeepalive.BINDER_DIED, mNai.network).sendToTarget();
+        }
+
+        void unlinkDeathRecipient() {
+            if (mBinder != null) {
+                mBinder.unlinkToDeath(this, 0);
+            }
+        }
+
+        private int checkNetworkConnected() {
+            if (!mNai.networkInfo.isConnectedOrConnecting()) {
+                return ERROR_INVALID_NETWORK;
+            }
+            return SUCCESS;
+        }
+
+        private int checkSourceAddress() {
+            // Check that we have the source address.
+            for (InetAddress address : mNai.linkProperties.getAddresses()) {
+                if (address.equals(mPacket.srcAddress)) {
+                    return SUCCESS;
+                }
+            }
+            return ERROR_INVALID_IP_ADDRESS;
+        }
+
+        private int checkInterval() {
+            return mInterval >= 20 ? SUCCESS : ERROR_INVALID_INTERVAL;
+        }
+
+        private int isValid() {
+            synchronized (mNai) {
+                int error = checkInterval();
+                if (error == SUCCESS) error = checkNetworkConnected();
+                if (error == SUCCESS) error = checkSourceAddress();
+                return error;
+            }
+        }
+
+        void start(int slot) {
+            int error = isValid();
+            if (error == SUCCESS) {
+                mSlot = slot;
+                Log.d(TAG, "Starting keepalive " + mSlot + " on " + mNai.name());
+                mNai.asyncChannel.sendMessage(CMD_START_PACKET_KEEPALIVE, slot, mInterval, mPacket);
+            } else {
+                notifyMessenger(NO_KEEPALIVE, error);
+                return;
+            }
+        }
+
+        void stop(int reason) {
+            int uid = Binder.getCallingUid();
+            if (uid != mUid && uid != Process.SYSTEM_UID) {
+                if (DBG) {
+                    Log.e(TAG, "Cannot stop unowned keepalive " + mSlot + " on " + mNai.network);
+                }
+            }
+            if (isStarted) {
+                Log.d(TAG, "Stopping keepalive " + mSlot + " on " + mNai.name());
+                mNai.asyncChannel.sendMessage(CMD_STOP_PACKET_KEEPALIVE, mSlot);
+            }
+            notifyMessenger(mSlot, reason);
+            unlinkDeathRecipient();
+        }
+    }
+
+    void notifyMessenger(Messenger messenger, int slot, int err) {
+        Message message = Message.obtain();
+        message.what = EVENT_PACKET_KEEPALIVE;
+        message.arg1 = slot;
+        message.arg2 = err;
+        message.obj = null;
+        try {
+            messenger.send(message);
+        } catch (RemoteException e) {
+            // Process died?
+        }
+    }
+
+    private  int findFirstFreeSlot(NetworkAgentInfo nai) {
+        HashMap networkKeepalives = mKeepalives.get(nai);
+        if (networkKeepalives == null) {
+            networkKeepalives = new HashMap<Integer, KeepaliveInfo>();
+            mKeepalives.put(nai, networkKeepalives);
+        }
+
+        // Find the lowest-numbered free slot.
+        int slot;
+        for (slot = 0; slot < networkKeepalives.size(); slot++) {
+            if (networkKeepalives.get(slot) == null) {
+                return slot;
+            }
+        }
+        // No free slot, pick one at the end.
+
+        // HACK for broadcom hardware that does not support slot 0!
+        if (slot == 0) slot = 1;
+        return slot;
+    }
+
+    public void handleStartKeepalive(Message message) {
+        KeepaliveInfo ki = (KeepaliveInfo) message.obj;
+        NetworkAgentInfo nai = ki.getNai();
+        int slot = findFirstFreeSlot(nai);
+        mKeepalives.get(nai).put(slot, ki);
+        ki.start(slot);
+    }
+
+    public void handleStopAllKeepalives(NetworkAgentInfo nai, int reason) {
+        HashMap <Integer, KeepaliveInfo> networkKeepalives = mKeepalives.get(nai);
+        if (networkKeepalives != null) {
+            for (KeepaliveInfo ki : networkKeepalives.values()) {
+                ki.stop(reason);
+            }
+            networkKeepalives.clear();
+            mKeepalives.remove(nai);
+        }
+    }
+
+    public void handleStopKeepalive(NetworkAgentInfo nai, int slot, int reason) {
+        HashMap <Integer, KeepaliveInfo> networkKeepalives = mKeepalives.get(nai);
+        if (networkKeepalives == null) {
+            Log.e(TAG, "Attempt to stop keepalive on nonexistent network " + nai.name());
+            return;
+        }
+        KeepaliveInfo ki = networkKeepalives.get(slot);
+        if (ki == null) {
+            Log.e(TAG, "Attempt to stop nonexistent keepalive " + slot + " on " + nai.name());
+            return;
+        }
+        ki.stop(reason);
+        networkKeepalives.remove(slot);
+        if (networkKeepalives.isEmpty()) {
+            mKeepalives.remove(nai);
+        }
+    }
+
+    public void handleCheckKeepalivesStillValid(NetworkAgentInfo nai) {
+        HashMap <Integer, KeepaliveInfo> networkKeepalives = mKeepalives.get(nai);
+        if (networkKeepalives != null) {
+            ArrayList<Pair<Integer, Integer>> invalidKeepalives = new ArrayList<>();
+            for (int slot : networkKeepalives.keySet()) {
+                int error = networkKeepalives.get(slot).isValid();
+                if (error != SUCCESS) {
+                    invalidKeepalives.add(Pair.create(slot, error));
+                }
+            }
+            for (Pair<Integer, Integer> slotAndError: invalidKeepalives) {
+                handleStopKeepalive(nai, slotAndError.first, slotAndError.second);
+            }
+        }
+    }
+
+    public void handleEventPacketKeepalive(NetworkAgentInfo nai, Message message) {
+        int slot = message.arg1;
+        int reason = message.arg2;
+
+        KeepaliveInfo ki = null;
+        try {
+            ki = mKeepalives.get(nai).get(slot);
+        } catch(NullPointerException e) {}
+        if (ki == null) {
+            Log.e(TAG, "Event for unknown keepalive " + slot + " on " + nai.name());
+            return;
+        }
+
+        if (reason == SUCCESS && !ki.isStarted) {
+            // Keepalive successfully started.
+            if (DBG) Log.d(TAG, "Started keepalive " + slot + " on " + nai.name());
+            ki.isStarted = true;
+            ki.notifyMessenger(slot, reason);
+        } else {
+            // Keepalive successfully stopped, or error.
+            ki.isStarted = false;
+            if (reason == SUCCESS) {
+                if (DBG) Log.d(TAG, "Successfully stopped keepalive " + slot + " on " + nai.name());
+            } else {
+                if (DBG) Log.d(TAG, "Keepalive " + slot + " on " + nai.name() + " error " + reason);
+            }
+            handleStopKeepalive(nai, slot, reason);
+        }
+    }
+
+    public void startNattKeepalive(NetworkAgentInfo nai, int intervalSeconds, Messenger messenger,
+            IBinder binder, String srcAddrString, int srcPort, String dstAddrString, int dstPort) {
+        InetAddress srcAddress, dstAddress;
+        try {
+            srcAddress = NetworkUtils.numericToInetAddress(srcAddrString);
+            dstAddress = NetworkUtils.numericToInetAddress(dstAddrString);
+        } catch (IllegalArgumentException e) {
+            notifyMessenger(messenger, NO_KEEPALIVE, ERROR_INVALID_IP_ADDRESS);
+            return;
+        }
+
+        KeepalivePacketData packet;
+        try {
+            packet = KeepalivePacketData.nattKeepalivePacket(
+                    srcAddress, srcPort, dstAddress, NATT_PORT);
+        } catch (KeepalivePacketData.InvalidPacketException e) {
+            notifyMessenger(messenger, NO_KEEPALIVE, e.error);
+            return;
+        }
+        KeepaliveInfo ki = new KeepaliveInfo(messenger, binder, nai, packet, intervalSeconds);
+        Log.d(TAG, "Created keepalive: " + ki.toString());
+        mConnectivityServiceHandler.obtainMessage(
+                NetworkAgent.CMD_START_PACKET_KEEPALIVE, ki).sendToTarget();
+    }
+
+    public void dump(IndentingPrintWriter pw) {
+        pw.println("Packet keepalives:");
+        pw.increaseIndent();
+        for (NetworkAgentInfo nai : mKeepalives.keySet()) {
+            pw.println(nai.name());
+            pw.increaseIndent();
+            for (int slot : mKeepalives.get(nai).keySet()) {
+                KeepaliveInfo ki = mKeepalives.get(nai).get(slot);
+                pw.println(slot + ": " + ki.toString());
+            }
+            pw.decreaseIndent();
+        }
+        pw.decreaseIndent();
+    }
+}