/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.server.wifi.aware;

import static android.net.RouteInfo.RTN_UNICAST;

import android.content.Context;
import android.net.ConnectivityManager;
import android.net.IpPrefix;
import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.MacAddress;
import android.net.MatchAllNetworkSpecifier;
import android.net.NetworkAgent;
import android.net.NetworkAgentConfig;
import android.net.NetworkCapabilities;
import android.net.NetworkFactory;
import android.net.NetworkProvider;
import android.net.NetworkRequest;
import android.net.NetworkSpecifier;
import android.net.RouteInfo;
import android.net.wifi.aware.TlvBufferUtils;
import android.net.wifi.aware.WifiAwareAgentNetworkSpecifier;
import android.net.wifi.aware.WifiAwareChannelInfo;
import android.net.wifi.aware.WifiAwareDataPathSecurityConfig;
import android.net.wifi.aware.WifiAwareManager;
import android.net.wifi.aware.WifiAwareNetworkInfo;
import android.net.wifi.aware.WifiAwareNetworkSpecifier;
import android.net.wifi.util.HexEncoding;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Log;
import android.util.Pair;
import android.util.SparseArray;

import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.utils.build.SdkLevel;
import com.android.server.wifi.Clock;
import com.android.server.wifi.hal.WifiNanIface.NanDataPathChannelCfg;
import com.android.server.wifi.hal.WifiNanIface.NanStatusCode;
import com.android.server.wifi.util.NetdWrapper;
import com.android.server.wifi.util.WifiPermissionsUtil;
import com.android.server.wifi.util.WifiPermissionsWrapper;
import com.android.wifi.resources.R;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.net.DatagramSocket;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

/**
 * Manages Aware data-path lifetime: interface creation/deletion, data-path setup and tear-down.
 * The Aware network configuration is:
 * - transport = TRANSPORT_WIFI_AWARE
 * - capabilities = NET_CAPABILITY_NOT_VPN
 * - network specifier generated by DiscoverySession.createNetworkSpecifier(...) or
 *   WifiAwareManager.createNetworkSpecifier(...).
 */
public class WifiAwareDataPathStateManager {
    private static final String TAG = "WifiAwareDataPathStMgr";
    private static boolean sVdbg = false; // STOPSHIP if true

    private static final String AWARE_INTERFACE_PREFIX = "aware_data";
    private static final String NETWORK_TAG = "WIFI_AWARE_FACTORY";
    private static final String AGENT_TAG_PREFIX = "WIFI_AWARE_AGENT_";
    private static final int NETWORK_FACTORY_SCORE_AVAIL = 1;
    private static final int NETWORK_FACTORY_BANDWIDTH_AVAIL = 1;
    private static final int NETWORK_FACTORY_SIGNAL_STRENGTH_AVAIL = 1;

    @VisibleForTesting
    public static final int ADDRESS_VALIDATION_RETRY_INTERVAL_MS = 1_000; // 1 second
    @VisibleForTesting
    public static final int ADDRESS_VALIDATION_TIMEOUT_MS = 5_000; // 5 seconds

    private boolean mVerboseLoggingEnabled = false;
    private final WifiAwareStateManager mMgr;
    private final Clock mClock;
    public NetworkInterfaceWrapper mNiWrapper = new NetworkInterfaceWrapper();
    private static final NetworkCapabilities sNetworkCapabilitiesFilter =
            makeNetworkCapabilitiesFilter();
    private final Set<String> mInterfaces = new HashSet<>();
    private final ArrayMap<WifiAwareNetworkSpecifier, AwareNetworkRequestInformation>
            mNetworkRequestsCache = new ArrayMap<>();
    private Context mContext;
    private WifiAwareMetrics mAwareMetrics;
    private WifiPermissionsUtil mWifiPermissionsUtil;
    private WifiPermissionsWrapper mPermissionsWrapper;
    private Looper mLooper;
    private Handler mHandler;
    private WifiAwareNetworkFactory mNetworkFactory;
    public NetdWrapper mNetdWrapper;
    private final SparseArray<Object> mDelayNetworkValidationMap = new SparseArray<>();

    // internal debug flag to override API check
    /* package */ boolean mAllowNdpResponderFromAnyOverride = false;

    public WifiAwareDataPathStateManager(WifiAwareStateManager mgr, Clock clock) {
        mMgr = mgr;
        mClock = clock;
    }

