/*
 * 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 static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.net.NattSocketKeepalive.NATT_PORT;
import static android.net.SocketKeepalive.BINDER_DIED;
import static android.net.SocketKeepalive.DATA_RECEIVED;
import static android.net.SocketKeepalive.ERROR_INSUFFICIENT_RESOURCES;
import static android.net.SocketKeepalive.ERROR_INVALID_INTERVAL;
import static android.net.SocketKeepalive.ERROR_INVALID_IP_ADDRESS;
import static android.net.SocketKeepalive.ERROR_INVALID_NETWORK;
import static android.net.SocketKeepalive.ERROR_INVALID_SOCKET;
import static android.net.SocketKeepalive.ERROR_NO_SUCH_SLOT;
import static android.net.SocketKeepalive.ERROR_STOP_REASON_UNINITIALIZED;
import static android.net.SocketKeepalive.ERROR_UNSUPPORTED;
import static android.net.SocketKeepalive.MAX_INTERVAL_SEC;
import static android.net.SocketKeepalive.MIN_INTERVAL_SEC;
import static android.net.SocketKeepalive.NO_KEEPALIVE;
import static android.net.SocketKeepalive.SUCCESS;
import static android.net.SocketKeepalive.SUCCESS_PAUSED;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.Context;
import android.net.ISocketKeepaliveCallback;
import android.net.InetAddresses;
import android.net.InvalidPacketException;
import android.net.KeepalivePacketData;
import android.net.NattKeepalivePacketData;
import android.net.NetworkAgent;
import android.net.SocketKeepalive.InvalidSocketException;
import android.net.TcpKeepalivePacketData;
import android.net.util.KeepaliveUtils;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.Process;
import android.os.RemoteException;
import android.system.ErrnoException;
import android.system.Os;
import android.util.Log;

import com.android.connectivity.resources.R;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.IndentingPrintWriter;
import com.android.net.module.util.HexDump;
import com.android.net.module.util.IpUtils;

import java.io.FileDescriptor;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;

/**
 * Manages socket 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
 * handle* methods must be called only from the ConnectivityService handler thread.
 */
public class KeepaliveTracker {

    private static final String TAG = "KeepaliveTracker";
    private static final boolean DBG = false;

    public static final String PERMISSION = android.Manifest.permission.PACKET_KEEPALIVE_OFFLOAD;

    /** Keeps track of keepalive requests. */
    private final HashMap <NetworkAgentInfo, HashMap<Integer, KeepaliveInfo>> mKeepalives =
            new HashMap<> ();
    @NonNull
    private final TcpKeepaliveController mTcpController;
    @NonNull
    private final Context mContext;

    // Supported keepalive count for each transport type, can be configured through
    // config_networkSupportedKeepaliveCount. For better error handling, use
    // {@link getSupportedKeepalivesForNetworkCapabilities} instead of direct access.
    @NonNull
    private final int[] mSupportedKeepalives;

    // Reserved privileged keepalive slots per transport. Caller's permission will be enforced if
    // the number of remaining keepalive slots is less than or equal to the threshold.
    private final int mReservedPrivilegedSlots;

    // Allowed unprivileged keepalive slots per uid. Caller's permission will be enforced if
    // the number of remaining keepalive slots is less than or equal to the threshold.
    private final int mAllowedUnprivilegedSlotsForUid;

    public KeepaliveTracker(Context context, Handler handler) {
        this(context, handler, new TcpKeepaliveController(handler));
    }

    @VisibleForTesting
    KeepaliveTracker(Context context, Handler handler, TcpKeepaliveController tcpController) {
        mTcpController = tcpController;
        mContext = context;

        mSupportedKeepalives = KeepaliveResourceUtil.getSupportedKeepalives(context);

        final ConnectivityResources res = new ConnectivityResources(mContext);
        mReservedPrivilegedSlots = res.get().getInteger(
                R.integer.config_reservedPrivilegedKeepaliveSlots);
        mAllowedUnprivilegedSlotsForUid = res.get().getInteger(
                R.integer.config_allowedUnprivilegedKeepalivePerUid);
    }

    /**
     * Tracks information about a socket 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.
     */
    @VisibleForTesting
    public class KeepaliveInfo implements IBinder.DeathRecipient {
        // TODO : remove this member. Only AutoOnOffKeepalive should have a reference to this.
        public final ISocketKeepaliveCallback mCallback;
        // Bookkeeping data.
        private final int mUid;
        private final int mPid;
        private final boolean mPrivileged;
        public final NetworkAgentInfo mNai;
        private final int mType;
        public final FileDescriptor mFd;
        // True if this was resumed from a previously turned off keepalive, otherwise false.
        // This is necessary to send the correct callbacks.
        public final boolean mResumed;

        public static final int TYPE_NATT = 1;
        public static final int TYPE_TCP = 2;

        // Keepalive slot. A small integer that identifies this keepalive among the ones handled
        // by this network. This is initialized to NO_KEEPALIVE for new keepalives, but to the
        // old slot for resumed keepalives.
        private int mSlot;

        // Packet data.
        private final KeepalivePacketData mPacket;
        private final int mInterval;

        // Whether the keepalive is started or not. The initial state is NOT_STARTED.
        private static final int NOT_STARTED = 1;
        private static final int STARTING = 2;
        private static final int STARTED = 3;
        private static final int STOPPING = 4;
        private int mStartedState = NOT_STARTED;
        private int mStopReason = ERROR_STOP_REASON_UNINITIALIZED;

        KeepaliveInfo(@NonNull ISocketKeepaliveCallback callback,
                @NonNull NetworkAgentInfo nai,
                @NonNull KeepalivePacketData packet,
                int interval,
                int type,
                @Nullable FileDescriptor fd) throws InvalidSocketException {
            this(callback, nai, packet, Binder.getCallingPid(), Binder.getCallingUid(), interval,
                    type, fd, NO_KEEPALIVE /* slot */, false /* resumed */);
        }

        KeepaliveInfo(@NonNull ISocketKeepaliveCallback callback,
                @NonNull NetworkAgentInfo nai,
                @NonNull KeepalivePacketData packet,
                int pid,
                int uid,
                int interval,
                int type,
                @Nullable FileDescriptor fd,
                int slot,
                boolean resumed) throws InvalidSocketException {
            mCallback = callback;
            mPid = pid;
            mUid = uid;
            mPrivileged = (PERMISSION_GRANTED == mContext.checkPermission(PERMISSION, mPid, mUid));

            mNai = nai;
            mPacket = packet;
            mInterval = interval;
            mType = type;
            mSlot = slot;
            mResumed = resumed;

            // For SocketKeepalive, a dup of fd is kept in mFd so the source port from which the
            // keepalives are sent cannot be reused by another app even if the fd gets closed by
            // the user. A null is acceptable here for backward compatibility of PacketKeepalive
            // API.
            try {
                if (fd != null) {
                    mFd = Os.dup(fd);
                }  else {
                    Log.d(TAG, toString() + " calls with null fd");
                    if (!mPrivileged) {
                        throw new SecurityException(
                                "null fd is not allowed for unprivileged access.");
                    }
                    if (mType == TYPE_TCP) {
                        throw new IllegalArgumentException(
                                "null fd is not allowed for tcp socket keepalives.");
                    }
                    mFd = null;
                }
            } catch (ErrnoException e) {
                Log.e(TAG, "Cannot dup fd: ", e);
                throw new InvalidSocketException(ERROR_INVALID_SOCKET, e);
            }

            try {
                mCallback.asBinder().linkToDeath(this, 0);
            } catch (RemoteException e) {
                binderDied();
            }
        }

        public NetworkAgentInfo getNai() {
            return mNai;
        }

        private String startedStateString(final int state) {
            switch (state) {
                case NOT_STARTED : return "NOT_STARTED";
                case STARTING : return "STARTING";
                case STARTED : return "STARTED";
                case STOPPING : return "STOPPING";
            }
            throw new IllegalArgumentException("Unknown state");
        }