    private static NetworkCapabilities makeNetworkCapabilitiesFilter() {
        NetworkCapabilities.Builder builder = new NetworkCapabilities.Builder()
                .addTransportType(NetworkCapabilities.TRANSPORT_WIFI_AWARE)
                .removeCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_ROAMING)
                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED)
                .addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
                .addCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
                .setNetworkSpecifier(new MatchAllNetworkSpecifier())
                .setLinkUpstreamBandwidthKbps(NETWORK_FACTORY_BANDWIDTH_AVAIL)
                .setLinkDownstreamBandwidthKbps(NETWORK_FACTORY_BANDWIDTH_AVAIL)
                .setSignalStrength(NETWORK_FACTORY_SIGNAL_STRENGTH_AVAIL);
        if (SdkLevel.isAtLeastS()) {
            builder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VCN_MANAGED);
        }
        return builder.build();
    }

    /**
     * Initialize the Aware data-path state manager. Specifically register the network factory with
     * connectivity service.
     */
    public void start(Context context, Looper looper, WifiAwareMetrics awareMetrics,
            WifiPermissionsUtil wifiPermissionsUtil, WifiPermissionsWrapper permissionsWrapper,
            NetdWrapper netdWrapper) {
        if (sVdbg) Log.v(TAG, "start");

        mContext = context;
        mAwareMetrics = awareMetrics;
        mWifiPermissionsUtil = wifiPermissionsUtil;
        mPermissionsWrapper = permissionsWrapper;
        mNetdWrapper = netdWrapper;
        mLooper = looper;
        mHandler = new Handler(mLooper);


        mNetworkFactory = new WifiAwareNetworkFactory(looper, context, sNetworkCapabilitiesFilter);
        mNetworkFactory.setScoreFilter(NETWORK_FACTORY_SCORE_AVAIL);
        mNetworkFactory.register();
    }

    /**
     * Enable/Disable verbose logging.
     *
     */
    public void enableVerboseLogging(boolean verboseEnabled, boolean vDbg) {
        mVerboseLoggingEnabled = verboseEnabled;
        sVdbg = vDbg;
    }

    /**
     * Get the number of the NDPs is already set up.
     */
    public int getNumOfNdps() {
        int numOfNdps = 0;
        for (AwareNetworkRequestInformation requestInformation : mNetworkRequestsCache.values()) {
            if (requestInformation.state != AwareNetworkRequestInformation.STATE_TERMINATING) {
                numOfNdps += requestInformation.ndpInfos.size();
            }
        }
        return numOfNdps;
    }

    private Map.Entry<WifiAwareNetworkSpecifier, AwareNetworkRequestInformation>
                getNetworkRequestByNdpId(int ndpId) {
        for (Map.Entry<WifiAwareNetworkSpecifier, AwareNetworkRequestInformation> entry :
                mNetworkRequestsCache.entrySet()) {
            if (entry.getValue().ndpInfos.contains(ndpId)) {
                return entry;
            }
        }

        return null;
    }

    private Map.Entry<WifiAwareNetworkSpecifier, AwareNetworkRequestInformation>
                getNetworkRequestByCanonicalDescriptor(CanonicalConnectionInfo cci) {
        if (sVdbg) Log.v(TAG, "getNetworkRequestByCanonicalDescriptor: cci=" + cci);
        for (Map.Entry<WifiAwareNetworkSpecifier, AwareNetworkRequestInformation> entry :
                mNetworkRequestsCache.entrySet()) {
            if (sVdbg) {
                Log.v(TAG, "getNetworkRequestByCanonicalDescriptor: entry=" + entry.getValue()
                        + " --> cci=" + entry.getValue().getCanonicalDescriptor());
            }
            if (entry.getValue().getCanonicalDescriptor().matches(cci)) {
                return entry;
            }
        }

        return null;
    }

    /**
     * Create all Aware data-path interfaces which are possible on the device - based on the
     * capabilities of the firmware.
     */
    public void createAllInterfaces() {
        Log.d(TAG, "createAllInterfaces");

        if (mMgr.getCapabilities() == null) {
            Log.e(TAG, "createAllInterfaces: capabilities aren't initialized yet!");
            return;
        }

        for (int i = 0; i < mMgr.getCapabilities().maxNdiInterfaces; ++i) {
            String name = AWARE_INTERFACE_PREFIX + i;
            if (mInterfaces.contains(name)) {
                Log.e(TAG, "createAllInterfaces(): interface already up, " + name
                        + ", possibly failed to delete - deleting/creating again to be safe");
                mMgr.deleteDataPathInterface(name);

                // critical to remove so that don't get infinite loop if the delete fails again
                mInterfaces.remove(name);
            }

            mMgr.createDataPathInterface(name);
        }
    }

    /**
     * Delete all Aware data-path interfaces which are currently up.
     */
    public void deleteAllInterfaces() {
        Log.d(TAG, "deleteAllInterfaces");
        onAwareDownCleanupDataPaths();

        if (mMgr.getCapabilities() == null) {
            Log.e(TAG, "deleteAllInterfaces: capabilities aren't initialized yet!");
            return;
        }

        for (int i = 0; i < mMgr.getCapabilities().maxNdiInterfaces; ++i) {
            String name = AWARE_INTERFACE_PREFIX + i;
            mMgr.deleteDataPathInterface(name);
        }
    }

    /**
     * Called when firmware indicates the an interface was created.
     */
    public void onInterfaceCreated(String interfaceName) {
        if (mVerboseLoggingEnabled) {
            Log.v(TAG, "onInterfaceCreated: interfaceName=" + interfaceName);
        }

        if (mInterfaces.contains(interfaceName)) {
            Log.w(TAG, "onInterfaceCreated: already contains interface -- " + interfaceName);
        }

        mInterfaces.add(interfaceName);
    }

    /**
     * Called when firmware indicates the an interface was deleted.
     */
    public void onInterfaceDeleted(String interfaceName) {
        Log.d(TAG, "onInterfaceDeleted: interfaceName=" + interfaceName);

        if (!mInterfaces.contains(interfaceName)) {
            Log.w(TAG, "onInterfaceDeleted: interface not on list -- " + interfaceName);
        }

        mInterfaces.remove(interfaceName);
    }

    /**
     * Response to initiating data-path request. Indicates that request is successful (not
     * complete!) and is now in progress.
     *
     * @param networkSpecifier The network specifier provided as part of the initiate request.
     * @param ndpId            The ID assigned to the data-path.
     * @return False if has error, otherwise return true
     */
    public boolean onDataPathInitiateSuccess(WifiAwareNetworkSpecifier networkSpecifier,
            int ndpId) {
        if (mVerboseLoggingEnabled) {
            Log.v(TAG,
                    "onDataPathInitiateSuccess: networkSpecifier=" + networkSpecifier + ", ndpId="
                            + ndpId);
        }

        AwareNetworkRequestInformation nnri = mNetworkRequestsCache.get(networkSpecifier);
        if (nnri == null) {
            Log.w(TAG, "onDataPathInitiateSuccess: network request not found for networkSpecifier="
                    + networkSpecifier);
            mMgr.endDataPath(ndpId);
            return false;
        }

        if (nnri.state
                != AwareNetworkRequestInformation.STATE_INITIATOR_WAIT_FOR_REQUEST_RESPONSE) {
            Log.w(TAG, "onDataPathInitiateSuccess: network request in incorrect state: state="
                    + nnri.state);
            mMgr.endDataPath(ndpId);
            mNetworkRequestsCache.remove(networkSpecifier);
            declareUnfullfillable(nnri);
            return false;
        }

        NdpInfo ndpInfo = new NdpInfo(ndpId);
        ndpInfo.state = NdpInfo.STATE_WAIT_FOR_CONFIRM;
        ndpInfo.peerDiscoveryMac = nnri.specifiedPeerDiscoveryMac;
        nnri.ndpInfos.put(ndpId, ndpInfo);

        nnri.state = AwareNetworkRequestInformation.STATE_IN_SETUP;

        return true;
    }

    /**
     * Response to an attempt to set up a data-path (on the initiator side).
     *
     * @param networkSpecifier The network specifier provided as part of the initiate request.
     * @param reason           Failure reason.
     */
    public void onDataPathInitiateFail(WifiAwareNetworkSpecifier networkSpecifier, int reason) {
        if (mVerboseLoggingEnabled) {
            Log.v(TAG,
                    "onDataPathInitiateFail: networkSpecifier=" + networkSpecifier + ", reason="
                            + reason);
        }

        AwareNetworkRequestInformation nnri = mNetworkRequestsCache.remove(networkSpecifier);
        if (nnri == null) {
            Log.w(TAG, "onDataPathInitiateFail: network request not found for networkSpecifier="
                    + networkSpecifier);
            return;
        }
        mNetworkFactory.letAppKnowThatRequestsAreUnavailable(nnri);

        if (nnri.state
                != AwareNetworkRequestInformation.STATE_INITIATOR_WAIT_FOR_REQUEST_RESPONSE) {
            Log.w(TAG, "onDataPathInitiateFail: network request in incorrect state: state="
                    + nnri.state);
        }

        mAwareMetrics.recordNdpStatus(reason, networkSpecifier.isOutOfBand(), networkSpecifier.role,
                mClock.getElapsedSinceBootMillis(), networkSpecifier.sessionId);
    }


    /**
     * Notification (unsolicited/asynchronous) that a peer has requested to set up a data-path
     * connection with us.
     *
     * @param pubSubId      The ID of the discovery session context for the data-path - or 0 if not
     *                      related to a discovery session.
     * @param mac           The discovery MAC address of the peer.
     * @param ndpId         The locally assigned ID for the data-path.
     * @param message       The app_info HAL field (peer's info: binary blob)
     * @return False if has error, otherwise return true
     */
    public boolean onDataPathRequest(int pubSubId, byte[] mac, int ndpId,
            byte[] message) {
        if (mVerboseLoggingEnabled) {
            Log.v(TAG, "onDataPathRequest: pubSubId=" + pubSubId + ", mac=" + String.valueOf(
                    HexEncoding.encode(mac)) + ", ndpId=" + ndpId);
        }

        // it is also possible that this is an initiator-side data-path request indication (which
        // happens when the Responder responds). In such a case it will be matched by the NDP ID.
        Map.Entry<WifiAwareNetworkSpecifier, AwareNetworkRequestInformation> nnriE =
                getNetworkRequestByNdpId(ndpId);
        if (nnriE != null) {
            if (sVdbg) {
                Log.v(TAG,
                        "onDataPathRequest: initiator-side indication for " + nnriE);
            }
            NdpInfo ndpInfo = nnriE.getValue().ndpInfos.get(ndpId);
            // potential transmission mechanism for port/transport-protocol information from
            // Responder (alternative to confirm message)
            NetworkInformationData.ParsedResults peerServerInfo = NetworkInformationData.parseTlv(
                    message);
            if (ndpInfo == null) {
                Log.wtf(TAG, "onDataPathRequest: initiator-side ndpInfo is null?!");
                return false;
            }
            if (peerServerInfo != null) {
                if (peerServerInfo.port != 0) {
                    ndpInfo.peerPort = peerServerInfo.port;
                }
                if (peerServerInfo.transportProtocol != -1) {
                    ndpInfo.peerTransportProtocol = peerServerInfo.transportProtocol;
                }
                if (peerServerInfo.ipv6Override != null) {
                    ndpInfo.peerIpv6Override = peerServerInfo.ipv6Override;
                }
            }

            return false; //ignore this for NDP set up flow: it is used to obtain app_info from Resp
        }

        AwareNetworkRequestInformation nnri = null;
        WifiAwareNetworkSpecifier networkSpecifier = null;
        for (int i = 0; i < mNetworkRequestsCache.size(); i++) {
            AwareNetworkRequestInformation requestInfo = mNetworkRequestsCache.valueAt(i);
            /*
             * Checking that the incoming request (from the Initiator) matches the request
             * we (the Responder) already have set up. The rules are:
             * - The discovery session (pub/sub ID) must match.
             * - The peer MAC address (if specified - i.e. non-null) must match. A null peer MAC ==
             *   accept (otherwise matching) requests from any peer MAC.
             * - The request must be pending (i.e. we could have completed requests for the same
             *   parameters)
             */
            if (requestInfo.pubSubId != 0 && requestInfo.pubSubId != pubSubId) {
                continue;
            }

            if (requestInfo.specifiedPeerDiscoveryMac != null) {
                if (Arrays.equals(requestInfo.specifiedPeerDiscoveryMac, mac) && requestInfo.state
                        == AwareNetworkRequestInformation.STATE_RESPONDER_WAIT_FOR_REQUEST) {
                    // If a peer specific request matches, use it.
                    networkSpecifier = mNetworkRequestsCache.keyAt(i);
                    nnri = requestInfo;
                    break;
                }
                continue;
            }
            // For Accept any, multiple NDP may setup in the same time. In idle or terminating state
            // it will not accept any request.
            if (requestInfo.state != AwareNetworkRequestInformation.STATE_IDLE
                    && requestInfo.state != AwareNetworkRequestInformation.STATE_TERMINATING) {
                // If an accepts any request matches, continually find if there is a peer specific
                // one. If there isn't any matched peer specific one, use the accepts any peer
                // request.
                networkSpecifier = mNetworkRequestsCache.keyAt(i);
                nnri = requestInfo;
            }
        }

        if (nnri == null) {
            Log.w(TAG, "onDataPathRequest: can't find a request with specified pubSubId=" + pubSubId
                    + ", mac=" + String.valueOf(HexEncoding.encode(mac)));
            if (sVdbg) {
                Log.v(TAG, "onDataPathRequest: network request cache = " + mNetworkRequestsCache);
            }
            mMgr.respondToDataPathRequest(false, ndpId, "", null, false, null);
            return false;
        }

        if (nnri.interfaceName == null) {
            nnri.interfaceName = selectInterfaceForRequest(nnri);
        }
        if (nnri.interfaceName == null) {
            Log.w(TAG,
                    "onDataPathRequest: request " + networkSpecifier + " no interface available");
            mMgr.respondToDataPathRequest(false, ndpId, "", null, false, null);
            mNetworkRequestsCache.remove(networkSpecifier);
            mNetworkFactory.letAppKnowThatRequestsAreUnavailable(nnri);
            return false;
        }

        NdpInfo ndpInfo = new NdpInfo(ndpId);
        ndpInfo.state = NdpInfo.STATE_RESPONDER_WAIT_FOR_RESPOND_RESPONSE;
        ndpInfo.peerDiscoveryMac = mac;
        ndpInfo.startTimestamp = mClock.getElapsedSinceBootMillis();
        nnri.ndpInfos.put(ndpId, ndpInfo);

        nnri.state = AwareNetworkRequestInformation.STATE_IN_SETUP;
        mMgr.respondToDataPathRequest(true, ndpId, nnri.interfaceName,
                NetworkInformationData.buildTlv(nnri.networkSpecifier.port,
                        nnri.networkSpecifier.transportProtocol),
                nnri.networkSpecifier.isOutOfBand(),
                nnri.networkSpecifier);

        return true;
    }

    /**
     * Called on the RESPONDER when the response to data-path request has been completed.
     *
     * @param ndpId The ID of the data-path (NDP)
     * @param success Whether or not the 'RespondToDataPathRequest' operation was a success.
     * @return true if framework start to waiting for the confirm
     */
    public boolean onRespondToDataPathRequest(int ndpId, boolean success, int reasonOnFailure) {
        if (mVerboseLoggingEnabled) {
            Log.v(TAG, "onRespondToDataPathRequest: ndpId=" + ndpId + ", success=" + success);
        }
        Map.Entry<WifiAwareNetworkSpecifier, AwareNetworkRequestInformation> nnriE =
                getNetworkRequestByNdpId(ndpId);

        if (nnriE == null) {
            Log.w(TAG, "onRespondToDataPathRequest: can't find a request with specified ndpId="
                    + ndpId);
            if (sVdbg) {
                Log.v(TAG, "onRespondToDataPathRequest: network request cache = "
                        + mNetworkRequestsCache);
            }
            return false;
        }

        WifiAwareNetworkSpecifier networkSpecifier = nnriE.getKey();
        AwareNetworkRequestInformation nnri = nnriE.getValue();
        NdpInfo ndpInfo = nnri.ndpInfos.get(ndpId);
        if (!success) {
            Log.w(TAG, "onRespondToDataPathRequest: request " + networkSpecifier
                    + " failed responding");
            mMgr.endDataPath(ndpId);
            nnri.ndpInfos.remove(ndpId);
            if (nnri.specifiedPeerDiscoveryMac != null) {
                mNetworkRequestsCache.remove(networkSpecifier);
                mNetworkFactory.letAppKnowThatRequestsAreUnavailable(nnri);
            }
            mAwareMetrics.recordNdpStatus(reasonOnFailure, networkSpecifier.isOutOfBand(),
                    nnri.networkSpecifier.role, ndpInfo.startTimestamp, networkSpecifier.sessionId);
            return false;
        }

        if (ndpInfo.state != NdpInfo.STATE_RESPONDER_WAIT_FOR_RESPOND_RESPONSE
                || nnri.state == AwareNetworkRequestInformation.STATE_TERMINATING) {
            Log.w(TAG, "onRespondToDataPathRequest: request " + networkSpecifier
                    + " is incorrect state=" + nnri.state);
            mMgr.endDataPath(ndpId);
            nnri.ndpInfos.remove(ndpId);
            if (nnri.specifiedPeerDiscoveryMac != null) {
                mNetworkRequestsCache.remove(networkSpecifier);
                mNetworkFactory.letAppKnowThatRequestsAreUnavailable(nnri);
            }
            return false;
        }

        ndpInfo.state = NdpInfo.STATE_WAIT_FOR_CONFIRM;
        return true;
    }

    /**
     * Notification (unsolicited/asynchronous) that the data-path (which we've been setting up)
     * is possibly (if {@code accept} is {@code true}) ready for use from the firmware's
     * perspective - now can do L3 configuration.
     *
     * @param ndpId         Id of the data-path
     * @param mac           The MAC address of the peer's data-path (not discovery interface). Only
     *                      valid
     *                      if {@code accept} is {@code true}.
     * @param accept        Indicates whether the data-path setup has succeeded (been accepted) or
     *                      failed (been rejected).
     * @param reason        If {@code accept} is {@code false} provides a reason code for the
     *                      rejection/failure.
     * @param message       The message provided by the peer as part of the data-path setup
     *                      process.
     * @param channelInfo   Lists of channels used for this NDP.
     * @return False if has error, otherwise return true
     */
    public boolean onDataPathConfirm(int ndpId, byte[] mac, boolean accept,
            int reason, byte[] message, List<WifiAwareChannelInfo> channelInfo) {
        if (mVerboseLoggingEnabled) {
            Log.v(TAG, "onDataPathConfirm: ndpId=" + ndpId
                    + ", mac=" + String.valueOf(HexEncoding.encode(mac))
                    + ", accept=" + accept + ", reason=" + reason
                    + ", message.length=" + ((message == null) ? 0 : message.length)
                    + ", channelInfo=" + channelInfo);
        }

        Map.Entry<WifiAwareNetworkSpecifier, AwareNetworkRequestInformation> nnriE =
                getNetworkRequestByNdpId(ndpId);
        if (nnriE == null) {
            Log.w(TAG, "onDataPathConfirm: network request not found for ndpId=" + ndpId);
            if (accept) {
                mMgr.endDataPath(ndpId);
            }
            return false;
        }

        WifiAwareNetworkSpecifier networkSpecifier = nnriE.getKey();
        AwareNetworkRequestInformation nnri = nnriE.getValue();
        NdpInfo ndpInfo = nnri.ndpInfos.get(ndpId);

        // validate state
        if (ndpInfo.state != NdpInfo.STATE_WAIT_FOR_CONFIRM
                || nnri.state == AwareNetworkRequestInformation.STATE_TERMINATING) {
            Log.w(TAG, "onDataPathConfirm: invalid state=" + nnri.state);
            nnri.ndpInfos.remove(ndpId);
            if (nnri.specifiedPeerDiscoveryMac != null) {
                mNetworkRequestsCache.remove(networkSpecifier);
                mNetworkFactory.letAppKnowThatRequestsAreUnavailable(nnri);
            }
            if (accept) {
                mMgr.endDataPath(ndpId);
            }
            return false;
        }

        if (accept) {
            ndpInfo.peerDataMac = mac;
            ndpInfo.state = NdpInfo.STATE_CONFIRMED;
            ndpInfo.channelInfos = channelInfo;
            nnri.state = AwareNetworkRequestInformation.STATE_CONFIRMED;
            // NetworkAgent may already be created for accept any peer request, interface should be
            // ready in that case.
            if (nnri.networkAgent == null && !isInterfaceUpAndUsedByAnotherNdp(nnri)) {
                try {
                    mNetdWrapper.setInterfaceUp(nnri.interfaceName);
                    mNetdWrapper.enableIpv6(nnri.interfaceName);
                } catch (Exception e) { // NwService throws runtime exceptions for errors
                    Log.e(TAG, "onDataPathConfirm: ACCEPT nnri=" + nnri
                                + ": can't configure network - "
                                + e);
                    mMgr.endDataPath(ndpId);
                    if (nnri.specifiedPeerDiscoveryMac != null) {
                        declareUnfullfillable(nnri);
                    }
                    return true;
                }
            } else {
                if (sVdbg) {
                    Log.v(TAG, "onDataPathConfirm: interface already configured: "
                            + nnri.interfaceName);
                }
            }

            // only relevant for the initiator
            if (nnri.networkSpecifier.role
                    == WifiAwareManager.WIFI_AWARE_DATA_PATH_ROLE_INITIATOR) {
                NetworkInformationData.ParsedResults peerServerInfo =
                        NetworkInformationData.parseTlv(message);
                if (peerServerInfo != null) {
                    if (peerServerInfo.port != 0) {
                        ndpInfo.peerPort = peerServerInfo.port;
                    }
                    if (peerServerInfo.transportProtocol != -1) {
                        ndpInfo.peerTransportProtocol = peerServerInfo.transportProtocol;
                    }
                    if (peerServerInfo.ipv6Override != null) {
                        ndpInfo.peerIpv6Override = peerServerInfo.ipv6Override;
                    }
                }
            }

            nnri.startValidationTimestamp = mClock.getElapsedSinceBootMillis();
            handleAddressValidation(nnri, ndpInfo, networkSpecifier.isOutOfBand(), mac);
        } else {
            if (sVdbg) {
                Log.v(TAG, "onDataPathConfirm: data-path for networkSpecifier=" + networkSpecifier
                        + " rejected - reason=" + reason);
            }
            nnri.ndpInfos.remove(ndpId);
            if (nnri.specifiedPeerDiscoveryMac != null) {
                mNetworkRequestsCache.remove(networkSpecifier);
                mNetworkFactory.letAppKnowThatRequestsAreUnavailable(nnri);
            }
            mAwareMetrics.recordNdpStatus(reason, networkSpecifier.isOutOfBand(),
                    networkSpecifier.role, ndpInfo.startTimestamp, networkSpecifier.sessionId);
        }
        return true;
    }

    private void getInet6Address(NdpInfo ndpInfo, byte[] mac, String interfaceName) {
        try {
            byte[] addr;
            if (ndpInfo.peerIpv6Override == null) {
                addr = MacAddress.fromBytes(mac).getLinkLocalIpv6FromEui48Mac().getAddress();
            } else {
                addr = new byte[16];
                addr[0] = (byte) 0xfe;
                addr[1] = (byte) 0x80;
                addr[8] = ndpInfo.peerIpv6Override[0];
                addr[9] = ndpInfo.peerIpv6Override[1];
                addr[10] = ndpInfo.peerIpv6Override[2];
                addr[11] = ndpInfo.peerIpv6Override[3];
                addr[12] = ndpInfo.peerIpv6Override[4];
                addr[13] = ndpInfo.peerIpv6Override[5];
                addr[14] = ndpInfo.peerIpv6Override[6];
                addr[15] = ndpInfo.peerIpv6Override[7];
            }
            ndpInfo.peerIpv6 = Inet6Address.getByAddress(null, addr,
                    NetworkInterface.getByName(interfaceName));
        } catch (SocketException | UnknownHostException e) {
            if (mVerboseLoggingEnabled) {
                Log.d(TAG, "onDataPathConfirm: error obtaining scoped IPv6 address -- " + e);
            }
            ndpInfo.peerIpv6 = null;
        }
    }

    private void handleAddressValidation(AwareNetworkRequestInformation nnri, NdpInfo ndpInfo,
            boolean isOutOfBand, byte[] mac) {
        final NetworkCapabilities.Builder ncBuilder = new NetworkCapabilities.Builder(
                sNetworkCapabilitiesFilter);
        LinkProperties linkProperties = new LinkProperties();
        getInet6Address(ndpInfo, mac, nnri.interfaceName);
        if (!(ndpInfo.peerIpv6 != null && mNiWrapper.configureAgentProperties(nnri,
                ncBuilder, linkProperties)
                && mNiWrapper.isAddressUsable(linkProperties))) {
            if (sVdbg) {
                Log.d(TAG, "Failed address validation");
            }
            if (!isAddressValidationExpired(nnri, ndpInfo.ndpId)) {
                Object token = mDelayNetworkValidationMap.get(ndpInfo.ndpId);
                if (token == null) {
                    token = new Object();
                    mDelayNetworkValidationMap.put(ndpInfo.ndpId, token);
                }
                mHandler.postDelayed(() ->
                        handleAddressValidation(nnri, ndpInfo, isOutOfBand, mac),
                        token, ADDRESS_VALIDATION_RETRY_INTERVAL_MS);
            }
            return;
        }
        mDelayNetworkValidationMap.remove(ndpInfo.ndpId);

        // Network agent may already setup finished. Update peer network info.
        if (nnri.networkAgent == null) {
            // Setup first NDP for new networkAgent.
            final WifiAwareNetworkInfo ni = new WifiAwareNetworkInfo(ndpInfo.peerIpv6,
                    ndpInfo.peerPort, ndpInfo.peerTransportProtocol,
                    ndpInfo.channelInfos);
            ncBuilder.setTransportInfo(ni);
            if (mVerboseLoggingEnabled) {
                Log.v(TAG, "onDataPathConfirm: AwareNetworkInfo=" + ni);
            }
            final NetworkAgentConfig naConfig = new NetworkAgentConfig.Builder()
                    .setLegacyType(ConnectivityManager.TYPE_NONE)
                    .setLegacyTypeName(NETWORK_TAG)
                    .build();
            nnri.networkAgent = new WifiAwareNetworkAgent(mLooper, mContext,
                    AGENT_TAG_PREFIX + ndpInfo.ndpId, ncBuilder.build(), linkProperties,
                    NETWORK_FACTORY_SCORE_AVAIL, naConfig, mNetworkFactory.getProvider(), nnri);
            mNiWrapper.setConnected(nnri.networkAgent);
        }
        int channelFreqMHz = (ndpInfo.channelInfos != null && !ndpInfo.channelInfos.isEmpty())
                    ? ndpInfo.channelInfos.get(0).getChannelFrequencyMhz() : 0;
        mAwareMetrics.recordNdpStatus(NanStatusCode.SUCCESS, isOutOfBand,
                nnri.networkSpecifier.role, ndpInfo.startTimestamp, nnri.networkSpecifier.sessionId,
                channelFreqMHz);
        mAwareMetrics.recordNdpCreation(nnri.uid, nnri.packageName, mNetworkRequestsCache);
    }

    private boolean isAddressValidationExpired(AwareNetworkRequestInformation nnri, int ndpId) {
        if (mClock.getElapsedSinceBootMillis() - nnri.startValidationTimestamp
                > ADDRESS_VALIDATION_TIMEOUT_MS) {
            Log.e(TAG, "Timed-out while waiting for IPv6 address to be usable");
            mMgr.endDataPath(ndpId);
            mDelayNetworkValidationMap.remove(ndpId);
            if (nnri.specifiedPeerDiscoveryMac != null) {
                declareUnfullfillable(nnri);
            }
            return true;
        }
        return false;
    }

    private void declareUnfullfillable(AwareNetworkRequestInformation nnri) {
        mNetworkFactory.letAppKnowThatRequestsAreUnavailable(nnri);
        nnri.state = AwareNetworkRequestInformation.STATE_TERMINATING;
    }

    /**
     * Notification (unsolicited/asynchronous) from the firmware that the specified data-path has
     * been terminated.
     *
     * @param ndpId The ID of the terminated data-path.
     */
    public void onDataPathEnd(int ndpId) {
        if (mVerboseLoggingEnabled) {
            Log.v(TAG, "onDataPathEnd: ndpId=" + ndpId);
        }

        cleanNetworkValidationTask(ndpId);

        Map.Entry<WifiAwareNetworkSpecifier, AwareNetworkRequestInformation> nnriE =
                getNetworkRequestByNdpId(ndpId);
        if (nnriE == null) {
            if (sVdbg) {
                Log.v(TAG, "onDataPathEnd: network request not found for ndpId=" + ndpId);
            }
            return;
        }
        AwareNetworkRequestInformation nnri = nnriE.getValue();
        NdpInfo ndpInfo = nnri.ndpInfos.get(ndpId);
        nnri.ndpInfos.remove(ndpId);

        if (ndpInfo.state == NdpInfo.STATE_CONFIRMED) {
            mAwareMetrics.recordNdpSessionDuration(ndpInfo.startTimestamp);
        }
        if (nnri.specifiedPeerDiscoveryMac == null
                && nnri.state != AwareNetworkRequestInformation.STATE_TERMINATING) {
            return;
        }
        if (nnri.ndpInfos.size() == 0) {
            tearDownInterfaceIfPossible(nnri);
            mNetworkRequestsCache.remove(nnriE.getKey());
            mNetworkFactory.tickleConnectivityIfWaiting();
        }
    }

    /**
     * Notification (unsolicited/asynchronous) from the firmware that the channel for the specified
     * NDP ids has been updated.
     */
    public void onDataPathSchedUpdate(byte[] peerMac, List<Integer> ndpIds,
            List<WifiAwareChannelInfo> channelInfo) {
        if (mVerboseLoggingEnabled) {
            Log.v(TAG, "onDataPathSchedUpdate: peerMac=" + MacAddress.fromBytes(peerMac).toString()
                    + ", ndpIds=" + ndpIds + ", channelInfo=" + channelInfo);
        }

        for (int ndpId : ndpIds) {
            Map.Entry<WifiAwareNetworkSpecifier, AwareNetworkRequestInformation> nnriE =
                    getNetworkRequestByNdpId(ndpId);
            if (nnriE == null) {
                Log.e(TAG, "onDataPathSchedUpdate: ndpId=" + ndpId + " - not found");
                continue;
            }
            NdpInfo ndpInfo = nnriE.getValue().ndpInfos.get(ndpId);
            if (!Arrays.equals(peerMac, ndpInfo.peerDiscoveryMac)) {
                Log.e(TAG, "onDataPathSchedUpdate: ndpId=" + ndpId + ", report NMI="
                        + MacAddress.fromBytes(peerMac).toString() + " doesn't match NDP NMI="
                        + MacAddress.fromBytes(ndpInfo.peerDiscoveryMac).toString());
                continue;
            }

            ndpInfo.channelInfos = channelInfo;
        }
    }

    /**
     * Called whenever Aware comes down. Clean up all pending and up network requests and agents.
     */
    public void onAwareDownCleanupDataPaths() {
        Log.d(TAG, "onAwareDownCleanupDataPaths");

        Iterator<Map.Entry<WifiAwareNetworkSpecifier, AwareNetworkRequestInformation>> it =
                mNetworkRequestsCache.entrySet().iterator();
        while (it.hasNext()) {
            AwareNetworkRequestInformation nnri = it.next().getValue();
            nnri.state = AwareNetworkRequestInformation.STATE_TERMINATING;
            tearDownInterfaceIfPossible(nnri);
            it.remove();
        }
    }

    /**
     * Called when timed-out waiting for confirmation of the data-path setup (i.e.
     * onDataPathConfirm). Started on the initiator when executing the request for the data-path
     * and on the responder when received a request for data-path (in both cases only on success
     * - i.e. when we're proceeding with data-path setup).
     */
    public void handleDataPathTimeout(int ndpId) {
        if (mVerboseLoggingEnabled) Log.v(TAG, "handleDataPathTimeout: networkSpecifier=" + ndpId);


        Map.Entry<WifiAwareNetworkSpecifier, AwareNetworkRequestInformation> nnriE =
                getNetworkRequestByNdpId(ndpId);
        if (nnriE == null) {
            if (sVdbg) {
                Log.v(TAG,
                        "handleDataPathTimeout: network request not found for networkSpecifier="
                                + ndpId);
            }
            return;
        }
        AwareNetworkRequestInformation nnri = nnriE.getValue();
        NdpInfo ndpInfo = nnri.ndpInfos.get(ndpId);
        mAwareMetrics.recordNdpStatus(NanStatusCode.INTERNAL_FAILURE,
                nnri.networkSpecifier.isOutOfBand(), nnri.networkSpecifier.role,
                ndpInfo.startTimestamp, nnri.networkSpecifier.sessionId);
        mMgr.endDataPath(ndpId);
        nnri.ndpInfos.remove(ndpId);
        if (nnri.specifiedPeerDiscoveryMac != null) {
            mNetworkFactory.letAppKnowThatRequestsAreUnavailable(nnri);
            mNetworkRequestsCache.remove(nnri.networkSpecifier);
            nnri.state = AwareNetworkRequestInformation.STATE_TERMINATING;
        }
    }

    private class WifiAwareNetworkFactory extends NetworkFactory {
        // Request received while waiting for confirmation that a canonically identical data-path
        // (NDP) is in the process of being terminated
        private boolean mWaitingForTermination = false;

        WifiAwareNetworkFactory(Looper looper, Context context, NetworkCapabilities filter) {
            super(looper, context, NETWORK_TAG, filter);
        }

        public void tickleConnectivityIfWaiting() {
            if (mWaitingForTermination) {
                if (sVdbg) Log.v(TAG, "tickleConnectivityIfWaiting: was waiting!");
                mWaitingForTermination = false;
                reevaluateAllRequests();
            }
        }

        @Override
        public boolean acceptRequest(NetworkRequest request) {
            if (mVerboseLoggingEnabled) {
                Log.v(TAG, "WifiAwareNetworkFactory.acceptRequest: request=" + request);
            }

            NetworkSpecifier networkSpecifierBase = request.getNetworkSpecifier();
            if (!(networkSpecifierBase instanceof WifiAwareNetworkSpecifier)) {
                Log.w(TAG, "WifiAwareNetworkFactory.acceptRequest: request=" + request
                        + " - not a WifiAwareNetworkSpecifier");
                return false;
            }

            if (!mMgr.isUsageEnabled()) {
                if (sVdbg) {
                    Log.v(TAG, "WifiAwareNetworkFactory.acceptRequest: request=" + request
                            + " -- Aware disabled");
                }
                releaseRequestAsUnfulfillableByAnyFactory(request);
                return false;
            }

            if (mInterfaces.isEmpty()) {
                Log.w(TAG, "WifiAwareNetworkFactory.acceptRequest: request=" + request
                        + " -- No Aware interfaces are up");
                releaseRequestAsUnfulfillableByAnyFactory(request);
                return false;
            }

            WifiAwareNetworkSpecifier networkSpecifier =
                    (WifiAwareNetworkSpecifier) networkSpecifierBase;

            // look up specifier - are we being called again?
            AwareNetworkRequestInformation nnri = mNetworkRequestsCache.get(networkSpecifier);
            if (nnri != null) {
                if (sVdbg) {
                    Log.v(TAG, "WifiAwareNetworkFactory.acceptRequest: request=" + request
                            + " - already in cache with state=" + nnri.state);
                }

                if (nnri.state == AwareNetworkRequestInformation.STATE_TERMINATING) {
                    mWaitingForTermination = true;
                    return false;
                }

                // seems to happen after a network agent is created - trying to rematch all
                // requests again!?
                return true;
            }

            nnri = AwareNetworkRequestInformation.processNetworkSpecifier(request, networkSpecifier,
                    mMgr, mWifiPermissionsUtil, mPermissionsWrapper,
                    mAllowNdpResponderFromAnyOverride);
            if (nnri == null) {
                Log.e(TAG, "WifiAwareNetworkFactory.acceptRequest: request=" + request
                        + " - can't parse network specifier");
                releaseRequestAsUnfulfillableByAnyFactory(request);
                return false;
            }

            // check to see if a canonical version exists
            Map.Entry<WifiAwareNetworkSpecifier, AwareNetworkRequestInformation> primaryRequest =
                    getNetworkRequestByCanonicalDescriptor(nnri.getCanonicalDescriptor());
            if (primaryRequest != null) {
                if (mVerboseLoggingEnabled) {
                    Log.v(TAG, "WifiAwareNetworkFactory.acceptRequest: request=" + request
                            + ", already has a primary request=" + primaryRequest.getKey()
                            + " with state=" + primaryRequest.getValue().state);
                }

                if (primaryRequest.getValue().state
                        == AwareNetworkRequestInformation.STATE_TERMINATING) {
                    mWaitingForTermination = true;
                } else {
                    primaryRequest.getValue().updateToSupportNewRequest(request);
                }
                return false;
            }

            mNetworkRequestsCache.put(networkSpecifier, nnri);
            mAwareMetrics.recordNdpRequestType(networkSpecifier.type);

            return true;
        }

        @Override
        protected void needNetworkFor(NetworkRequest networkRequest) {
            if (mVerboseLoggingEnabled) {
                Log.v(TAG, "WifiAwareNetworkFactory.needNetworkFor: networkRequest=");
            }

            NetworkSpecifier networkSpecifierObj = networkRequest.getNetworkSpecifier();
            WifiAwareNetworkSpecifier networkSpecifier = null;
            if (networkSpecifierObj instanceof WifiAwareNetworkSpecifier) {
                networkSpecifier = (WifiAwareNetworkSpecifier) networkSpecifierObj;
            }
            AwareNetworkRequestInformation nnri = mNetworkRequestsCache.get(networkSpecifier);
            if (nnri == null) {
                Log.e(TAG, "WifiAwareNetworkFactory.needNetworkFor: networkRequest="
                        + networkRequest + " not in cache!?");
                return;
            }

            if (nnri.state != AwareNetworkRequestInformation.STATE_IDLE) {
                if (sVdbg) {
                    Log.v(TAG, "WifiAwareNetworkFactory.needNetworkFor: networkRequest="
                            + networkRequest + " - already in progress");
                    // TODO: understand how/when can be called again/while in progress (seems
                    // to be related to score re-calculation after a network agent is created)
                }
                return;
            }
            if (nnri.networkSpecifier.role
                    == WifiAwareManager.WIFI_AWARE_DATA_PATH_ROLE_INITIATOR) {
                nnri.interfaceName = selectInterfaceForRequest(nnri);
                if (nnri.interfaceName == null) {
                    Log.w(TAG, "needNetworkFor: request " + networkSpecifier
                            + " no interface available");
                    mNetworkRequestsCache.remove(networkSpecifier);
                    letAppKnowThatRequestsAreUnavailable(nnri);
                    return;
                }

                int channel = selectChannelForRequest(nnri);
                int channelRequestType = NanDataPathChannelCfg.CHANNEL_NOT_REQUESTED;
                if (mContext.getResources().getBoolean(R.bool.config_wifiSupportChannelOnDataPath)
                        && nnri.networkSpecifier.getChannelFrequencyMhz() != 0) {
                    channel = nnri.networkSpecifier.getChannelFrequencyMhz();
                    channelRequestType = nnri.networkSpecifier.isChannelRequired()
                            ? NanDataPathChannelCfg.FORCE_CHANNEL_SETUP
                            : NanDataPathChannelCfg.REQUEST_CHANNEL_SETUP;
                }
                mMgr.initiateDataPathSetup(networkSpecifier, nnri.specifiedPeerInstanceId,
                        channelRequestType, channel,
                        nnri.specifiedPeerDiscoveryMac, nnri.interfaceName,
                        nnri.networkSpecifier.isOutOfBand(), null
                );
                nnri.state =
                        AwareNetworkRequestInformation.STATE_INITIATOR_WAIT_FOR_REQUEST_RESPONSE;
            } else {
                nnri.state = AwareNetworkRequestInformation.STATE_RESPONDER_WAIT_FOR_REQUEST;
            }
        }

        @Override
        protected void releaseNetworkFor(NetworkRequest networkRequest) {
            if (mVerboseLoggingEnabled) {
                Log.v(TAG, "WifiAwareNetworkFactory.releaseNetworkFor: networkRequest="
                        + networkRequest);
            }

            NetworkSpecifier networkSpecifierObj = networkRequest.getNetworkSpecifier();
            WifiAwareNetworkSpecifier networkSpecifier = null;
            if (networkSpecifierObj instanceof WifiAwareNetworkSpecifier) {
                networkSpecifier = (WifiAwareNetworkSpecifier) networkSpecifierObj;
            }

            AwareNetworkRequestInformation nnri = mNetworkRequestsCache.get(networkSpecifier);
            if (nnri == null) {
                Log.e(TAG, "WifiAwareNetworkFactory.releaseNetworkFor: networkRequest="
                        + networkRequest + " not in cache!?");
                return;
            }

            if (nnri.networkAgent != null) {
                if (sVdbg) {
                    Log.v(TAG, "WifiAwareNetworkFactory.releaseNetworkFor: networkRequest="
                            + networkRequest + ", nnri=" + nnri
                            + ": agent already created - deferring ending data-path to agent"
                            + ".unwanted()");
                }
                return;
            }

            /*
             * Since there's no agent it means we're in the process of setting up the NDP.
             * However, it is possible that there were other equivalent requests for this NDP. We
             * should keep going in that case.
             */
            nnri.removeSupportForRequest(networkRequest);
            if (nnri.equivalentRequests.isEmpty()) {
                if (mVerboseLoggingEnabled) {
                    Log.v(TAG, "releaseNetworkFor: there are no further requests, networkRequest="
                            + networkRequest);
                }
                if (nnri.ndpInfos.size() != 0) {
                    if (sVdbg) Log.v(TAG, "releaseNetworkFor: in progress NDP being terminated");
                    for (int index = 0; index < nnri.ndpInfos.size(); index++) {
                        int ndpId = nnri.ndpInfos.keyAt(index);
                        cleanNetworkValidationTask(ndpId);
                        mMgr.endDataPath(ndpId);
                    }
                    nnri.state = AwareNetworkRequestInformation.STATE_TERMINATING;
                } else {
                    mNetworkRequestsCache.remove(networkSpecifier);
                }
            } else {
                if (sVdbg) {
                    Log.v(TAG, "releaseNetworkFor: equivalent requests exist - not terminating "
                            + "networkRequest=" + networkRequest);
                }
            }
        }

        void letAppKnowThatRequestsAreUnavailable(AwareNetworkRequestInformation nnri) {
            for (NetworkRequest nr : nnri.equivalentRequests) {
                releaseRequestAsUnfulfillableByAnyFactory(nr);
            }
        }
    }

    /**
     * Network agent for Wi-Fi Aware.
     */
    @VisibleForTesting
    public class WifiAwareNetworkAgent extends NetworkAgent {
        private final AwareNetworkRequestInformation mAwareNetworkRequestInfo;
        @VisibleForTesting
        public final NetworkCapabilities mDataPathCapabilities;

        WifiAwareNetworkAgent(Looper looper, Context context, String logTag,
                NetworkCapabilities nc, LinkProperties lp, int score,
                NetworkAgentConfig config, NetworkProvider provider,
                AwareNetworkRequestInformation anri) {
            super(context, looper, logTag, nc, lp, score, config, provider);
            mAwareNetworkRequestInfo = anri;
            mDataPathCapabilities = nc;
            register();
        }

        @Override
        public void onNetworkUnwanted() {
            if (mVerboseLoggingEnabled) {
                Log.v(TAG, "WifiAwareNetworkAgent.unwanted: request=" + mAwareNetworkRequestInfo);
            }
            for (int index = 0; index < mAwareNetworkRequestInfo.ndpInfos.size(); index++) {
                mMgr.endDataPath(mAwareNetworkRequestInfo.ndpInfos.keyAt(index));
            }
            mAwareNetworkRequestInfo.state = AwareNetworkRequestInformation.STATE_TERMINATING;

            // Will get a callback (on both initiator and responder) when data-path actually
            // terminated. At that point will inform the agent and will clear the cache.
        }

        void reconfigureAgentAsDisconnected() {
            if (mVerboseLoggingEnabled) {
                Log.v(TAG, "WifiAwareNetworkAgent.reconfigureAgentAsDisconnected: request="
                        + mAwareNetworkRequestInfo);
            }
            unregister();
        }
    }

    private void tearDownInterfaceIfPossible(AwareNetworkRequestInformation nnri) {
        if (mVerboseLoggingEnabled) {
            Log.v(TAG, "tearDownInterfaceIfPossible: nnri=" + nnri);
        }

        if (!TextUtils.isEmpty(nnri.interfaceName)) {
            boolean interfaceUsedByAnotherNdp = isInterfaceUpAndUsedByAnotherNdp(nnri);
            if (interfaceUsedByAnotherNdp) {
                if (mVerboseLoggingEnabled) {
                    Log.v(TAG, "tearDownInterfaceIfPossible: interfaceName=" + nnri.interfaceName
                            + ", still in use - not turning down");
                }
            } else {
                try {
                    mNetdWrapper.setInterfaceDown(nnri.interfaceName);
                } catch (Exception e) { // NwService throws runtime exceptions for errors
                    Log.e(TAG, "tearDownInterfaceIfPossible: nnri=" + nnri
                            + ": can't bring interface down - " + e);
                }
            }
        }

        if (nnri.networkAgent == null) {
            mNetworkFactory.letAppKnowThatRequestsAreUnavailable(nnri);
        } else {
            nnri.networkAgent.reconfigureAgentAsDisconnected();
        }
    }

    private boolean isInterfaceUpAndUsedByAnotherNdp(AwareNetworkRequestInformation nri) {
        for (AwareNetworkRequestInformation lnri : mNetworkRequestsCache.values()) {
            if (lnri == nri) {
                continue;
            }

            if (nri.interfaceName.equals(lnri.interfaceName) && (
                    lnri.state == AwareNetworkRequestInformation.STATE_CONFIRMED
                            || lnri.state == AwareNetworkRequestInformation.STATE_TERMINATING)) {
                return true;
            }
        }

        return false;
    }

    /**
     * Select one of the existing interfaces for the new network request. A request is canonical
     * (otherwise it wouldn't be executed).
     *
     * Criteria:
     * 1. If the request peer is already setup on an interface, if security update is enabled
     * (based on a device overlay) that interface will be used, otherwise must select an unused
     * interface for the new request.
     * 2. Select a network interface which is unused. This is because the network stack does not
     * support multiple networks per interface.
     * 3. If no network interface is available then (based on a device overlay) either fail (new
     * behavior) or (preserve legacy behavior) pick an interface which isn't used for
     * communication to the same peer.
     */
    private String selectInterfaceForRequest(AwareNetworkRequestInformation req) {
        SortedSet<String> unused = new TreeSet<>(mInterfaces);
        Set<String> invalid = new HashSet<>();
        SortedSet<String> inuse = new TreeSet<>();

        if (mVerboseLoggingEnabled) {
            Log.v(TAG, "selectInterfaceForRequest: req=" + req + ", mNetworkRequestsCache="
                    + mNetworkRequestsCache);
        }

        for (AwareNetworkRequestInformation nnri : mNetworkRequestsCache.values()) {
            if (nnri == req || nnri.interfaceName == null) {
                continue;
            }

            if (Arrays.equals(req.specifiedPeerDiscoveryMac, nnri.specifiedPeerDiscoveryMac)) {
                if (mContext.getResources()
                        .getBoolean(R.bool.config_wifiAwareNdpSecurityUpdateOnSameNdi)) {
                    if (mVerboseLoggingEnabled) {
                        Log.v(TAG, "Using the same NDI to the same peer with security upgrade "
                                + "feature enabled.");
                    }
                    return nnri.interfaceName;
                }
                invalid.add(nnri.interfaceName);
            } else {
                inuse.add(nnri.interfaceName);
            }
            unused.remove(nnri.interfaceName);
        }

        if (sVdbg) {
            Log.v(TAG, "selectInterfaceForRequest: unUsed=" + unused + ", inuse=" + inuse
                    + ", invalid" + invalid + ", allInterfaces" + mInterfaces);
        }

        // If any interface is unused, pick it first
        if (!unused.isEmpty()) {
            return unused.first();
        }

        // If device doesn't allow to make multiple network on same interface, return null.
        if (!mContext.getResources()
                .getBoolean(R.bool.config_wifiAllowMultipleNetworksOnSameAwareNdi)) {
            Log.e(TAG, "selectInterfaceForRequest: req=" + req + " - no interfaces available!");
            return null;
        }

        for (String iface : inuse) {
            if (!invalid.contains(iface)) {
                return iface;
            }
        }

        Log.e(TAG, "selectInterfaceForRequest: req=" + req + " - no interfaces available!");
        return null;
    }

    /**
     * Select a channel for the network request.
     *
     * TODO (b/38209409): The value from this function isn't currently used - the channel selection
     * is delegated to the HAL.
     */
    private int selectChannelForRequest(AwareNetworkRequestInformation req) {
        return 2437;
    }

    private static class NdpInfo {
        static final int STATE_WAIT_FOR_CONFIRM = 107;
        static final int STATE_CONFIRMED = 108;
        static final int STATE_RESPONDER_WAIT_FOR_RESPOND_RESPONSE = 109;

        public int state;

        public byte[] peerDiscoveryMac = null;
        public int peerInstanceId = 0;
        public int ndpId = 0; // 0 is never a valid ID!
        public byte[] peerDataMac;
        public Inet6Address peerIpv6;
        public int peerPort = 0; // uninitialized (invalid) value
        public int peerTransportProtocol = -1; // uninitialized (invalid) value
        public byte[] peerIpv6Override = null;
        public List<WifiAwareChannelInfo> channelInfos;
        public long startTimestamp = 0; // request is made (initiator) / get request (responder)

        NdpInfo(int ndpId) {
            this.ndpId = ndpId;
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append(", ndpInfo[");
            sb.append("ndpId=").append(ndpId).append(", peerInstanceId=").append(
                    peerInstanceId).append(", peerDiscoveryMac=").append(
                    peerDiscoveryMac == null ? ""
                            : String.valueOf(HexEncoding.encode(peerDiscoveryMac)))
                    .append(", peerDataMac=").append(
                    peerDataMac == null ? ""
                            : String.valueOf(HexEncoding.encode(peerDataMac)))
                    .append(", peerIpv6=").append(peerIpv6).append(
                    ", peerPort=").append(
                    peerPort).append(", peerTransportProtocol=").append(
                    peerTransportProtocol).append(", startTimestamp=").append(
                    startTimestamp).append(", channelInfo=").append(
                            channelInfos);
            sb.append("]");
            return sb.toString();
        }
    }

    /**
     * Aware network request. State object: contains network request information/state through its
     * lifetime.
     */
    @VisibleForTesting
    public static class AwareNetworkRequestInformation {
        static final int STATE_IDLE = 100;
        static final int STATE_CONFIRMED = 101;
        static final int STATE_INITIATOR_WAIT_FOR_REQUEST_RESPONSE = 102;
        static final int STATE_RESPONDER_WAIT_FOR_REQUEST = 103;
        static final int STATE_TERMINATING = 104;
        static final int STATE_IN_SETUP = 105;

        public int state;

        public int uid;
        public String packageName;
        public String interfaceName;
        public int pubSubId = 0;
        public int specifiedPeerInstanceId = 0;
        public byte[] specifiedPeerDiscoveryMac = null;
        public WifiAwareNetworkSpecifier networkSpecifier;
        public long startValidationTimestamp = 0; // NDP created and starting to validate IPv6 addr
        public SparseArray<NdpInfo> ndpInfos = new SparseArray();

        public WifiAwareNetworkAgent networkAgent;

        /* A collection of request which are equivalent to the current request and are
         * supported by it's agent. This list DOES include the original (first) network request
         * (whose specifier is also stored separately above).
         */
        public Set<NetworkRequest> equivalentRequests = new HashSet<>();

        void updateToSupportNewRequest(NetworkRequest ns) {
            if (sVdbg) Log.v(TAG, "updateToSupportNewRequest: ns=" + ns);
            if (equivalentRequests.add(ns) && state == STATE_CONFIRMED) {
                if (networkAgent == null) {
                    Log.wtf(TAG, "updateToSupportNewRequest: null agent in CONFIRMED state!?");
                    return;
                }

                networkAgent.sendNetworkCapabilities(getNetworkCapabilities());
            }
        }

        void removeSupportForRequest(NetworkRequest ns) {
            if (sVdbg) Log.v(TAG, "removeSupportForRequest: ns=" + ns);
            equivalentRequests.remove(ns);

            // we will not update the agent:
            // 1. this will only get called before the agent is created
            // 2. connectivity service does not allow (WTF) updates with reduced capabilities
        }

        private NetworkCapabilities getNetworkCapabilities() {
            final NetworkCapabilities.Builder builder =
                    new NetworkCapabilities.Builder(sNetworkCapabilitiesFilter);
            builder.setNetworkSpecifier(new WifiAwareAgentNetworkSpecifier(
                    equivalentRequests.stream()
                            .map(NetworkRequest::getNetworkSpecifier)
                            .toArray(WifiAwareNetworkSpecifier[]::new)));

            if (ndpInfos.size() == 0) {
                Log.wtf(TAG, "Number of NDPs is 0 when Network agent is created?! "
                        + "AwareNetworkRequestInformation" + this);
                return builder.setTransportInfo(new WifiAwareNetworkInfo()).build();
            }
            if (ndpInfos.valueAt(0).peerIpv6 != null) {
                NdpInfo ndpInfo = ndpInfos.valueAt(0);
                builder.setTransportInfo(
                        new WifiAwareNetworkInfo(ndpInfo.peerIpv6, ndpInfo.peerPort,
                                ndpInfo.peerTransportProtocol, ndpInfo.channelInfos));
            }
            return builder.build();
        }

        /**
         * Returns a canonical descriptor for the network request.
         */
        CanonicalConnectionInfo getCanonicalDescriptor() {
            return new CanonicalConnectionInfo(specifiedPeerDiscoveryMac,
                    networkSpecifier.sessionId,
                    networkSpecifier.getWifiAwareDataPathSecurityConfig());
        }

        static AwareNetworkRequestInformation processNetworkSpecifier(NetworkRequest request,
                WifiAwareNetworkSpecifier ns, WifiAwareStateManager mgr,
                WifiPermissionsUtil wifiPermissionsUtil, WifiPermissionsWrapper permissionWrapper,
                boolean allowNdpResponderFromAnyOverride) {
            int uid, pubSubId = 0;
            int peerInstanceId = 0;
            String packageName = null;
            byte[] peerMac = ns.peerMac;

            if (sVdbg) {
                Log.v(TAG, "processNetworkSpecifier: networkSpecifier=" + ns);
            }

            // type: always valid
            if (ns.type < 0
                    || ns.type > WifiAwareNetworkSpecifier.NETWORK_SPECIFIER_TYPE_MAX_VALID) {
                Log.e(TAG, "processNetworkSpecifier: networkSpecifier=" + ns
                        + ", invalid 'type' value");
                return null;
            }

            // role: always valid
            if (ns.role != WifiAwareManager.WIFI_AWARE_DATA_PATH_ROLE_INITIATOR
                    && ns.role != WifiAwareManager.WIFI_AWARE_DATA_PATH_ROLE_RESPONDER) {
                Log.e(TAG, "processNetworkSpecifier: networkSpecifier=" + ns
                        + " -- invalid 'role' value");
                return null;
            }

            if (ns.role == WifiAwareManager.WIFI_AWARE_DATA_PATH_ROLE_INITIATOR
                    && ns.type != WifiAwareNetworkSpecifier.NETWORK_SPECIFIER_TYPE_IB
                    && ns.type != WifiAwareNetworkSpecifier.NETWORK_SPECIFIER_TYPE_OOB) {
                Log.e(TAG, "processNetworkSpecifier: networkSpecifier=" + ns
                        + " -- invalid 'type' value for INITIATOR (only IB and OOB are "
                        + "permitted)");
                return null;
            }

            // look up network specifier information in Aware state manager
            WifiAwareClientState client = mgr.getClient(ns.clientId);
            if (client == null) {
                Log.e(TAG, "processNetworkSpecifier: networkSpecifier=" + ns
                        + " -- not client with this id -- clientId=" + ns.clientId);
                return null;
            }
            uid = client.getUid();
            packageName = client.getCallingPackage();

            // API change post 30: allow accepts any peer responder.

            if (!SdkLevel.isAtLeastS() && !allowNdpResponderFromAnyOverride
                    && !wifiPermissionsUtil.isTargetSdkLessThan(
                            client.getCallingPackage(), Build.VERSION_CODES.P, uid)) {
                if (ns.type != WifiAwareNetworkSpecifier.NETWORK_SPECIFIER_TYPE_IB
                        && ns.type != WifiAwareNetworkSpecifier.NETWORK_SPECIFIER_TYPE_OOB) {
                    Log.e(TAG, "processNetworkSpecifier: networkSpecifier=" + ns
                            + " -- no ANY specifications allowed for this API level");
                    return null;
                }
            }

            // validate the port & transportProtocol
            if (ns.port < 0 || ns.transportProtocol < -1) {
                Log.e(TAG, "processNetworkSpecifier: networkSpecifier=" + ns
                        + " -- invalid port/transportProtocol");
                return null;
            }
            if (ns.port != 0 || ns.transportProtocol != -1) {
                if (ns.role != WifiAwareManager.WIFI_AWARE_DATA_PATH_ROLE_RESPONDER) {
                    Log.e(TAG, "processNetworkSpecifier: networkSpecifier=" + ns
                            + " -- port/transportProtocol can only be specified on responder");
                    return null;
                }
                if (ns.getWifiAwareDataPathSecurityConfig() == null
                        || !ns.getWifiAwareDataPathSecurityConfig().isValid()) {
                    Log.e(TAG, "processNetworkSpecifier: networkSpecifier=" + ns
                            + " -- port/transportProtocol can only be specified on secure ndp");
                    return null;
                }
            }

            // validate the role (if session ID provided: i.e. session 1xx)
            if (ns.type == WifiAwareNetworkSpecifier.NETWORK_SPECIFIER_TYPE_IB
                    || ns.type == WifiAwareNetworkSpecifier.NETWORK_SPECIFIER_TYPE_IB_ANY_PEER) {
                WifiAwareDiscoverySessionState session = client.getSession(ns.sessionId);
                if (session == null) {
                    Log.e(TAG,
                            "processNetworkSpecifier: networkSpecifier=" + ns
                                    + " -- no session with this id -- sessionId=" + ns.sessionId);
                    return null;
                }

                if ((session.isPublishSession()
                        && ns.role != WifiAwareManager.WIFI_AWARE_DATA_PATH_ROLE_RESPONDER) || (
                        !session.isPublishSession() && ns.role
                                != WifiAwareManager.WIFI_AWARE_DATA_PATH_ROLE_INITIATOR)) {
                    Log.e(TAG, "processNetworkSpecifier: networkSpecifier=" + ns
                            + " -- invalid role for session type");
                    return null;
                }

                if (ns.type == WifiAwareNetworkSpecifier.NETWORK_SPECIFIER_TYPE_IB) {
                    pubSubId = session.getPubSubId();
                    WifiAwareDiscoverySessionState.PeerInfo peerInfo = session.getPeerInfo(
                            ns.peerId);
                    if (peerInfo == null) {
                        Log.e(TAG, "processNetworkSpecifier: networkSpecifier=" + ns
                                + " -- no peer info associated with this peer id -- peerId="
                                + ns.peerId);
                        return null;
                    }
                    peerInstanceId = peerInfo.mInstanceId;
                    try {
                        peerMac = peerInfo.mMac;
                        if (peerMac == null || peerMac.length != 6) {
                            Log.e(TAG, "processNetworkSpecifier: networkSpecifier="
                                    + ns + " -- invalid peer MAC address");
                            return null;
                        }
                    } catch (IllegalArgumentException e) {
                        Log.e(TAG, "processNetworkSpecifier: networkSpecifier=" + ns
                                + " -- invalid peer MAC address -- e=" + e);
                        return null;
                    }
                }
            }

            // validate UID && package name
            if (request.getRequestorUid() != uid
                    || !TextUtils.equals(request.getRequestorPackageName(), packageName)) {
                Log.e(TAG, "processNetworkSpecifier: networkSpecifier=" + ns.toString()
                        + " -- UID or package name mismatch to clientId's uid=" + uid
                        + ", packageName=" + packageName);
                return null;
            }

            // validate passphrase & PMK (if provided)
            if (ns.getWifiAwareDataPathSecurityConfig() != null
                    && (!ns.getWifiAwareDataPathSecurityConfig().isValid()
                    || (mgr.getCapabilities().supportedDataPathCipherSuites
                    & ns.getWifiAwareDataPathSecurityConfig().getCipherSuite()) == 0)) {
                    Log.e(TAG, "processNetworkSpecifier: networkSpecifier=" + ns.toString()
                            + " -- invalid security config: ");
                    return null;
            }

            // create container and populate
            AwareNetworkRequestInformation nnri = new AwareNetworkRequestInformation();
            nnri.state = AwareNetworkRequestInformation.STATE_IDLE;
            nnri.uid = uid;
            nnri.packageName = packageName;
            nnri.pubSubId = pubSubId;
            nnri.specifiedPeerInstanceId = peerInstanceId;
            nnri.specifiedPeerDiscoveryMac = peerMac;
            nnri.networkSpecifier = ns;
            nnri.equivalentRequests.add(request);

            return nnri;
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder("AwareNetworkRequestInformation: ");
            sb.append("state=").append(state).append(", ns=").append(networkSpecifier)
                    .append(", uid=").append(uid)
                    .append(", packageName=").append(packageName)
                    .append(", interfaceName=").append(interfaceName).append(
                    ", pubSubId=").append(pubSubId).append(", specifiedPeerInstanceId=").append(
                    specifiedPeerInstanceId).append(", specifiedPeerDiscoveryMac=").append(
                    specifiedPeerDiscoveryMac == null ? ""
                            : String.valueOf(HexEncoding.encode(specifiedPeerDiscoveryMac)))
                    .append(", equivalentSpecifiers=[");
            for (NetworkRequest nr : equivalentRequests) {
                sb.append(nr.toString()).append(", ");
            }
            sb.append("]");
            sb.append(", NdpInfos[");
            for (int index = 0; index < ndpInfos.size(); index++) {
                sb.append(" ").append(index).append(": ").append(ndpInfos.valueAt(index));
            }
            sb.append("]");
            return sb.toString();
        }
    }

    /**
     * A canonical (unique) descriptor of the peer connection.
     */
    static class CanonicalConnectionInfo {
        CanonicalConnectionInfo(byte[] peerDiscoveryMac, int sessionId,
                WifiAwareDataPathSecurityConfig securityConfig) {
            this.peerDiscoveryMac = peerDiscoveryMac;
            this.sessionId = sessionId;
            this.securityConfig = securityConfig;
        }

        public final byte[] peerDiscoveryMac;


        public final int sessionId;
        public final WifiAwareDataPathSecurityConfig securityConfig;

        public boolean matches(CanonicalConnectionInfo other) {
            return Arrays.equals(peerDiscoveryMac, other.peerDiscoveryMac)
                    && Objects.equals(securityConfig, other.securityConfig)
                    && (securityConfig == null || sessionId == other.sessionId);
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder("CanonicalConnectionInfo: [");
            sb.append("peerDiscoveryMac=").append(peerDiscoveryMac == null ? ""
                    : String.valueOf(HexEncoding.encode(peerDiscoveryMac)))
                    .append(", security=").append(securityConfig == null ? "" : securityConfig)
                    .append(", sessionId=").append(sessionId).append("]");
            return sb.toString();
        }
    }

    /**
     * Enables mocking.
     */
    @VisibleForTesting
    public class NetworkInterfaceWrapper {
        /**
         * Configures network agent properties: link-local address, connected status, interface
         * name. Delegated to enable mocking.
         */
        public boolean configureAgentProperties(AwareNetworkRequestInformation nnri,
                NetworkCapabilities.Builder ncBuilder, LinkProperties linkProperties) {
            // find link-local address
            InetAddress linkLocal = null;
            NetworkInterface ni;
            try {
                ni = NetworkInterface.getByName(nnri.interfaceName);
            } catch (SocketException e) {
                Log.v(TAG, "onDataPathConfirm: ACCEPT nnri=" + nnri
                        + ": can't get network interface - " + e);
                return false;
            }
            if (ni == null) {
                Log.v(TAG, "onDataPathConfirm: ACCEPT nnri=" + nnri
                        + ": can't get network interface (null)");
                return false;
            }
            Enumeration<InetAddress> addresses = ni.getInetAddresses();
            while (addresses.hasMoreElements()) {
                InetAddress ip = addresses.nextElement();
                if (ip instanceof Inet6Address && ip.isLinkLocalAddress()) {
                    linkLocal = ip;
                    break;
                }
            }

            if (linkLocal == null) {
                Log.v(TAG, "onDataPathConfirm: ACCEPT nnri=" + nnri + ": no link local addresses");
                return false;
            }

            ncBuilder.setRequestorUid(nnri.uid);
            ncBuilder.setRequestorPackageName(nnri.packageName);
            ncBuilder.setNetworkSpecifier(new WifiAwareAgentNetworkSpecifier(
                    nnri.equivalentRequests.stream()
                            .map(NetworkRequest::getNetworkSpecifier)
                            .toArray(WifiAwareNetworkSpecifier[]::new)));

            linkProperties.setInterfaceName(nnri.interfaceName);
            linkProperties.addLinkAddress(new LinkAddress(linkLocal, 64));
            linkProperties.addRoute(
                    new RouteInfo(new IpPrefix("fe80::/64"), null, nnri.interfaceName,
                            RTN_UNICAST));

            return true;
        }

        /**
         * Tries binding to the input address to check whether it is configured (and therefore
         * usable).
         */
        public boolean isAddressUsable(LinkProperties linkProperties) {
            InetAddress address = linkProperties.getLinkAddresses().get(0).getAddress();
            DatagramSocket testDatagramSocket = null;
            try {
                testDatagramSocket = new DatagramSocket(0, address);
            } catch (SocketException e) {
                if (mVerboseLoggingEnabled) {
                    Log.v(TAG, "Can't create socket on address " + address + " -- " + e);
                }
                return false;
            } finally {
                if (testDatagramSocket != null) {
                    testDatagramSocket.close();
                }
            }
            return true;
        }

        /**
         * Tell the network agent the network is now connected.
         */
        public void setConnected(WifiAwareNetworkAgent networkAgent) {
            networkAgent.markConnected();
        }
    }

    /**
     * Utility (hence static) class encapsulating the data structure used to communicate Wi-Fi Aware
     * specific network capabilities. The TLV is defined as part of the NANv3 spec:
     *
     * - Generic Service Protocol
     *   - Port
     *   - Transport protocol
     */
    @VisibleForTesting
    public static class NetworkInformationData {
        // All package visible to allow usage in unit testing
        /* package */ static final int IPV6_LL_TYPE = 0x00; // Table 82
        /* package */ static final int SERVICE_INFO_TYPE = 0x01; // Table 83
        /* package */ static final byte[] WFA_OUI = {0x50, 0x6F, (byte) 0x9A}; // Table 83
        /* package */ static final int GENERIC_SERVICE_PROTOCOL_TYPE = 0x02; // Table 50
        /* package */ static final int SUB_TYPE_PORT = 0x00; // Table 127
        /* package */ static final int SUB_TYPE_TRANSPORT_PROTOCOL = 0x01; // Table 128

        /**
         * Construct the TLV.
         */
        public static byte[] buildTlv(int port, int transportProtocol) {
            if (port == 0 && transportProtocol == -1) {
                return null;
            }

            TlvBufferUtils.TlvConstructor tlvc = new TlvBufferUtils.TlvConstructor(1, 2);
            tlvc.setByteOrder(ByteOrder.LITTLE_ENDIAN);
            tlvc.allocate(20); // safe size for now

            tlvc.putRawByteArray(WFA_OUI);
            tlvc.putRawByte((byte) GENERIC_SERVICE_PROTOCOL_TYPE);

            if (port != 0) {
                tlvc.putShort(SUB_TYPE_PORT, (short) port);
            }
            if (transportProtocol != -1) {
                tlvc.putByte(SUB_TYPE_TRANSPORT_PROTOCOL, (byte) transportProtocol);
            }

            byte[] subTypes = tlvc.getArray();

            tlvc.allocate(20);
            tlvc.putByteArray(SERVICE_INFO_TYPE, subTypes);

            return tlvc.getArray();
        }

        static class ParsedResults {
            ParsedResults(int port, int transportProtocol, byte[] ipv6Override) {
                this.port = port;
                this.transportProtocol = transportProtocol;
                this.ipv6Override = ipv6Override;
            }

            public int port = 0;
            public int transportProtocol = -1;
            public byte[] ipv6Override = null;
        }

        /**
         * Parse the TLV and returns:
         * - Null on parsing error
         * - <port | 0, transport-protocol | -1, ipv6-override | null> otherwise
         */
        public static ParsedResults parseTlv(byte[] tlvs) {
            int port = 0;
            int transportProtocol = -1;
            byte[] ipv6Override = null;

            try {
                TlvBufferUtils.TlvIterable tlvi = new TlvBufferUtils.TlvIterable(1, 2, tlvs);
                tlvi.setByteOrder(ByteOrder.LITTLE_ENDIAN);
                for (TlvBufferUtils.TlvElement tlve : tlvi) {
                    switch (tlve.type) {
                        case IPV6_LL_TYPE:
                            if (tlve.length != 8) { // 8 bytes in IPv6 address
                                Log.e(TAG, "NetworkInformationData: invalid IPv6 TLV -- length: "
                                        + tlve.length);
                                return null;
                            }
                            ipv6Override = tlve.getRawData();
                            break;
                        case SERVICE_INFO_TYPE:
                            Pair<Integer, Integer> serviceInfo = parseServiceInfoTlv(
                                    tlve.getRawData());
                            if (serviceInfo == null) {
                                return null;
                            }
                            port = serviceInfo.first;
                            transportProtocol = serviceInfo.second;
                            break;
                        default:
                            Log.w(TAG,
                                    "NetworkInformationData: ignoring unknown T -- " + tlve.type);
                            break;
                    }
                }
            } catch (Exception e) {
                Log.e(TAG, "NetworkInformationData: error parsing TLV -- " + e);
                return null;
            }
            return new ParsedResults(port, transportProtocol, ipv6Override);
        }

        /**
         * Parse the Service Info TLV:
         * - Returns null on error
         * - Returns <port | 0, transport-protocol | -1> otherwise
         */
        private static Pair<Integer, Integer> parseServiceInfoTlv(byte[] tlv) {
            int port = 0;
            int transportProtocol = -1;

            if (tlv.length < 4) {
                Log.e(TAG, "NetworkInformationData: invalid SERVICE_INFO_TYPE length");
                return null;
            }
            if (tlv[0] != WFA_OUI[0] || tlv[1] != WFA_OUI[1] || tlv[2] != WFA_OUI[2]) {
                Log.e(TAG, "NetworkInformationData: unexpected OUI");
                return null;
            }
            if (tlv[3] != GENERIC_SERVICE_PROTOCOL_TYPE) {
                Log.e(TAG, "NetworkInformationData: invalid type -- " + tlv[3]);
                return null;
            }
            TlvBufferUtils.TlvIterable subTlvi = new TlvBufferUtils.TlvIterable(1,
                    2, Arrays.copyOfRange(tlv, 4, tlv.length));
            subTlvi.setByteOrder(ByteOrder.LITTLE_ENDIAN);
            for (TlvBufferUtils.TlvElement subTlve : subTlvi) {
                switch (subTlve.type) {
                    case SUB_TYPE_PORT:
                        if (subTlve.length != 2) {
                            Log.e(TAG,
                                    "NetworkInformationData: invalid port TLV "
                                            + "length -- " + subTlve.length);
                            return null;
                        }
                        port = subTlve.getShort();
                        if (port < 0) {
                            port += -2 * (int) Short.MIN_VALUE;
                        }
                        if (port == 0) {
                            Log.e(TAG, "NetworkInformationData: invalid port "
                                    + port);
                            return null;
                        }
                        break;
                    case SUB_TYPE_TRANSPORT_PROTOCOL:
                        if (subTlve.length != 1) {
                            Log.e(TAG,  "NetworkInformationData: invalid transport "
                                    + "protocol TLV length -- " + subTlve.length);
                            return null;
                        }
                        transportProtocol = subTlve.getByte();
                        if (transportProtocol < 0) {
                            transportProtocol += -2 * (int) Byte.MIN_VALUE;
                        }
                        break;
                    default:
                        Log.w(TAG,  "NetworkInformationData: ignoring unknown "
                                + "SERVICE_INFO.T -- " + subTlve.type);
                        break;
                }
            }
            return Pair.create(port, transportProtocol);
        }
    }

    private void cleanNetworkValidationTask(int ndpId) {
        Object token = mDelayNetworkValidationMap.get(ndpId);
        if (token != null) {
            mHandler.removeCallbacksAndMessages(token);
            mDelayNetworkValidationMap.remove(ndpId);
        }
    }

    /**
     * Dump the internal state of the class.
     */
    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
        pw.println("WifiAwareDataPathStateManager:");
        pw.println("  mInterfaces: " + mInterfaces);
        pw.println("  sNetworkCapabilitiesFilter: " + sNetworkCapabilitiesFilter);
        pw.println("  mNetworkRequestsCache: " + mNetworkRequestsCache);
        pw.println("  mNetworkFactory:");
        mNetworkFactory.dump(fd, pw, args);
    }
}