        public String toString() {
            return "KeepaliveInfo ["
                    + " type=" + mType
                    + " network=" + mNai.network
                    + " startedState=" + startedStateString(mStartedState)
                    + " "
                    + IpUtils.addressAndPortToString(mPacket.getSrcAddress(), mPacket.getSrcPort())
                    + "->"
                    + IpUtils.addressAndPortToString(mPacket.getDstAddress(), mPacket.getDstPort())
                    + " interval=" + mInterval
                    + " uid=" + mUid + " pid=" + mPid + " privileged=" + mPrivileged
                    + " packetData=" + HexDump.toHexString(mPacket.getPacket())
                    + " ]";
        }

        /** Called when the application process is killed. */
        public void binderDied() {
            // TODO b/267106526 : this is not called on the handler thread but stop() happily
            // assumes it is, which means this is a pretty dangerous race condition.
            stop(BINDER_DIED);
        }

        void unlinkDeathRecipient() {
            if (mCallback != null) {
                mCallback.asBinder().unlinkToDeath(this, 0);
            }
        }

        public int getSlot() {
            return mSlot;
        }

        int getKeepaliveIntervalSec() {
            return mInterval;
        }

        public int getUid() {
            return mUid;
        }

        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.getSrcAddress())) {
                    return SUCCESS;
                }
            }
            return ERROR_INVALID_IP_ADDRESS;
        }

        private int checkInterval() {
            if (mInterval < MIN_INTERVAL_SEC || mInterval > MAX_INTERVAL_SEC) {
                return ERROR_INVALID_INTERVAL;
            }
            return SUCCESS;
        }

        private int checkPermission() {
            final HashMap<Integer, KeepaliveInfo> networkKeepalives = mKeepalives.get(mNai);
            if (networkKeepalives == null) {
                return ERROR_INVALID_NETWORK;
            }

            if (mPrivileged) return SUCCESS;

            final int supported = KeepaliveUtils.getSupportedKeepalivesForNetworkCapabilities(
                    mSupportedKeepalives, mNai.networkCapabilities);

            int takenUnprivilegedSlots = 0;
            for (final KeepaliveInfo ki : networkKeepalives.values()) {
                if (!ki.mPrivileged) ++takenUnprivilegedSlots;
            }
            if (takenUnprivilegedSlots > supported - mReservedPrivilegedSlots) {
                return ERROR_INSUFFICIENT_RESOURCES;
            }

            // Count unprivileged keepalives for the same uid across networks.
            int unprivilegedCountSameUid = 0;
            for (final HashMap<Integer, KeepaliveInfo> kaForNetwork : mKeepalives.values()) {
                for (final KeepaliveInfo ki : kaForNetwork.values()) {
                    if (ki.mUid == mUid) {
                        unprivilegedCountSameUid++;
                    }
                }
            }
            if (unprivilegedCountSameUid > mAllowedUnprivilegedSlotsForUid) {
                return ERROR_INSUFFICIENT_RESOURCES;
            }
            return SUCCESS;
        }

        private int checkLimit() {
            final HashMap<Integer, KeepaliveInfo> networkKeepalives = mKeepalives.get(mNai);
            if (networkKeepalives == null) {
                return ERROR_INVALID_NETWORK;
            }
            final int supported = KeepaliveUtils.getSupportedKeepalivesForNetworkCapabilities(
                    mSupportedKeepalives, mNai.networkCapabilities);
            if (supported == 0) return ERROR_UNSUPPORTED;
            if (networkKeepalives.size() > supported) return ERROR_INSUFFICIENT_RESOURCES;
            return SUCCESS;
        }

        /**
         * Checks if the keepalive info is valid to start.
         *
         * @return SUCCESS if the keepalive is valid and the error reason otherwise.
         */
        public int isValid() {
            synchronized (mNai) {
                int error = checkInterval();
                if (error == SUCCESS) error = checkLimit();
                if (error == SUCCESS) error = checkPermission();
                if (error == SUCCESS) error = checkNetworkConnected();
                if (error == SUCCESS) error = checkSourceAddress();
                return error;
            }
        }

        /**
         * Attempt to start the keepalive on the given slot.
         *
         * @param slot the slot to start the keepalive on.
         * @return SUCCESS if the keepalive is successfully starting and the error reason otherwise.
         */
        int start(int slot) {
            // BINDER_DIED can happen if the binder died before the KeepaliveInfo was created and
            // the constructor set the state to BINDER_DIED. If that's the case, the KI is already
            // cleaned up.
            if (BINDER_DIED == mStartedState) return BINDER_DIED;
            mSlot = slot;
            int error = isValid();
            if (error == SUCCESS) {
                Log.d(TAG, "Starting keepalive " + mSlot + " on " + mNai.toShortString());
                switch (mType) {
                    case TYPE_NATT:
                        final NattKeepalivePacketData nattData = (NattKeepalivePacketData) mPacket;
                        mNai.onAddNattKeepalivePacketFilter(slot, nattData);
                        mNai.onStartNattSocketKeepalive(slot, mInterval, nattData);
                        break;
                    case TYPE_TCP:
                        try {
                            mTcpController.startSocketMonitor(mFd, mCallback, mSlot);
                        } catch (InvalidSocketException e) {
                            handleStopKeepalive(mNai, mSlot, ERROR_INVALID_SOCKET);
                            return ERROR_INVALID_SOCKET;
                        }
                        final TcpKeepalivePacketData tcpData = (TcpKeepalivePacketData) mPacket;
                        mNai.onAddTcpKeepalivePacketFilter(slot, tcpData);
                        // TODO: check result from apf and notify of failure as needed.
                        mNai.onStartTcpSocketKeepalive(slot, mInterval, tcpData);
                        break;
                    default:
                        Log.wtf(TAG, "Starting keepalive with unknown type: " + mType);
                        handleStopKeepalive(mNai, mSlot, ERROR_UNSUPPORTED);
                        return ERROR_UNSUPPORTED;
                }
                mStartedState = STARTING;
                return SUCCESS;
            } else {
                handleStopKeepalive(mNai, mSlot, error);
                return error;
            }
        }

        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);
                }
            }
            // To prevent races from re-entrance of stop(), return if the state is already stopping.
            // This might happen if multiple event sources stop keepalive in a short time. Such as
            // network disconnect after user calls stop(), or tear down socket after binder died.
            // Note that it's always possible this method is called by the auto keepalive timer
            // or any other way after the binder died, hence the check for BINDER_DIED. If the
            // binder has died, then the KI has already been cleaned up.
            if (mStartedState == STOPPING || mStartedState == BINDER_DIED) return;

            // Store the reason of stopping, and report it after the keepalive is fully stopped.
            if (mStopReason != ERROR_STOP_REASON_UNINITIALIZED) {
                throw new IllegalStateException("Unexpected stop reason: " + mStopReason);
            }
            mStopReason = reason;
            Log.d(TAG, "Stopping keepalive " + mSlot + " on " + mNai.toShortString()
                    + ": " + reason);
            switch (mStartedState) {
                case NOT_STARTED:
                    // Remove the reference to this keepalive that had an error before starting,
                    // e.g. invalid parameter.
                    cleanupStoppedKeepalive(mNai, mSlot);
                    if (BINDER_DIED == reason) mStartedState = BINDER_DIED;
                    break;
                default:
                    mStartedState = STOPPING;
                    switch (mType) {
                        case TYPE_TCP:
                            mTcpController.stopSocketMonitor(mSlot);
                            // fall through
                        case TYPE_NATT:
                            mNai.onStopSocketKeepalive(mSlot);
                            mNai.onRemoveKeepalivePacketFilter(mSlot);
                            break;
                        default:
                            Log.wtf(TAG, "Stopping keepalive with unknown type: " + mType);
                    }
            }

            // Close the duplicated fd that maintains the lifecycle of socket whenever
            // keepalive is running.
            if (mFd != null) {
                try {
                    Os.close(mFd);
                } catch (ErrnoException e) {
                    // This should not happen since system server controls the lifecycle of fd when
                    // keepalive offload is running.
                    Log.wtf(TAG, "Error closing fd for keepalive " + mSlot + ": " + e);
                }
            }
        }

        /**
         * Construct a new KeepaliveInfo from existing KeepaliveInfo with a new fd.
         */
        public KeepaliveInfo withFd(@NonNull FileDescriptor fd) throws InvalidSocketException {
            return new KeepaliveInfo(mCallback, mNai, mPacket, mPid, mUid, mInterval, mType,
                    fd, mSlot, true /* resumed */);
        }
    }

    void notifyErrorCallback(ISocketKeepaliveCallback cb, int error) {
        if (DBG) Log.w(TAG, "Sending onError(" + error + ") callback");
        try {
            cb.onError(error);
        } catch (RemoteException e) {
            Log.w(TAG, "Discarded onError(" + error + ") callback");
        }
    }

    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. Slot numbers start from 1, because that's what two
        // separate chipset implementations independently came up with.
        int slot;
        for (slot = 1; slot <= networkKeepalives.size(); slot++) {
            if (networkKeepalives.get(slot) == null) {
                return slot;
            }
        }
        return slot;
    }

    /**
     * Handle start keepalives with the message.
     *
     * @param ki the keepalive to start.
     * @return SUCCESS if the keepalive is successfully starting and the error reason otherwise.
     */
    public int handleStartKeepalive(KeepaliveInfo ki) {
        NetworkAgentInfo nai = ki.getNai();
        // If this was a paused keepalive, then reuse the same slot that was kept for it. Otherwise,
        // use the first free slot for this network agent.
        final int slot = NO_KEEPALIVE != ki.mSlot ? ki.mSlot : findFirstFreeSlot(nai);
        mKeepalives.get(nai).put(slot, ki);
        return ki.start(slot);
    }

    public void handleStopAllKeepalives(NetworkAgentInfo nai, int reason) {
        final HashMap<Integer, KeepaliveInfo> networkKeepalives = mKeepalives.get(nai);
        if (networkKeepalives != null) {
            final ArrayList<KeepaliveInfo> kalist = new ArrayList(networkKeepalives.values());
            for (KeepaliveInfo ki : kalist) {
                // Check if keepalive is already stopped
                if (ki.mStopReason == SUCCESS_PAUSED) continue;
                ki.stop(reason);
                // Clean up keepalives since the network agent is disconnected and unable to pass
                // back asynchronous result of stop().
                cleanupStoppedKeepalive(nai, ki.mSlot);
            }
        }
    }

    public void handleStopKeepalive(NetworkAgentInfo nai, int slot, int reason) {
        final String networkName = NetworkAgentInfo.toShortString(nai);
        HashMap <Integer, KeepaliveInfo> networkKeepalives = mKeepalives.get(nai);
        if (networkKeepalives == null) {
            Log.e(TAG, "Attempt to stop keepalive on nonexistent network " + networkName);
            return;
        }
        KeepaliveInfo ki = networkKeepalives.get(slot);
        if (ki == null) {
            Log.e(TAG, "Attempt to stop nonexistent keepalive " + slot + " on " + networkName);
            return;
        }
        ki.stop(reason);
        // Clean up keepalives will be done as a result of calling ki.stop() after the slots are
        // freed.
    }

    private void cleanupStoppedKeepalive(NetworkAgentInfo nai, int slot) {
        final String networkName = NetworkAgentInfo.toShortString(nai);
        HashMap<Integer, KeepaliveInfo> networkKeepalives = mKeepalives.get(nai);
        if (networkKeepalives == null) {
            Log.e(TAG, "Attempt to remove keepalive on nonexistent network " + networkName);
            return;
        }
        KeepaliveInfo ki = networkKeepalives.get(slot);
        if (ki == null) {
            Log.e(TAG, "Attempt to remove nonexistent keepalive " + slot + " on " + networkName);
            return;
        }

        // If the keepalive was stopped for good, remove it from the hash table so the slot can
        // be considered available when reusing it. If it was only a pause, let it sit in the map
        // so it sits on the slot.
        final int reason = ki.mStopReason;
        if (reason != SUCCESS_PAUSED) {
            networkKeepalives.remove(slot);
            Log.d(TAG, "Remove keepalive " + slot + " on " + networkName + ", "
                    + networkKeepalives.size() + " remains.");
        } else {
            Log.d(TAG, "Pause keepalive " + slot + " on " + networkName + ", keep slot reserved");
        }
        if (networkKeepalives.isEmpty()) {
            mKeepalives.remove(nai);
        }

        // Notify app that the keepalive is stopped.
        if (reason == SUCCESS) {
            try {
                ki.mCallback.onStopped();
            } catch (RemoteException e) {
                Log.w(TAG, "Discarded onStop callback: " + reason);
            }
        } else if (reason == SUCCESS_PAUSED) {
            try {
                ki.mCallback.onPaused();
            } catch (RemoteException e) {
                Log.w(TAG, "Discarded onPaused callback: " + reason);
            }
        } else if (reason == DATA_RECEIVED) {
            try {
                ki.mCallback.onDataReceived();
            } catch (RemoteException e) {
                Log.w(TAG, "Discarded onDataReceived callback: " + reason);
            }
        } else if (reason == ERROR_STOP_REASON_UNINITIALIZED) {
            throw new IllegalStateException("Unexpected stop reason: " + reason);
        } else if (reason == ERROR_NO_SUCH_SLOT) {
            // There are multiple independent reasons a keepalive can stop. Some
            // are software (e.g. the app stops the keepalive) and some are hardware
            // (e.g. the SIM card gets removed). Therefore, there is a very low
            // probability that both of these happen at the same time, which would
            // result in the first stop attempt returning SUCCESS and the second
            // stop attempt returning NO_SUCH_SLOT. Such a race condition can be
            // ignored with a log.
            // This should still be reported because if it happens with any frequency
            // it probably means there is a bug where the system server is trying
            // to use a non-existing hardware slot.
            // TODO : separate the non-existing hardware slot from the case where
            // there is no keepalive running on this slot.
            Log.wtf(TAG, "Keepalive on slot " + slot + " can't be stopped : " + reason);
            notifyErrorCallback(ki.mCallback, reason);
        } else {
            notifyErrorCallback(ki.mCallback, reason);
        }

        ki.unlinkDeathRecipient();
    }

    /**
     * Finalize a paused keepalive.
     *
     * This will send the appropriate callback after checking that this keepalive is indeed paused,
     * and free the slot.
     *
     * @param ki the keepalive to finalize
     * @param reason the reason the keepalive is stopped
     */
    public void finalizePausedKeepalive(@NonNull final KeepaliveInfo ki, int reason) {
        if (SUCCESS_PAUSED != ki.mStopReason) {
            throw new IllegalStateException("Keepalive is not paused");
        }
        if (reason == SUCCESS) {
            try {
                ki.mCallback.onStopped();
            } catch (RemoteException e) {
                Log.w(TAG, "Discarded onStopped callback while finalizing paused keepalive");
            }
        } else {
            notifyErrorCallback(ki.mCallback, reason);
        }

        final HashMap<Integer, KeepaliveInfo> networkKeepalives = mKeepalives.get(ki.mNai);
        if (networkKeepalives == null) {
            Log.e(TAG, "Attempt to finalize keepalive on nonexistent network " + ki.mNai);
            return;
        }
        networkKeepalives.remove(ki.mSlot);
    }

    /**
     * Handle keepalive events from lower layer.
     *
     * @return false if the event caused handleStopKeepalive to be called, i.e. the keepalive is
     *     forced to stop. Otherwise, return true.
     */
    public boolean handleEventSocketKeepalive(@NonNull NetworkAgentInfo nai, int slot, int reason) {
        KeepaliveInfo ki = null;
        try {
            ki = mKeepalives.get(nai).get(slot);
        } catch(NullPointerException e) {}
        if (ki == null) {
            Log.e(TAG, "Event " + NetworkAgent.EVENT_SOCKET_KEEPALIVE + "," + slot + "," + reason
                    + " for unknown keepalive " + slot + " on " + nai.toShortString());
            return true;
        }

        // This can be called in a number of situations :
        // - startedState is STARTING.
        //   - reason is SUCCESS => go to STARTED.
        //   - reason isn't SUCCESS => it's an error starting. Go to NOT_STARTED and stop keepalive.
        // - startedState is STARTED.
        //   - reason is SUCCESS => it's a success stopping. Go to NOT_STARTED and stop keepalive.
        //   - reason isn't SUCCESS => it's an error in exec. Go to NOT_STARTED and stop keepalive.
        // The control is not supposed to ever come here if the state is NOT_STARTED. This is
        // because in NOT_STARTED state, the code will switch to STARTING before sending messages
        // to start, and the only way to NOT_STARTED is this function, through the edges outlined
        // above : in all cases, keepalive gets stopped and can't restart without going into
        // STARTING as messages are ordered. This also depends on the hardware processing the
        // messages in order.
        // TODO : clarify this code and get rid of mStartedState. Using a StateMachine is an
        // option.
        if (KeepaliveInfo.STARTING == ki.mStartedState) {
            if (SUCCESS == reason) {
                // Keepalive successfully started.
                Log.d(TAG, "Started keepalive " + slot + " on " + nai.toShortString());
                ki.mStartedState = KeepaliveInfo.STARTED;
                try {
                    if (ki.mResumed) {
                        ki.mCallback.onResumed();
                    } else {
                        ki.mCallback.onStarted();
                    }
                } catch (RemoteException e) {
                    Log.w(TAG, "Discarded " + (ki.mResumed ? "onResumed" : "onStarted")
                            + " callback for slot " + slot);
                }
                return true;
            } else {
                Log.d(TAG, "Failed to start keepalive " + slot + " on " + nai.toShortString()
                        + ": " + reason);
                // The message indicated some error trying to start: do call handleStopKeepalive.
                handleStopKeepalive(nai, slot, reason);
                return false;
            }
        } else if (KeepaliveInfo.STOPPING == ki.mStartedState) {
            // The message indicated result of stopping : clean up keepalive slots.
            Log.d(TAG, "Stopped keepalive " + slot + " on " + nai.toShortString()
                    + " stopped: " + reason);
            ki.mStartedState = KeepaliveInfo.NOT_STARTED;
            cleanupStoppedKeepalive(nai, slot);
            return true;
        } else {
            Log.wtf(TAG, "Event " + NetworkAgent.EVENT_SOCKET_KEEPALIVE + "," + slot + "," + reason
                    + " for keepalive in wrong state: " + ki.toString());
            // Although this is an unexpected event, the keepalive is not stopped here.
            return true;
        }
    }

    /**
     * Called when requesting that keepalives be started on a IPsec NAT-T socket. See
     * {@link android.net.SocketKeepalive}.
     **/
    @Nullable
    public KeepaliveInfo makeNattKeepaliveInfo(@Nullable NetworkAgentInfo nai,
            @Nullable FileDescriptor fd,
            int intervalSeconds,
            @NonNull ISocketKeepaliveCallback cb,
            @NonNull String srcAddrString,
            int srcPort,
            @NonNull String dstAddrString,
            int dstPort) {
        if (nai == null) {
            notifyErrorCallback(cb, ERROR_INVALID_NETWORK);
            return null;
        }

        InetAddress srcAddress, dstAddress;
        try {
            srcAddress = InetAddresses.parseNumericAddress(srcAddrString);
            dstAddress = InetAddresses.parseNumericAddress(dstAddrString);
        } catch (IllegalArgumentException e) {
            notifyErrorCallback(cb, ERROR_INVALID_IP_ADDRESS);
            return null;
        }

        KeepalivePacketData packet;
        try {
            packet = NattKeepalivePacketData.nattKeepalivePacket(
                    srcAddress, srcPort, dstAddress, NATT_PORT);
        } catch (InvalidPacketException e) {
            notifyErrorCallback(cb, e.getError());
            return null;
        }
        KeepaliveInfo ki = null;
        try {
            ki = new KeepaliveInfo(cb, nai, packet, intervalSeconds,
                    KeepaliveInfo.TYPE_NATT, fd);
        } catch (InvalidSocketException | IllegalArgumentException | SecurityException e) {
            Log.e(TAG, "Fail to construct keepalive", e);
            notifyErrorCallback(cb, ERROR_INVALID_SOCKET);
            return null;
        }
        Log.d(TAG, "Created keepalive: " + ki);
        return ki;
    }

    /**
     * Make a KeepaliveInfo for a TCP socket.
     *
     * In order to offload keepalive for application correctly, sequence number, ack number and
     * other fields are needed to form the keepalive packet. Thus, this function synchronously
     * puts the socket into repair mode to get the necessary information. After the socket has been
     * put into repair mode, the application cannot access the socket until reverted to normal.
     *
     * See {@link android.net.SocketKeepalive}.
     **/
    @Nullable
    public KeepaliveInfo makeTcpKeepaliveInfo(@Nullable NetworkAgentInfo nai,
            @NonNull FileDescriptor fd,
            int intervalSeconds,
            @NonNull ISocketKeepaliveCallback cb) {
        if (nai == null) {
            notifyErrorCallback(cb, ERROR_INVALID_NETWORK);
            return null;
        }

        final TcpKeepalivePacketData packet;
        try {
            packet = TcpKeepaliveController.getTcpKeepalivePacket(fd);
        } catch (InvalidSocketException e) {
            notifyErrorCallback(cb, e.error);
            return null;
        } catch (InvalidPacketException e) {
            notifyErrorCallback(cb, e.getError());
            return null;
        }
        KeepaliveInfo ki = null;
        try {
            ki = new KeepaliveInfo(cb, nai, packet, intervalSeconds,
                    KeepaliveInfo.TYPE_TCP, fd);
        } catch (InvalidSocketException | IllegalArgumentException | SecurityException e) {
            Log.e(TAG, "Fail to construct keepalive e=" + e);
            notifyErrorCallback(cb, ERROR_INVALID_SOCKET);
            return null;
        }
        Log.d(TAG, "Created keepalive: " + ki.toString());
        return ki;
    }

    /**
     * Make a KeepaliveInfo for an IPSec NAT-T socket.
     *
     * This function is identical to {@link #makeNattKeepaliveInfo}, 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.
     **/
    @Nullable
    public KeepaliveInfo makeNattKeepaliveInfo(@Nullable NetworkAgentInfo nai,
            @Nullable FileDescriptor fd,
            int resourceId,
            int intervalSeconds,
            @NonNull ISocketKeepaliveCallback cb,
            @NonNull String srcAddrString,
            @NonNull String dstAddrString,
            int dstPort) {
        // Ensure that the socket is created by IpSecService.
        if (!isNattKeepaliveSocketValid(fd, resourceId)) {
            notifyErrorCallback(cb, ERROR_INVALID_SOCKET);
            return null;
        }

        // Get src port to adopt old API.
        int srcPort = 0;
        try {
            final SocketAddress srcSockAddr = Os.getsockname(fd);
            srcPort = ((InetSocketAddress) srcSockAddr).getPort();
        } catch (ErrnoException e) {
            notifyErrorCallback(cb, ERROR_INVALID_SOCKET);
            return null;
        }

        // Forward request to old API.
        return makeNattKeepaliveInfo(nai, fd, intervalSeconds, cb, 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;
    }

    /**
     * Dump KeepaliveTracker state.
     */
    public void dump(IndentingPrintWriter pw) {
        pw.println("Supported Socket keepalives: " + Arrays.toString(mSupportedKeepalives));
        pw.println("Reserved Privileged keepalives: " + mReservedPrivilegedSlots);
        pw.println("Allowed Unprivileged keepalives per uid: " + mAllowedUnprivilegedSlotsForUid);
        pw.println("Socket keepalives:");
        pw.increaseIndent();
        for (NetworkAgentInfo nai : mKeepalives.keySet()) {
            pw.println(nai.toShortString());
            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();
    }
}
