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

package android.net.ip;

import static android.net.IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ROAM;
import static android.net.IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_CONFIRM;
import static android.net.IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_ORGANIC;
import static android.net.IIpMemoryStore.NETWORK_EVENT_NUD_FAILURE_MAC_ADDRESS_CHANGED;
import static android.net.RouteInfo.RTN_UNICAST;
import static android.net.RouteInfo.RTN_UNREACHABLE;
import static android.net.dhcp.DhcpResultsParcelableUtil.toStableParcelable;
import static android.net.ip.IIpClient.PROV_IPV4_DISABLED;
import static android.net.ip.IIpClient.PROV_IPV6_DISABLED;
import static android.net.ip.IIpClient.PROV_IPV6_LINKLOCAL;
import static android.net.ip.IIpClient.PROV_IPV6_SLAAC;
import static android.net.ip.IIpClientCallbacks.DTIM_MULTIPLIER_RESET;
import static android.net.ip.IpClient.IpClientCommands.CMD_ADDRESSES_CLEARED;
import static android.net.ip.IpClient.IpClientCommands.CMD_ADD_KEEPALIVE_PACKET_FILTER_TO_APF;
import static android.net.ip.IpClient.IpClientCommands.CMD_COMPLETE_PRECONNECTION;
import static android.net.ip.IpClient.IpClientCommands.CMD_CONFIRM;
import static android.net.ip.IpClient.IpClientCommands.CMD_JUMP_RUNNING_TO_STOPPING;
import static android.net.ip.IpClient.IpClientCommands.CMD_JUMP_STOPPING_TO_STOPPED;
import static android.net.ip.IpClient.IpClientCommands.CMD_REMOVE_KEEPALIVE_PACKET_FILTER_FROM_APF;
import static android.net.ip.IpClient.IpClientCommands.CMD_SET_DTIM_MULTIPLIER_AFTER_DELAY;
import static android.net.ip.IpClient.IpClientCommands.CMD_SET_MULTICAST_FILTER;
import static android.net.ip.IpClient.IpClientCommands.CMD_START;
import static android.net.ip.IpClient.IpClientCommands.CMD_STOP;
import static android.net.ip.IpClient.IpClientCommands.CMD_TERMINATE_AFTER_STOP;
import static android.net.ip.IpClient.IpClientCommands.CMD_UPDATE_APF_CAPABILITIES;
import static android.net.ip.IpClient.IpClientCommands.CMD_UPDATE_APF_DATA_SNAPSHOT;
import static android.net.ip.IpClient.IpClientCommands.CMD_UPDATE_HTTP_PROXY;
import static android.net.ip.IpClient.IpClientCommands.CMD_UPDATE_L2INFORMATION;
import static android.net.ip.IpClient.IpClientCommands.CMD_UPDATE_TCP_BUFFER_SIZES;
import static android.net.ip.IpClient.IpClientCommands.EVENT_DHCPACTION_TIMEOUT;
import static android.net.ip.IpClient.IpClientCommands.EVENT_IPV6_AUTOCONF_TIMEOUT;
import static android.net.ip.IpClient.IpClientCommands.EVENT_NETLINK_LINKPROPERTIES_CHANGED;
import static android.net.ip.IpClient.IpClientCommands.EVENT_NUD_FAILURE_QUERY_FAILURE;
import static android.net.ip.IpClient.IpClientCommands.EVENT_NUD_FAILURE_QUERY_SUCCESS;
import static android.net.ip.IpClient.IpClientCommands.EVENT_NUD_FAILURE_QUERY_TIMEOUT;
import static android.net.ip.IpClient.IpClientCommands.EVENT_PRE_DHCP_ACTION_COMPLETE;
import static android.net.ip.IpClient.IpClientCommands.EVENT_PROVISIONING_TIMEOUT;
import static android.net.ip.IpClient.IpClientCommands.EVENT_READ_PACKET_FILTER_COMPLETE;
import static android.net.ip.IpClientLinkObserver.IpClientNetlinkMonitor;
import static android.net.ip.IpClientLinkObserver.IpClientNetlinkMonitor.INetlinkMessageProcessor;
import static android.net.ip.IpReachabilityMonitor.INVALID_REACHABILITY_LOSS_TYPE;
import static android.net.ip.IpReachabilityMonitor.nudEventTypeToInt;
import static android.net.util.SocketUtils.makePacketSocketAddress;
import static android.provider.DeviceConfig.NAMESPACE_CONNECTIVITY;
import static android.stats.connectivity.NetworkQuirkEvent.QE_DHCP6_HEURISTIC_TRIGGERED;
import static android.stats.connectivity.NetworkQuirkEvent.QE_DHCP6_PD_PROVISIONED;
import static android.system.OsConstants.AF_PACKET;
import static android.system.OsConstants.ARPHRD_ETHER;
import static android.system.OsConstants.ETH_P_ARP;
import static android.system.OsConstants.ETH_P_IPV6;
import static android.system.OsConstants.IFA_F_NODAD;
import static android.system.OsConstants.RT_SCOPE_UNIVERSE;
import static android.system.OsConstants.SOCK_NONBLOCK;
import static android.system.OsConstants.SOCK_RAW;

import static com.android.net.module.util.LinkPropertiesUtils.CompareResult;
import static com.android.net.module.util.NetworkStackConstants.ARP_REPLY;
import static com.android.net.module.util.NetworkStackConstants.ETHER_BROADCAST;
import static com.android.net.module.util.NetworkStackConstants.IPV6_ADDR_ALL_ROUTERS_MULTICAST;
import static com.android.net.module.util.NetworkStackConstants.RFC7421_PREFIX_LENGTH;
import static com.android.net.module.util.NetworkStackConstants.VENDOR_SPECIFIC_IE_ID;
import static com.android.networkstack.apishim.ConstantsShim.IFA_F_MANAGETEMPADDR;
import static com.android.networkstack.apishim.ConstantsShim.IFA_F_NOPREFIXROUTE;
import static com.android.networkstack.util.NetworkStackUtils.APF_HANDLE_ARP_OFFLOAD;
import static com.android.networkstack.util.NetworkStackUtils.APF_HANDLE_ND_OFFLOAD;
import static com.android.networkstack.util.NetworkStackUtils.APF_NEW_RA_FILTER_VERSION;
import static com.android.networkstack.util.NetworkStackUtils.APF_POLLING_COUNTERS_VERSION;
import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_DHCPV6_PREFIX_DELEGATION_VERSION;
import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_GARP_NA_ROAMING_VERSION;
import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_IGNORE_LOW_RA_LIFETIME_VERSION;
import static com.android.networkstack.util.NetworkStackUtils.IPCLIENT_POPULATE_LINK_ADDRESS_LIFETIME_VERSION;
import static com.android.networkstack.util.NetworkStackUtils.IP_REACHABILITY_IGNORE_NUD_FAILURE_VERSION;
import static com.android.networkstack.util.NetworkStackUtils.createInet6AddressFromEui64;
import static com.android.networkstack.util.NetworkStackUtils.macAddressToEui64;
import static com.android.server.util.PermissionUtil.enforceNetworkStackCallingPermission;

import android.annotation.SuppressLint;
import android.app.admin.DevicePolicyManager;
import android.content.ComponentName;
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.net.ConnectivityManager;
import android.net.DhcpResults;
import android.net.INetd;
import android.net.IpPrefix;
import android.net.Layer2InformationParcelable;
import android.net.Layer2PacketParcelable;
import android.net.LinkAddress;
import android.net.LinkProperties;
import android.net.MacAddress;
import android.net.NattKeepalivePacketDataParcelable;
import android.net.NetworkStackIpMemoryStore;
import android.net.ProvisioningConfigurationParcelable;
import android.net.ProxyInfo;
import android.net.RouteInfo;
import android.net.TcpKeepalivePacketDataParcelable;
import android.net.Uri;
import android.net.apf.AndroidPacketFilter;
import android.net.apf.ApfCapabilities;
import android.net.apf.ApfFilter;
import android.net.apf.LegacyApfFilter;
import android.net.dhcp.DhcpClient;
import android.net.dhcp.DhcpPacket;
import android.net.dhcp6.Dhcp6Client;
import android.net.ipmemorystore.OnNetworkEventCountRetrievedListener;
import android.net.ipmemorystore.Status;
import android.net.metrics.IpConnectivityLog;
import android.net.metrics.IpManagerEvent;
import android.net.networkstack.aidl.dhcp.DhcpOption;
import android.net.networkstack.aidl.ip.ReachabilityLossInfoParcelable;
import android.net.networkstack.aidl.ip.ReachabilityLossReason;
import android.net.shared.InitialConfiguration;
import android.net.shared.Layer2Information;
import android.net.shared.ProvisioningConfiguration;
import android.net.shared.ProvisioningConfiguration.ScanResultInfo;
import android.net.shared.ProvisioningConfiguration.ScanResultInfo.InformationElement;
import android.os.ConditionVariable;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.RemoteException;
import android.os.ServiceSpecificException;
import android.os.SystemClock;
import android.os.UserHandle;
import android.stats.connectivity.DisconnectCode;
import android.stats.connectivity.NetworkQuirkEvent;
import android.stats.connectivity.NudEventType;
import android.system.ErrnoException;
import android.system.Os;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.LocalLog;
import android.util.Log;
import android.util.SparseArray;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.HexDump;
import com.android.internal.util.IState;
import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.MessageUtils;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;
import com.android.internal.util.WakeupMessage;
import com.android.modules.expresslog.Counter;
import com.android.modules.utils.build.SdkLevel;
import com.android.net.module.util.CollectionUtils;
import com.android.net.module.util.ConnectivityUtils;
import com.android.net.module.util.DeviceConfigUtils;
import com.android.net.module.util.HandlerUtils;
import com.android.net.module.util.InterfaceParams;
import com.android.net.module.util.LinkPropertiesUtils;
import com.android.net.module.util.SharedLog;
import com.android.net.module.util.SocketUtils;
import com.android.net.module.util.arp.ArpPacket;
import com.android.net.module.util.ip.InterfaceController;
import com.android.net.module.util.netlink.NetlinkUtils;
import com.android.net.module.util.structs.IaPrefixOption;
import com.android.networkstack.R;
import com.android.networkstack.apishim.NetworkInformationShimImpl;
import com.android.networkstack.apishim.SocketUtilsShimImpl;
import com.android.networkstack.apishim.common.NetworkInformationShim;
import com.android.networkstack.apishim.common.ShimUtils;
import com.android.networkstack.metrics.IpProvisioningMetrics;
import com.android.networkstack.metrics.NetworkQuirkMetrics;
import com.android.networkstack.packets.NeighborAdvertisement;
import com.android.networkstack.packets.NeighborSolicitation;
import com.android.networkstack.util.NetworkStackUtils;
import com.android.server.NetworkStackService.NetworkStackServiceManager;

import java.io.File;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.SocketAddress;
import java.net.SocketException;
import java.net.URL;
import java.nio.BufferUnderflowException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.StringJoiner;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Predicate;
import java.util.stream.Collectors;

/**
 * IpClient
 *
 * This class provides the interface to IP-layer provisioning and maintenance
 * functionality that can be used by transport layers like Wi-Fi, Ethernet,
 * et cetera.
 *
 * [ Lifetime ]
 * IpClient is designed to be instantiated as soon as the interface name is
 * known and can be as long-lived as the class containing it (i.e. declaring
 * it "private final" is okay).
 *
 * @hide
 */
public class IpClient extends StateMachine {
    private static final String TAG = IpClient.class.getSimpleName();
    private static final boolean DBG = false;
    private final boolean mApfDebug;

    // For message logging.
    private static final Class[] sMessageClasses = { IpClientCommands.class, DhcpClient.class };
    private static final SparseArray<String> sWhatToString =
            MessageUtils.findMessageNames(sMessageClasses);
    // Static concurrent hashmaps of interface name to logging classes.
    // This map holds StateMachine logs.
    private static final ConcurrentHashMap<String, SharedLog> sSmLogs = new ConcurrentHashMap<>();
    // This map holds connectivity packet logs.
    private static final ConcurrentHashMap<String, LocalLog> sPktLogs = new ConcurrentHashMap<>();
    // This map holds Apf logs.
    private static final ConcurrentHashMap<String, SharedLog> sApfLogs = new ConcurrentHashMap<>();
    private final NetworkStackIpMemoryStore mIpMemoryStore;
    private final NetworkInformationShim mShim = NetworkInformationShimImpl.newInstance();
    private final IpProvisioningMetrics mIpProvisioningMetrics = new IpProvisioningMetrics();
    private final NetworkQuirkMetrics mNetworkQuirkMetrics;

    private boolean mHasSeenClatInterface = false;

    /**
     * Dump all state machine and connectivity packet logs to the specified writer.
     * @param skippedIfaces Interfaces for which logs should not be dumped.
     */
    public static void dumpAllLogs(PrintWriter writer, Set<String> skippedIfaces) {
        for (String ifname : sSmLogs.keySet()) {
            if (skippedIfaces.contains(ifname)) continue;

            writer.println(String.format("--- BEGIN %s ---", ifname));

            final SharedLog apfLog = sApfLogs.get(ifname);
            if (apfLog != null) {
                writer.println("APF log:");
                apfLog.dump(null, writer, null);
            }

            final SharedLog smLog = sSmLogs.get(ifname);
            if (smLog != null) {
                writer.println("State machine log:");
                smLog.dump(null, writer, null);
            }

            writer.println("");

            final LocalLog pktLog = sPktLogs.get(ifname);
            if (pktLog != null) {
                writer.println("Connectivity packet log:");
                pktLog.readOnlyLocalLog().dump(null, writer, null);
            }

            writer.println(String.format("--- END %s ---", ifname));
        }
    }

    // Use a wrapper class to log in order to ensure complete and detailed
    // logging. This method is lighter weight than annotations/reflection
    // and has the following benefits:
    //
    //     - No invoked method can be forgotten.
    //       Any new method added to IpClient.Callback must be overridden
    //       here or it will never be called.
    //
    //     - No invoking call site can be forgotten.
    //       Centralized logging in this way means call sites don't need to
    //       remember to log, and therefore no call site can be forgotten.
    //
    //     - No variation in log format among call sites.
    //       Encourages logging of any available arguments, and all call sites
    //       are necessarily logged identically.
    //
    // NOTE: Log first because passed objects may or may not be thread-safe and
    // once passed on to the callback they may be modified by another thread.
    //
    // TODO: Find an lighter weight approach.
    public static class IpClientCallbacksWrapper {
        private static final String PREFIX = "INVOKE ";
        private final IIpClientCallbacks mCallback;
        @NonNull
        private final SharedLog mLog;
        @NonNull
        private final SharedLog mApfLog;
        @NonNull
        private final NetworkInformationShim mShim;

        private final boolean mApfDebug;

        @VisibleForTesting
        protected IpClientCallbacksWrapper(IIpClientCallbacks callback, @NonNull SharedLog log,
                @NonNull SharedLog apfLog, @NonNull NetworkInformationShim shim,
                boolean apfDebug) {
            mCallback = callback;
            mLog = log;
            mApfLog = apfLog;
            mShim = shim;
            mApfDebug = apfDebug;
        }

        private void log(String msg) {
            mLog.log(PREFIX + msg);
        }

        private void log(String msg, Throwable e) {
            mLog.e(PREFIX + msg, e);
        }

        /**
         * Callback called prior to DHCP discovery/renewal only if the pre DHCP action
         * is enabled.
         */
        public void onPreDhcpAction() {
            log("onPreDhcpAction()");
            try {
                mCallback.onPreDhcpAction();
            } catch (RemoteException e) {
                log("Failed to call onPreDhcpAction", e);
            }
        }

        /**
         * Callback called after DHCP discovery/renewal only if the pre DHCP action
         * is enabled.
         */
        public void onPostDhcpAction() {
            log("onPostDhcpAction()");
            try {
                mCallback.onPostDhcpAction();
            } catch (RemoteException e) {
                log("Failed to call onPostDhcpAction", e);
            }
        }

        /**
         * Callback called when new DHCP results are available.
         */
        public void onNewDhcpResults(DhcpResults dhcpResults) {
            log("onNewDhcpResults({" + dhcpResults + "})");
            try {
                mCallback.onNewDhcpResults(toStableParcelable(dhcpResults));
            } catch (RemoteException e) {
                log("Failed to call onNewDhcpResults", e);
            }
        }

        /**
         * Indicates that provisioning was successful.
         */
        public void onProvisioningSuccess(LinkProperties newLp) {
            log("onProvisioningSuccess({" + newLp + "})");
            try {
                mCallback.onProvisioningSuccess(mShim.makeSensitiveFieldsParcelingCopy(newLp));
            } catch (RemoteException e) {
                log("Failed to call onProvisioningSuccess", e);
            }
        }

        /**
         * Indicates that provisioning failed.
         */
        public void onProvisioningFailure(LinkProperties newLp) {
            log("onProvisioningFailure({" + newLp + "})");
            try {
                mCallback.onProvisioningFailure(mShim.makeSensitiveFieldsParcelingCopy(newLp));
            } catch (RemoteException e) {
                log("Failed to call onProvisioningFailure", e);
            }
        }

        /**
         * Invoked on LinkProperties changes.
         */
        public void onLinkPropertiesChange(LinkProperties newLp) {
            log("onLinkPropertiesChange({" + newLp + "})");
            try {
                mCallback.onLinkPropertiesChange(mShim.makeSensitiveFieldsParcelingCopy(newLp));
            } catch (RemoteException e) {
                log("Failed to call onLinkPropertiesChange", e);
            }
        }

        /**
         * Called when the internal IpReachabilityMonitor (if enabled) has detected the loss of
         * required neighbors (e.g. on-link default gw or dns servers) due to NUD_FAILED.
         *
         * Note this method is only supported on networkstack-aidl-interfaces-v12 or below.
         * For above aidl versions, the caller should call {@link onReachabilityFailure} instead.
         * For callbacks extending IpClientCallbacks, this method will be called iff the callback
         * does not implement onReachabilityFailure.
         */
        public void onReachabilityLost(String logMsg) {
            log("onReachabilityLost(" + logMsg + ")");
            try {
                mCallback.onReachabilityLost(logMsg);
            } catch (RemoteException e) {
                log("Failed to call onReachabilityLost", e);
            }
        }

        /**
         * Called when the IpClient state machine terminates.
         */
        public void onQuit() {
            log("onQuit()");
            try {
                mCallback.onQuit();
            } catch (RemoteException e) {
                log("Failed to call onQuit", e);
            }
        }

        /**
         * Called to indicate that a new APF program must be installed to filter incoming packets.
         */
        public boolean installPacketFilter(byte[] filter) {
            log("installPacketFilter(byte[" + filter.length + "])");
            try {
                if (mApfDebug) {
                    mApfLog.log("updated APF program: " + HexDump.toHexString(filter));
                }
                mCallback.installPacketFilter(filter);
            } catch (RemoteException e) {
                log("Failed to call installPacketFilter", e);
                return false;
            }
            return true;
        }

        /**
         * Called to indicate that the APF Program & data buffer must be read asynchronously from
         * the wifi driver.
         */
        public void startReadPacketFilter(@NonNull String event) {
            log("startReadPacketFilter(), event: " + event);
            try {
                mCallback.startReadPacketFilter();
            } catch (RemoteException e) {
                log("Failed to call startReadPacketFilter", e);
            }
        }

        /**
         * If multicast filtering cannot be accomplished with APF, this function will be called to
         * actuate multicast filtering using another means.
         */
        public void setFallbackMulticastFilter(boolean enabled) {
            log("setFallbackMulticastFilter(" + enabled + ")");
            try {
                mCallback.setFallbackMulticastFilter(enabled);
            } catch (RemoteException e) {
                log("Failed to call setFallbackMulticastFilter", e);
            }
        }

        /**
         * Enabled/disable Neighbor Discover offload functionality. This is called, for example,
         * whenever 464xlat is being started or stopped.
         */
        public void setNeighborDiscoveryOffload(boolean enable) {
            log("setNeighborDiscoveryOffload(" + enable + ")");
            try {
                mCallback.setNeighborDiscoveryOffload(enable);
            } catch (RemoteException e) {
                log("Failed to call setNeighborDiscoveryOffload", e);
            }
        }

        /**
         * Invoked on starting preconnection process.
         */
        public void onPreconnectionStart(List<Layer2PacketParcelable> packets) {
            log("onPreconnectionStart(Layer2Packets[" + packets.size() + "])");
            try {
                mCallback.onPreconnectionStart(packets);
            } catch (RemoteException e) {
                log("Failed to call onPreconnectionStart", e);
            }
        }

        /**
         * Called when Neighbor Unreachability Detection fails, that might be caused by the organic
         * probe or probeAll from IpReachabilityMonitor (if enabled).
         */
        public void onReachabilityFailure(ReachabilityLossInfoParcelable lossInfo) {
            log("onReachabilityFailure(" + lossInfo.message + ", loss reason: "
                    + reachabilityLossReasonToString(lossInfo.reason) + ")");
            try {
                mCallback.onReachabilityFailure(lossInfo);
            } catch (RemoteException e) {
                log("Failed to call onReachabilityFailure", e);
            }
        }

        /**
         * Set maximum acceptable DTIM multiplier to hardware driver.
         */
        public void setMaxDtimMultiplier(int multiplier) {
            try {
                // {@link IWifiStaIface#setDtimMultiplier} has been implemented since U, calling
                // this method on U- platforms does nothing actually.
                if (!SdkLevel.isAtLeastU()) {
                    log("SDK level is lower than U, do not call setMaxDtimMultiplier method");
                    return;
                }
                log("setMaxDtimMultiplier(" + multiplier + ")");
                mCallback.setMaxDtimMultiplier(multiplier);
            } catch (RemoteException e) {
                log("Failed to call setMaxDtimMultiplier", e);
            }
        }

        /**
         * Get the version of the IIpClientCallbacks AIDL interface.
         */
        public int getInterfaceVersion() {
            log("getInterfaceVersion");
            try {
                return mCallback.getInterfaceVersion();
            } catch (RemoteException e) {
                // This can never happen for callers in the system server, because if the
                // system server crashes, then the networkstack will crash as well. But it can
                // happen for other callers such as bluetooth or telephony (if it starts to use
                // IpClient). 0 will generally work but will assume an old client and disable
                // all new features.
                log("Failed to call getInterfaceVersion", e);
                return 0;
            }
        }
    }

    public static final String DUMP_ARG_CONFIRM = "confirm";

    // Sysctl parameter strings.
    private static final String ACCEPT_RA = "accept_ra";
    private static final String ACCEPT_RA_DEFRTR = "accept_ra_defrtr";
    @VisibleForTesting
    static final String ACCEPT_RA_MIN_LFT = "accept_ra_min_lft";
    private static final String DAD_TRANSMITS = "dad_transmits";

    /**
     * The IpClientCommands constant values.
     *
     * @hide
     */
    public static class IpClientCommands {
        private IpClientCommands() {
        }

        // Below constants are picked up by MessageUtils and exempt from ProGuard optimization.
        static final int CMD_TERMINATE_AFTER_STOP = 1;
        static final int CMD_STOP = 2;
        static final int CMD_START = 3;
        static final int CMD_CONFIRM = 4;
        static final int EVENT_PRE_DHCP_ACTION_COMPLETE = 5;
        // Triggered by IpClientLinkObserver to communicate netlink events.
        static final int EVENT_NETLINK_LINKPROPERTIES_CHANGED = 6;
        static final int CMD_UPDATE_TCP_BUFFER_SIZES = 7;
        static final int CMD_UPDATE_HTTP_PROXY = 8;
        static final int CMD_SET_MULTICAST_FILTER = 9;
        static final int EVENT_PROVISIONING_TIMEOUT = 10;
        static final int EVENT_DHCPACTION_TIMEOUT = 11;
        static final int EVENT_READ_PACKET_FILTER_COMPLETE = 12;
        static final int CMD_ADD_KEEPALIVE_PACKET_FILTER_TO_APF = 13;
        static final int CMD_REMOVE_KEEPALIVE_PACKET_FILTER_FROM_APF = 14;
        static final int CMD_COMPLETE_PRECONNECTION = 15;
        static final int CMD_UPDATE_L2INFORMATION = 16;
        static final int CMD_SET_DTIM_MULTIPLIER_AFTER_DELAY = 17;
        static final int CMD_UPDATE_APF_CAPABILITIES = 18;
        static final int EVENT_IPV6_AUTOCONF_TIMEOUT = 19;
        static final int CMD_UPDATE_APF_DATA_SNAPSHOT = 20;
        static final int EVENT_NUD_FAILURE_QUERY_TIMEOUT = 21;
        static final int EVENT_NUD_FAILURE_QUERY_SUCCESS = 22;
        static final int EVENT_NUD_FAILURE_QUERY_FAILURE = 23;
        // Internal commands to use instead of trying to call transitionTo() inside
        // a given State's enter() method. Calling transitionTo() from enter/exit
        // encounters a Log.wtf() that can cause trouble on eng builds.
        static final int CMD_ADDRESSES_CLEARED = 100;
        static final int CMD_JUMP_RUNNING_TO_STOPPING = 101;
        static final int CMD_JUMP_STOPPING_TO_STOPPED = 102;
    }

    private static final int ARG_LINKPROP_CHANGED_LINKSTATE_DOWN = 0;
    private static final int ARG_LINKPROP_CHANGED_LINKSTATE_UP = 1;

    // IpClient shares a handler with DhcpClient: commands must not overlap
    public static final int DHCPCLIENT_CMD_BASE = 1000;

    // IpClient shares a handler with Dhcp6Client: commands must not overlap
    public static final int DHCP6CLIENT_CMD_BASE = 2000;
    private static final int DHCPV6_PREFIX_DELEGATION_ADDRESS_FLAGS =
            IFA_F_MANAGETEMPADDR | IFA_F_NOPREFIXROUTE | IFA_F_NODAD;

    // Settings and default values.
    private static final int MAX_LOG_RECORDS = 500;
    private static final int MAX_PACKET_RECORDS = 100;

    @VisibleForTesting
    static final String CONFIG_ACCEPT_RA_MIN_LFT = "ipclient_accept_ra_min_lft";
    @VisibleForTesting
    static final int DEFAULT_ACCEPT_RA_MIN_LFT = 180;

    @VisibleForTesting
    static final String CONFIG_APF_COUNTER_POLLING_INTERVAL_SECS =
            "ipclient_apf_counter_polling_interval_secs";
    @VisibleForTesting
    static final int DEFAULT_APF_COUNTER_POLLING_INTERVAL_SECS = 300;

    // Used to wait for the provisioning to complete eventually and then decide the target
    // network type, which gives the accurate hint to set DTIM multiplier. Per current IPv6
    // provisioning connection latency metrics, the latency of 95% can go up to 16s, so pick
    // ProvisioningConfiguration.DEFAULT_TIMEOUT_MS value for this delay.
    @VisibleForTesting
    static final String CONFIG_INITIAL_PROVISIONING_DTIM_DELAY_MS =
            "ipclient_initial_provisioning_dtim_delay";
    private static final int DEFAULT_INITIAL_PROVISIONING_DTIM_DELAY_MS = 18000;

    @VisibleForTesting
    static final String CONFIG_MULTICAST_LOCK_MAX_DTIM_MULTIPLIER =
            "ipclient_multicast_lock_max_dtim_multiplier";
    @VisibleForTesting
    static final int DEFAULT_MULTICAST_LOCK_MAX_DTIM_MULTIPLIER = 1;

    @VisibleForTesting
    static final String CONFIG_IPV6_ONLY_NETWORK_MAX_DTIM_MULTIPLIER =
            "ipclient_ipv6_only_max_dtim_multiplier";
    @VisibleForTesting
    static final int DEFAULT_IPV6_ONLY_NETWORK_MAX_DTIM_MULTIPLIER = 2;

    @VisibleForTesting
    static final String CONFIG_IPV4_ONLY_NETWORK_MAX_DTIM_MULTIPLIER =
            "ipclient_ipv4_only_max_dtim_multiplier";
    @VisibleForTesting
    static final int DEFAULT_IPV4_ONLY_NETWORK_MAX_DTIM_MULTIPLIER = 9;

    @VisibleForTesting
    static final String CONFIG_DUAL_STACK_MAX_DTIM_MULTIPLIER =
            "ipclient_dual_stack_max_dtim_multiplier";
    // The default value for dual-stack networks is the min of maximum DTIM multiplier to use for
    // IPv4-only and IPv6-only networks.
    @VisibleForTesting
    static final int DEFAULT_DUAL_STACK_MAX_DTIM_MULTIPLIER = 2;

    @VisibleForTesting
    static final String CONFIG_BEFORE_IPV6_PROV_MAX_DTIM_MULTIPLIER =
            "ipclient_before_ipv6_prov_max_dtim_multiplier";
    @VisibleForTesting
    static final int DEFAULT_BEFORE_IPV6_PROV_MAX_DTIM_MULTIPLIER = 1;

    // Timeout to wait for IPv6 autoconf via SLAAC to complete before starting DHCPv6
    // prefix delegation.
    @VisibleForTesting
    static final String CONFIG_IPV6_AUTOCONF_TIMEOUT = "ipclient_ipv6_autoconf_timeout";
    private static final int DEFAULT_IPV6_AUTOCONF_TIMEOUT_MS = 5000;

    private static final int IPMEMORYSTORE_TIMEOUT_MS = 1000;
    @VisibleForTesting
    static final long SIX_HOURS_IN_MS = 6 * 3600 * 1000L;
    @VisibleForTesting
    public static final long ONE_DAY_IN_MS = 4 * SIX_HOURS_IN_MS;
    @VisibleForTesting
    public static final long ONE_WEEK_IN_MS = 7 * ONE_DAY_IN_MS;
    @VisibleForTesting
    static final String CONFIG_NUD_FAILURE_COUNT_DAILY_THRESHOLD =
            "nud_failure_count_daily_threshold";
    @VisibleForTesting
    static final int DEFAULT_NUD_FAILURE_COUNT_DAILY_THRESHOLD = 10;
    @VisibleForTesting
    static final String CONFIG_NUD_FAILURE_COUNT_WEEKLY_THRESHOLD =
            "nud_failure_count_weekly_threshold";
    @VisibleForTesting
    static final int DEFAULT_NUD_FAILURE_COUNT_WEEKLY_THRESHOLD = 20;

    private static final boolean NO_CALLBACKS = false;
    private static final boolean SEND_CALLBACKS = true;

    private static final int IMMEDIATE_FAILURE_DURATION = 0;

    private static final int PROV_CHANGE_STILL_NOT_PROVISIONED = 1;
    private static final int PROV_CHANGE_LOST_PROVISIONING = 2;
    private static final int PROV_CHANGE_GAINED_PROVISIONING = 3;
    private static final int PROV_CHANGE_STILL_PROVISIONED = 4;

    // onReachabilityFailure callback is added since networkstack-aidl-interfaces-v13.
    @VisibleForTesting
    static final int VERSION_ADDED_REACHABILITY_FAILURE = 13;

    // Specific vendor OUI(3 bytes)/vendor specific type(1 byte) pattern for upstream hotspot
    // device detection. Add new byte array pattern below in turn.
    private static final List<byte[]> METERED_IE_PATTERN_LIST = Collections.singletonList(
            new byte[] { (byte) 0x00, (byte) 0x17, (byte) 0xf2, (byte) 0x06 }
    );

    // Allows Wi-Fi to pass in DHCP options when particular vendor-specific IEs are present.
    // Maps each DHCP option code to a list of IEs, any of which will allow that option.
    private static final Map<Byte, List<byte[]>> DHCP_OPTIONS_ALLOWED = Map.of(
            (byte) 60, Collections.singletonList(
                    // KT OUI: 00:17:C3, type: 33(0x21). See b/236745261.
                    new byte[]{ (byte) 0x00, (byte) 0x17, (byte) 0xc3, (byte) 0x21 }),
            (byte) 77, Collections.singletonList(
                    // KT OUI: 00:17:C3, type: 33(0x21). See b/236745261.
                    new byte[]{ (byte) 0x00, (byte) 0x17, (byte) 0xc3, (byte) 0x21 })
    );

    // Initialize configurable particular SSID set supporting DHCP Roaming feature. See
    // b/131797393 for more details.
    private static final Set<String> DHCP_ROAMING_SSID_SET = new HashSet<>(
            Arrays.asList(
                    "0001docomo", "ollehWiFi", "olleh GiGa WiFi", "KT WiFi",
                    "KT GiGA WiFi", "marente"
    ));

    // The NUD failure event types to query. Although only NETWORK_EVENT_NUD_FAILURE_ORGANIC event
    // is stored in the database currently, this array is still maintained to include other event
    // types for testing and future expansion.
    @VisibleForTesting
    public static final int[] NETWORK_EVENT_NUD_FAILURE_TYPES = new int[] {
            NETWORK_EVENT_NUD_FAILURE_ROAM,
            NETWORK_EVENT_NUD_FAILURE_CONFIRM,
            NETWORK_EVENT_NUD_FAILURE_ORGANIC,
            NETWORK_EVENT_NUD_FAILURE_MAC_ADDRESS_CHANGED
    };

    private final State mStoppedState = new StoppedState();
    private final State mStoppingState = new StoppingState();
    private final State mClearingIpAddressesState = new ClearingIpAddressesState();
    private final State mStartedState = new StartedState();
    private final State mRunningState = new RunningState();
    private final State mPreconnectingState = new PreconnectingState();
    private final State mNudFailureQueryState = new NudFailureQueryState();

    private final String mTag;
    private final Context mContext;
    private final String mInterfaceName;
    @VisibleForTesting
    protected final IpClientCallbacksWrapper mCallback;
    private final Dependencies mDependencies;
    private final ConnectivityManager mCm;
    private final INetd mNetd;
    private final IpClientLinkObserver mLinkObserver;
    private final WakeupMessage mProvisioningTimeoutAlarm;
    private final WakeupMessage mDhcpActionTimeoutAlarm;
    private final SharedLog mLog;
    private final SharedLog mApfLog;
    private final LocalLog mConnectivityPacketLog;
    private final MessageHandlingLogger mMsgStateLogger;
    private final IpConnectivityLog mMetricsLog;
    private final InterfaceController mInterfaceCtrl;
    // Set of IPv6 addresses for which unsolicited gratuitous NA packets have been sent.
    private final Set<Inet6Address> mGratuitousNaTargetAddresses = new HashSet<>();
    // Set of IPv6 addresses from which multicast NS packets have been sent.
    private final Set<Inet6Address> mMulticastNsSourceAddresses = new HashSet<>();
    // Set of delegated prefixes.
    private final Set<IpPrefix> mDelegatedPrefixes = new HashSet<>();
    @Nullable
    private final DevicePolicyManager mDevicePolicyManager;

    // Ignore any nonzero RA section with lifetime below this value.
    private final int mAcceptRaMinLft;

    // Polling interval to update APF data snapshot
    private final long mApfCounterPollingIntervalMs;

    private final int mNudFailureCountDailyThreshold;
    private final int mNudFailureCountWeeklyThreshold;

    // Experiment flag read from device config.
    private final boolean mDhcp6PrefixDelegationEnabled;
    private final boolean mUseNewApfFilter;
    private final boolean mIsAcceptRaMinLftEnabled;
    private final boolean mEnableApfPollingCounters;
    private final boolean mPopulateLinkAddressLifetime;
    private final boolean mApfShouldHandleArpOffload;
    private final boolean mApfShouldHandleNdOffload;
    private final boolean mApfShouldHandleMdnsOffload;
    private final boolean mIgnoreNudFailureEnabled;

    private InterfaceParams mInterfaceParams;

    /**
     * Non-final member variables accessed only from within our StateMachine.
     */
    private LinkProperties mLinkProperties;
    private android.net.shared.ProvisioningConfiguration mConfiguration;
    private IpReachabilityMonitor mIpReachabilityMonitor;
    private DhcpClient mDhcpClient;
    private Dhcp6Client mDhcp6Client;
    private DhcpResults mDhcpResults;
    private String mTcpBufferSizes;
    private ProxyInfo mHttpProxy;
    private AndroidPacketFilter mApfFilter;
    private String mL2Key; // The L2 key for this network, for writing into the memory store
    private String mCluster; // The cluster for this network, for writing into the memory store
    private int mCreatorUid; // Uid of app creating the wifi configuration
    private boolean mMulticastFiltering;
    private long mStartTimeMillis;
    private long mIPv6ProvisioningDtimGracePeriodMillis;
    private MacAddress mCurrentBssid;
    private boolean mHasDisabledAcceptRaDefrtrOnProvLoss;
    private Integer mDadTransmits = null;
    private int mMaxDtimMultiplier = DTIM_MULTIPLIER_RESET;
    private ApfCapabilities mCurrentApfCapabilities;
    private WakeupMessage mIpv6AutoconfTimeoutAlarm = null;
    private boolean mIgnoreNudFailure;
    /**
     * An array of NUD failure event counts retrieved from the memory store  since the timestamps
     * in the past, and is always initialized to null in StoppedState. Currently supported array
     * elements are as follows:
     * element 0: failures in the past week
     * element 1: failures in the past day
     * element 2: failures in the past 6h
     */
    @Nullable
    private int[] mNudFailureEventCounts = null;

    /**
     * The number of NUD failure events that were stored in the memory store since this IpClient
     * was last started. Always set to zero in StoppedState. Used to prevent writing excessive NUD
     * failure events to the memory store.
     */
    private int mNudFailuresStoredSinceStart = 0;

    /**
     * Reading the snapshot is an asynchronous operation initiated by invoking
     * Callback.startReadPacketFilter() and completed when the WiFi Service responds with an
     * EVENT_READ_PACKET_FILTER_COMPLETE message. The mApfDataSnapshotComplete condition variable
     * signals when a new snapshot is ready.
     */
    private final ConditionVariable mApfDataSnapshotComplete = new ConditionVariable();

    public static class Dependencies {
        /**
         * Get interface parameters for the specified interface.
         */
        public InterfaceParams getInterfaceParams(String ifname) {
            return InterfaceParams.getByName(ifname);
        }

        /**
         * Get a INetd connector.
         */
        public INetd getNetd(Context context) {
            return INetd.Stub.asInterface((IBinder) context.getSystemService(Context.NETD_SERVICE));
        }

        /**
         * Get a IpMemoryStore instance.
         */
        public NetworkStackIpMemoryStore getIpMemoryStore(Context context,
                NetworkStackServiceManager nssManager) {
            return new NetworkStackIpMemoryStore(context, nssManager.getIpMemoryStoreService());
        }

        /**
         * Get a DhcpClient instance.
         */
        public DhcpClient makeDhcpClient(Context context, StateMachine controller,
                InterfaceParams ifParams, DhcpClient.Dependencies deps) {
            return DhcpClient.makeDhcpClient(context, controller, ifParams, deps);
        }

        /**
         * Get a Dhcp6Client instance.
         */
        public Dhcp6Client makeDhcp6Client(Context context, StateMachine controller,
                InterfaceParams ifParams, Dhcp6Client.Dependencies deps) {
            return Dhcp6Client.makeDhcp6Client(context, controller, ifParams, deps);
        }

        /**
         * Get a DhcpClient Dependencies instance.
         */
        public DhcpClient.Dependencies getDhcpClientDependencies(
                NetworkStackIpMemoryStore ipMemoryStore, IpProvisioningMetrics metrics) {
            return new DhcpClient.Dependencies(ipMemoryStore, metrics);
        }

        /**
         * Get a Dhcp6Client Dependencies instance.
         */
        public Dhcp6Client.Dependencies getDhcp6ClientDependencies() {
            return new Dhcp6Client.Dependencies();
        }

        /**
         * Read an integer DeviceConfig property.
         */
        public int getDeviceConfigPropertyInt(String name, int defaultValue) {
            return DeviceConfigUtils.getDeviceConfigPropertyInt(NAMESPACE_CONNECTIVITY, name,
                    defaultValue);
        }

        /**
         * Get a IpConnectivityLog instance.
         */
        public IpConnectivityLog getIpConnectivityLog() {
            return new IpConnectivityLog();
        }

        /**
         * Get a NetworkQuirkMetrics instance.
         */
        public NetworkQuirkMetrics getNetworkQuirkMetrics() {
            return new NetworkQuirkMetrics();
        }

        /**
         * Get a IpReachabilityMonitor instance.
         */
        public IpReachabilityMonitor getIpReachabilityMonitor(Context context,
                InterfaceParams ifParams, Handler h, SharedLog log,
                IpReachabilityMonitor.Callback callback, boolean usingMultinetworkPolicyTracker,
                IpReachabilityMonitor.Dependencies deps, final INetd netd) {
            return new IpReachabilityMonitor(context, ifParams, h, log, callback,
                    usingMultinetworkPolicyTracker, deps, netd);
        }

        /**
         * Get a IpReachabilityMonitor dependencies instance.
         */
        public IpReachabilityMonitor.Dependencies getIpReachabilityMonitorDeps(Context context,
                String name) {
            return IpReachabilityMonitor.Dependencies.makeDefault(context, name);
        }

        /**
         * Return whether a feature guarded by a feature flag is enabled.
         * @see DeviceConfigUtils#isNetworkStackFeatureEnabled(Context, String)
         */
        public boolean isFeatureEnabled(final Context context, final String name) {
            return DeviceConfigUtils.isNetworkStackFeatureEnabled(context, name);
        }

        /**
         * Check whether one specific feature is not disabled.
         * @see DeviceConfigUtils#isNetworkStackFeatureNotChickenedOut(Context, String)
         */
        public boolean isFeatureNotChickenedOut(final Context context, final String name) {
            return DeviceConfigUtils.isNetworkStackFeatureNotChickenedOut(context, name);
        }

        /**
         * Create an APF filter if apfCapabilities indicates support for packet filtering using
         * APF programs.
         * @see ApfFilter#maybeCreate
         */
        public AndroidPacketFilter maybeCreateApfFilter(Handler handler, Context context,
                ApfFilter.ApfConfiguration config, InterfaceParams ifParams,
                IpClientCallbacksWrapper cb, NetworkQuirkMetrics networkQuirkMetrics,
                boolean useNewApfFilter) {
            if (useNewApfFilter) {
                return ApfFilter.maybeCreate(handler, context, config, ifParams, cb,
                        networkQuirkMetrics);
            } else {
                return LegacyApfFilter.maybeCreate(context, config, ifParams, cb,
                        networkQuirkMetrics);
            }
        }

        /**
         * Check if a specific IPv6 sysctl file exists or not.
         */
        public boolean hasIpv6Sysctl(final String ifname, final String name) {
            final String path = "/proc/sys/net/ipv6/conf/" + ifname + "/" + name;
            final File sysctl = new File(path);
            return sysctl.exists();
        }

        /**
         * Get the configuration from RRO to check whether or not to send domain search list
         * option in DHCPDISCOVER/DHCPREQUEST message.
         */
        public boolean getSendDomainSearchListOption(final Context context) {
            return context.getResources().getBoolean(R.bool.config_dhcp_client_domain_search_list);
        }

        /**
         * Create an IpClientNetlinkMonitor instance.
         */
        public IpClientNetlinkMonitor makeIpClientNetlinkMonitor(Handler h, SharedLog log,
                String tag, int sockRcvbufSize, INetlinkMessageProcessor p) {
            return new IpClientNetlinkMonitor(h, log, tag, sockRcvbufSize, p);
        }
    }

    public IpClient(Context context, String ifName, IIpClientCallbacks callback,
            NetworkStackServiceManager nssManager) {
        this(context, ifName, callback, nssManager, new Dependencies());
    }

    @VisibleForTesting
    public IpClient(Context context, String ifName, IIpClientCallbacks callback,
            NetworkStackServiceManager nssManager, Dependencies deps) {
        super(IpClient.class.getSimpleName() + "." + ifName);
        Objects.requireNonNull(ifName);
        Objects.requireNonNull(callback);

        mTag = getName();

        mDevicePolicyManager = (DevicePolicyManager)
                context.getSystemService(Context.DEVICE_POLICY_SERVICE);
        mContext = context;
        mInterfaceName = ifName;
        mDependencies = deps;
        mMetricsLog = deps.getIpConnectivityLog();
        mNetworkQuirkMetrics = deps.getNetworkQuirkMetrics();
        mCm = mContext.getSystemService(ConnectivityManager.class);
        mIpMemoryStore = deps.getIpMemoryStore(context, nssManager);

        sSmLogs.putIfAbsent(mInterfaceName, new SharedLog(MAX_LOG_RECORDS, mTag));
        mLog = sSmLogs.get(mInterfaceName);
        sPktLogs.putIfAbsent(mInterfaceName, new LocalLog(MAX_PACKET_RECORDS));
        mConnectivityPacketLog = sPktLogs.get(mInterfaceName);
        sApfLogs.putIfAbsent(mInterfaceName, new SharedLog(10 /* maxRecords */, mTag));
        mApfLog = sApfLogs.get(mInterfaceName);
        mApfDebug = Log.isLoggable(ApfFilter.class.getSimpleName(), Log.DEBUG);
        mMsgStateLogger = new MessageHandlingLogger();
        mCallback = new IpClientCallbacksWrapper(callback, mLog, mApfLog, mShim, mApfDebug);

        // TODO: Consider creating, constructing, and passing in some kind of
        // InterfaceController.Dependencies class.
        mNetd = deps.getNetd(mContext);
        mInterfaceCtrl = new InterfaceController(mInterfaceName, mNetd, mLog);

        mDhcp6PrefixDelegationEnabled = mDependencies.isFeatureEnabled(mContext,
                IPCLIENT_DHCPV6_PREFIX_DELEGATION_VERSION);

        mAcceptRaMinLft = mDependencies.getDeviceConfigPropertyInt(CONFIG_ACCEPT_RA_MIN_LFT,
                DEFAULT_ACCEPT_RA_MIN_LFT);
        mApfCounterPollingIntervalMs = mDependencies.getDeviceConfigPropertyInt(
                CONFIG_APF_COUNTER_POLLING_INTERVAL_SECS,
                DEFAULT_APF_COUNTER_POLLING_INTERVAL_SECS) * DateUtils.SECOND_IN_MILLIS;
        mUseNewApfFilter = SdkLevel.isAtLeastV() || mDependencies.isFeatureNotChickenedOut(context,
                APF_NEW_RA_FILTER_VERSION);
        mEnableApfPollingCounters = mDependencies.isFeatureEnabled(context,
                APF_POLLING_COUNTERS_VERSION);
        mIsAcceptRaMinLftEnabled =
                SdkLevel.isAtLeastV() || mDependencies.isFeatureEnabled(context,
                        IPCLIENT_IGNORE_LOW_RA_LIFETIME_VERSION);
        mApfShouldHandleArpOffload = mDependencies.isFeatureNotChickenedOut(
                mContext, APF_HANDLE_ARP_OFFLOAD);
        mApfShouldHandleNdOffload = mDependencies.isFeatureNotChickenedOut(
                mContext, APF_HANDLE_ND_OFFLOAD);
        // TODO: turn on APF mDNS offload.
        mApfShouldHandleMdnsOffload = false;
        mPopulateLinkAddressLifetime = mDependencies.isFeatureEnabled(context,
                IPCLIENT_POPULATE_LINK_ADDRESS_LIFETIME_VERSION);
        mIgnoreNudFailureEnabled = mDependencies.isFeatureEnabled(mContext,
                IP_REACHABILITY_IGNORE_NUD_FAILURE_VERSION);
        mNudFailureCountDailyThreshold = mDependencies.getDeviceConfigPropertyInt(
                CONFIG_NUD_FAILURE_COUNT_DAILY_THRESHOLD,
                DEFAULT_NUD_FAILURE_COUNT_DAILY_THRESHOLD);
        mNudFailureCountWeeklyThreshold = mDependencies.getDeviceConfigPropertyInt(
                CONFIG_NUD_FAILURE_COUNT_WEEKLY_THRESHOLD,
                DEFAULT_NUD_FAILURE_COUNT_WEEKLY_THRESHOLD);

        IpClientLinkObserver.Configuration config = new IpClientLinkObserver.Configuration(
                mAcceptRaMinLft, mPopulateLinkAddressLifetime);

        mLinkObserver = new IpClientLinkObserver(
                mContext, getHandler(),
                mInterfaceName,
                new IpClientLinkObserver.Callback() {
                    @Override
                    public void update(boolean linkState) {
                        sendMessage(EVENT_NETLINK_LINKPROPERTIES_CHANGED, linkState
                                ? ARG_LINKPROP_CHANGED_LINKSTATE_UP
                                : ARG_LINKPROP_CHANGED_LINKSTATE_DOWN);
                    }

                    @Override
                    public void onIpv6AddressRemoved(final Inet6Address address) {
                        // The update of Gratuitous NA target addresses set or unsolicited
                        // multicast NS source addresses set should be only accessed from the
                        // handler thread of IpClient StateMachine. This can be done by either
                        // sending a message to StateMachine or posting a handler.
                        if (address.isLinkLocalAddress()) return;
                        getHandler().post(() -> {
                            mLog.log("Remove IPv6 GUA " + address
                                    + " from both Gratuituous NA and Multicast NS sets");
                            mGratuitousNaTargetAddresses.remove(address);
                            mMulticastNsSourceAddresses.remove(address);
                        });
                    }

                    @Override
                    public void onClatInterfaceStateUpdate(boolean add) {
                        getHandler().post(() -> {
                            if (mHasSeenClatInterface == add) return;
                            // If Apf is not supported or Apf doesn't support ND offload, then
                            // configure the vendor ND offload feature based on the Clat
                            // interface state.
                            if (mApfFilter == null || !mApfFilter.supportNdOffload()) {
                                // Clat interface information is spliced into LinkProperties by
                                // ConnectivityService, so it cannot be added to the LinkProperties
                                // here as those propagate back to ConnectivityService.
                                mCallback.setNeighborDiscoveryOffload(add ? false : true);
                            }
                            mHasSeenClatInterface = add;
                            if (mApfFilter != null) {
                                mApfFilter.updateClatInterfaceState(add);
                            }
                        });
                    }
                },
                config, mLog, mDependencies
        );

        mLinkProperties = new LinkProperties();
        mLinkProperties.setInterfaceName(mInterfaceName);

        mProvisioningTimeoutAlarm = new WakeupMessage(mContext, getHandler(),
                mTag + ".EVENT_PROVISIONING_TIMEOUT", EVENT_PROVISIONING_TIMEOUT);
        mDhcpActionTimeoutAlarm = new WakeupMessage(mContext, getHandler(),
                mTag + ".EVENT_DHCPACTION_TIMEOUT", EVENT_DHCPACTION_TIMEOUT);

        // Anything the StateMachine may access must have been instantiated
        // before this point.
        configureAndStartStateMachine();

        // Anything that may send messages to the StateMachine must only be
        // configured to do so after the StateMachine has started (above).
        startStateMachineUpdaters();
    }

    /**
     * Make a IIpClient connector to communicate with this IpClient.
     */
    public IIpClient makeConnector() {
        return new IpClientConnector();
    }

    class IpClientConnector extends IIpClient.Stub {
        @Override
        public void completedPreDhcpAction() {
            enforceNetworkStackCallingPermission();
            IpClient.this.completedPreDhcpAction();
        }
        @Override
        public void confirmConfiguration() {
            enforceNetworkStackCallingPermission();
            IpClient.this.confirmConfiguration();
        }
        @Override
        public void readPacketFilterComplete(byte[] data) {
            enforceNetworkStackCallingPermission();
            IpClient.this.readPacketFilterComplete(data);
        }
        @Override
        public void shutdown() {
            enforceNetworkStackCallingPermission();
            IpClient.this.shutdown();
        }
        @Override
        public void startProvisioning(ProvisioningConfigurationParcelable req) {
            enforceNetworkStackCallingPermission();
            IpClient.this.startProvisioning(ProvisioningConfiguration.fromStableParcelable(req,
                    mCallback.getInterfaceVersion()));
        }
        @Override
        public void stop() {
            enforceNetworkStackCallingPermission();
            IpClient.this.stop();
        }
        @Override
        public void setL2KeyAndGroupHint(String l2Key, String cluster) {
            enforceNetworkStackCallingPermission();
            // This method is not supported anymore. The caller should call
            // #updateLayer2Information() instead.
        }
        @Override
        public void setTcpBufferSizes(String tcpBufferSizes) {
            enforceNetworkStackCallingPermission();
            IpClient.this.setTcpBufferSizes(tcpBufferSizes);
        }
        @Override
        public void setHttpProxy(ProxyInfo proxyInfo) {
            enforceNetworkStackCallingPermission();
            IpClient.this.setHttpProxy(proxyInfo);
        }
        @Override
        public void setMulticastFilter(boolean enabled) {
            enforceNetworkStackCallingPermission();
            IpClient.this.setMulticastFilter(enabled);
        }
        @Override
        public void addKeepalivePacketFilter(int slot, TcpKeepalivePacketDataParcelable pkt) {
            enforceNetworkStackCallingPermission();
            IpClient.this.addKeepalivePacketFilter(slot, pkt);
        }
        @Override
        public void addNattKeepalivePacketFilter(int slot, NattKeepalivePacketDataParcelable pkt) {
            enforceNetworkStackCallingPermission();
            IpClient.this.addNattKeepalivePacketFilter(slot, pkt);
        }
        @Override
        public void removeKeepalivePacketFilter(int slot) {
            enforceNetworkStackCallingPermission();
            IpClient.this.removeKeepalivePacketFilter(slot);
        }
        @Override
        public void notifyPreconnectionComplete(boolean success) {
            enforceNetworkStackCallingPermission();
            IpClient.this.notifyPreconnectionComplete(success);
        }
        @Override
        public void updateLayer2Information(Layer2InformationParcelable info) {
            enforceNetworkStackCallingPermission();
            IpClient.this.updateLayer2Information(info);
        }
        @Override
        public void updateApfCapabilities(ApfCapabilities apfCapabilities) {
            enforceNetworkStackCallingPermission();
            IpClient.this.updateApfCapabilities(apfCapabilities);
        }

        @Override
        public int getInterfaceVersion() {
            return this.VERSION;
        }

        @Override
        public String getInterfaceHash() {
            return this.HASH;
        }
    }

    public String getInterfaceName() {
        return mInterfaceName;
    }

    private void configureAndStartStateMachine() {
        // CHECKSTYLE:OFF IndentationCheck
        addState(mStoppedState);
        addState(mStartedState);
            addState(mPreconnectingState, mStartedState);
            addState(mNudFailureQueryState, mStartedState);
            addState(mClearingIpAddressesState, mStartedState);
            addState(mRunningState, mStartedState);
        addState(mStoppingState);
        // CHECKSTYLE:ON IndentationCheck

        setInitialState(mStoppedState);

        super.start();
    }

    private void startStateMachineUpdaters() {
    }

    private void stopStateMachineUpdaters() {
        mLinkObserver.clearInterfaceParams();
        mLinkObserver.shutdown();
    }

    private boolean isGratuitousArpNaRoamingEnabled() {
        return mDependencies.isFeatureNotChickenedOut(mContext, IPCLIENT_GARP_NA_ROAMING_VERSION);
    }

    @VisibleForTesting
    static MacAddress getInitialBssid(final Layer2Information layer2Info,
            final ScanResultInfo scanResultInfo, boolean isAtLeastS) {
        MacAddress bssid = null;
        // http://b/185202634
        // ScanResultInfo is not populated in some situations.
        // On S and above, prefer getting the BSSID from the Layer2Info.
        // On R and below, get the BSSID from the ScanResultInfo and fall back to
        // getting it from the Layer2Info. This ensures no regressions if any R
        // devices pass in a null or meaningless BSSID in the Layer2Info.
        if (!isAtLeastS && scanResultInfo != null) {
            try {
                bssid = MacAddress.fromString(scanResultInfo.getBssid());
            } catch (IllegalArgumentException e) {
                Log.wtf(TAG, "Invalid BSSID: " + scanResultInfo.getBssid()
                        + " in provisioning configuration", e);
            }
        }
        if (bssid == null && layer2Info != null) {
            bssid = layer2Info.mBssid;
        }
        return bssid;
    }

    @Override
    protected void onQuitting() {
        mCallback.onQuit();
    }

    /**
     * Shut down this IpClient instance altogether.
     */
    public void shutdown() {
        stop();
        sendMessage(CMD_TERMINATE_AFTER_STOP);
    }

    /**
     * Start provisioning with the provided parameters.
     */
    public void startProvisioning(ProvisioningConfiguration req) {
        if (!req.isValid()) {
            doImmediateProvisioningFailure(IpManagerEvent.ERROR_INVALID_PROVISIONING);
            return;
        }

        mCurrentBssid = getInitialBssid(req.mLayer2Info, req.mScanResultInfo,
                ShimUtils.isAtLeastS());
        mCurrentApfCapabilities = req.mApfCapabilities;
        mCreatorUid = req.mCreatorUid;
        if (req.mLayer2Info != null) {
            mL2Key = req.mLayer2Info.mL2Key;
            mCluster = req.mLayer2Info.mCluster;
        }
        sendMessage(CMD_START, new android.net.shared.ProvisioningConfiguration(req));
    }

    /**
     * Stop this IpClient.
     *
     * <p>This does not shut down the StateMachine itself, which is handled by {@link #shutdown()}.
     *    The message "arg1" parameter is used to record the disconnect code metrics.
     *    Usually this method is called by the peer (e.g. wifi) intentionally to stop IpClient,
     *    consider that's the normal user termination.
     */
    public void stop() {
        sendMessage(CMD_STOP, DisconnectCode.DC_NORMAL_TERMINATION.getNumber());
    }

    /**
     * Confirm the provisioning configuration.
     */
    public void confirmConfiguration() {
        sendMessage(CMD_CONFIRM);
    }

    /**
     * For clients using {@link ProvisioningConfiguration.Builder#withPreDhcpAction()}, must be
     * called after {@link IIpClientCallbacks#onPreDhcpAction} to indicate that DHCP is clear to
     * proceed.
     */
    public void completedPreDhcpAction() {
        sendMessage(EVENT_PRE_DHCP_ACTION_COMPLETE);
    }

    /**
     * Indicate that packet filter read is complete.
     */
    public void readPacketFilterComplete(byte[] data) {
        sendMessage(EVENT_READ_PACKET_FILTER_COMPLETE, data);
    }

    /**
     * Set the TCP buffer sizes to use.
     *
     * This may be called, repeatedly, at any time before or after a call to
     * #startProvisioning(). The setting is cleared upon calling #stop().
     */
    public void setTcpBufferSizes(String tcpBufferSizes) {
        sendMessage(CMD_UPDATE_TCP_BUFFER_SIZES, tcpBufferSizes);
    }

    /**
     * Set the HTTP Proxy configuration to use.
     *
     * This may be called, repeatedly, at any time before or after a call to
     * #startProvisioning(). The setting is cleared upon calling #stop().
     */
    public void setHttpProxy(ProxyInfo proxyInfo) {
        sendMessage(CMD_UPDATE_HTTP_PROXY, proxyInfo);
    }

    /**
     * Enable or disable the multicast filter.  Attempts to use APF to accomplish the filtering,
     * if not, Callback.setFallbackMulticastFilter() is called.
     */
    public void setMulticastFilter(boolean enabled) {
        sendMessage(CMD_SET_MULTICAST_FILTER, enabled);
    }

    /**
     * Called by WifiStateMachine to add TCP keepalive packet filter before setting up
     * keepalive offload.
     */
    public void addKeepalivePacketFilter(int slot, @NonNull TcpKeepalivePacketDataParcelable pkt) {
        sendMessage(CMD_ADD_KEEPALIVE_PACKET_FILTER_TO_APF, slot, 0 /* Unused */, pkt);
    }

    /**
     *  Called by WifiStateMachine to add NATT keepalive packet filter before setting up
     *  keepalive offload.
     */
    public void addNattKeepalivePacketFilter(int slot,
            @NonNull NattKeepalivePacketDataParcelable pkt) {
        sendMessage(CMD_ADD_KEEPALIVE_PACKET_FILTER_TO_APF, slot, 0 /* Unused */ , pkt);
    }

    /**
     * Called by WifiStateMachine to remove keepalive packet filter after stopping keepalive
     * offload.
     */
    public void removeKeepalivePacketFilter(int slot) {
        sendMessage(CMD_REMOVE_KEEPALIVE_PACKET_FILTER_FROM_APF, slot, 0 /* Unused */);
    }

    /**
     * Notify IpClient that preconnection is complete and that the link is ready for use.
     * The success parameter indicates whether the packets passed in by onPreconnectionStart were
     * successfully sent to the network or not.
     */
    public void notifyPreconnectionComplete(boolean success) {
        sendMessage(CMD_COMPLETE_PRECONNECTION, success ? 1 : 0);
    }

    /**
     * Update the network bssid, L2Key and cluster on L2 roaming happened.
     */
    public void updateLayer2Information(@NonNull Layer2InformationParcelable info) {
        sendMessage(CMD_UPDATE_L2INFORMATION, info);
    }

    /**
     * Update the APF capabilities.
     *
     * This method will update the APF capabilities used in IpClient and decide if a new APF
     * program should be installed to filter the incoming packets based on that. So far this
     * method only allows for the APF capabilities to go from null to non-null, and no other
     * changes are allowed. One use case is when WiFi interface switches from secondary to
     * primary in STA+STA mode.
     */
    public void updateApfCapabilities(@NonNull ApfCapabilities apfCapabilities) {
        sendMessage(CMD_UPDATE_APF_CAPABILITIES, apfCapabilities);
    }

    /**
     * Dump logs of this IpClient.
     */
    public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
        if (args != null && args.length > 0 && DUMP_ARG_CONFIRM.equals(args[0])) {
            // Execute confirmConfiguration() and take no further action.
            confirmConfiguration();
            return;
        }

        // Thread-unsafe access to mApfFilter but just used for debugging.
        final AndroidPacketFilter apfFilter = mApfFilter;
        final android.net.shared.ProvisioningConfiguration provisioningConfig = mConfiguration;
        final ApfCapabilities apfCapabilities = mCurrentApfCapabilities;

        IndentingPrintWriter pw = new IndentingPrintWriter(writer, "  ");
        pw.println(mTag + " APF dump:");
        pw.increaseIndent();
        if (apfFilter != null) {
            if (apfCapabilities != null && apfFilter.hasDataAccess(
                    apfCapabilities.apfVersionSupported)) {
                // Request a new snapshot, then wait for it.
                mApfDataSnapshotComplete.close();
                mCallback.startReadPacketFilter("dumpsys");
                if (!mApfDataSnapshotComplete.block(1000)) {
                    pw.print("TIMEOUT: DUMPING STALE APF SNAPSHOT");
                }
            }
            final Handler handler = getHandler();
            if (handler == null) {
                // This situation is unexpected. The getHandler() function should not return null
                // unless the IpClient has stopped running. When the IpClient exits the RunningState
                // , it should have already set apfFilter to null.
                pw.println("ApfFilter is not null even if IpClient is not running.");
            } else {
                HandlerUtils.runWithScissorsForDump(handler, () -> apfFilter.dump(pw),
                        10_000 /* ms */);
            }
            pw.println("APF log:");
            pw.println("mApfDebug: " + mApfDebug);
            mApfLog.dump(fd, pw, args);
        } else {
            pw.print("No active ApfFilter; ");
            if (provisioningConfig == null) {
                pw.println("IpClient not yet started.");
            } else if (apfCapabilities == null || apfCapabilities.apfVersionSupported == 0) {
                pw.println("Hardware does not support APF.");
            } else {
                pw.println("ApfFilter not yet started, APF capabilities: " + apfCapabilities);
            }
        }
        pw.decreaseIndent();
        pw.println();
        pw.println(mTag + " current ProvisioningConfiguration:");
        pw.increaseIndent();
        pw.println(Objects.toString(provisioningConfig, "N/A"));
        pw.decreaseIndent();

        final IpReachabilityMonitor iprm = mIpReachabilityMonitor;
        if (iprm != null) {
            pw.println();
            pw.println(mTag + " current IpReachabilityMonitor state:");
            pw.increaseIndent();
            iprm.dump(pw);
            pw.decreaseIndent();
        }

        pw.println();
        pw.println(mTag + " StateMachine dump:");
        pw.increaseIndent();
        mLog.dump(fd, pw, args);
        pw.decreaseIndent();

        pw.println();
        pw.println(mTag + " connectivity packet log:");
        pw.println();
        pw.println("Debug with python and scapy via:");
        pw.println("shell$ python");
        pw.println(">>> from scapy import all as scapy");
        pw.println(">>> scapy.Ether(\"<paste_hex_string>\".decode(\"hex\")).show2()");
        pw.println();

        pw.increaseIndent();
        mConnectivityPacketLog.readOnlyLocalLog().dump(fd, pw, args);
        pw.decreaseIndent();
    }

    /**
     * Handle "adb shell cmd apf" command.
     */
    public String apfShellCommand(String cmd, @Nullable String optarg) {
        final long oneDayInMs = 86400 * 1000;
        if (SystemClock.elapsedRealtime() >= oneDayInMs) {
            throw new IllegalStateException("Error: This test interface requires uptime < 24h");
        }

        // Waiting for a "read" result cannot block the handler thread, since the result gets
        // processed on it. This is test only code, so mApfFilter going away is not a concern.
        if (cmd.equals("read")) {
            if (mApfFilter == null) {
                throw new IllegalStateException("Error: No active APF filter");
            }
            // Request a new snapshot, then wait for it.
            mApfDataSnapshotComplete.close();
            mCallback.startReadPacketFilter("shell command");
            if (!mApfDataSnapshotComplete.block(5000 /* ms */)) {
                throw new RuntimeException("Error: Failed to read APF program");
            }
        }

        final CompletableFuture<String> result = new CompletableFuture<>();

        getHandler().post(() -> {
            try {
                if (mApfFilter == null) {
                    // IpClient has either stopped or the interface does not support APF.
                    throw new IllegalStateException("No active APF filter.");
                }
                switch (cmd) {
                    case "status":
                        result.complete(mApfFilter.isRunning() ? "running" : "paused");
                        break;
                    case "pause":
                        mApfFilter.pause();
                        result.complete("success");
                        break;
                    case "resume":
                        mApfFilter.resume();
                        result.complete("success");
                        break;
                    case "install":
                        Objects.requireNonNull(optarg, "No program provided");
                        if (mApfFilter.isRunning()) {
                            throw new IllegalStateException("APF filter must first be paused");
                        }
                        mCallback.installPacketFilter(HexDump.hexStringToByteArray(optarg));
                        result.complete("success");
                        break;
                    case "capabilities":
                        final StringJoiner joiner = new StringJoiner(",");
                        joiner.add(Integer.toString(mCurrentApfCapabilities.apfVersionSupported));
                        joiner.add(Integer.toString(mCurrentApfCapabilities.maximumApfProgramSize));
                        joiner.add(Integer.toString(mCurrentApfCapabilities.apfPacketFormat));
                        result.complete(joiner.toString());
                        break;
                    case "read":
                        final String snapshot = mApfFilter.getDataSnapshotHexString();
                        Objects.requireNonNull(snapshot, "No data snapshot recorded.");
                        result.complete(snapshot);
                        break;
                    default:
                        throw new IllegalArgumentException("Invalid apf command: " + cmd);
                }
            } catch (Exception e) {
                result.completeExceptionally(e);
            }
        });

        try {
            return result.get(30, TimeUnit.SECONDS);
        } catch (ExecutionException | InterruptedException | TimeoutException e) {
            // completeExceptionally is solely used to return error messages back to the user, so
            // the stack trace is not all that interesting. (A similar argument can be made for
            // InterruptedException). Only extract the message from the checked exception.
            throw new RuntimeException(e.getMessage());
        }
    }

    /**
     * Internals.
     */

    @Override
    protected String getWhatToString(int what) {
        return sWhatToString.get(what, "UNKNOWN: " + Integer.toString(what));
    }

    @Override
    protected String getLogRecString(Message msg) {
        final String logLine = String.format(
                "%s/%d %d %d %s [%s]",
                mInterfaceName, (mInterfaceParams == null) ? -1 : mInterfaceParams.index,
                msg.arg1, msg.arg2, Objects.toString(msg.obj), mMsgStateLogger);

        final String richerLogLine = getWhatToString(msg.what) + " " + logLine;
        mLog.log(richerLogLine);
        if (DBG) {
            Log.d(mTag, richerLogLine);
        }

        mMsgStateLogger.reset();
        return logLine;
    }

    @Override
    protected boolean recordLogRec(Message msg) {
        // Don't log EVENT_NETLINK_LINKPROPERTIES_CHANGED. They can be noisy,
        // and we already log any LinkProperties change that results in an
        // invocation of IpClient.Callback#onLinkPropertiesChange().
        final boolean shouldLog = (msg.what != EVENT_NETLINK_LINKPROPERTIES_CHANGED);
        if (!shouldLog) {
            mMsgStateLogger.reset();
        }
        return shouldLog;
    }

    private void logError(String fmt, Throwable e, Object... args) {
        mLog.e(String.format(fmt, args), e);
    }

    private void logError(String fmt, Object... args) {
        logError(fmt, null, args);
    }

    // This needs to be called with care to ensure that our LinkProperties
    // are in sync with the actual LinkProperties of the interface. For example,
    // we should only call this if we know for sure that there are no IP addresses
    // assigned to the interface, etc.
    private void resetLinkProperties() {
        mLinkObserver.clearLinkProperties();
        mConfiguration = null;
        mDhcpResults = null;
        mTcpBufferSizes = "";
        mHttpProxy = null;

        mLinkProperties = new LinkProperties();
        mLinkProperties.setInterfaceName(mInterfaceName);
    }

    private void recordMetric(final int type) {
        // We may record error metrics prior to starting.
        // Map this to IMMEDIATE_FAILURE_DURATION.
        final long duration = (mStartTimeMillis > 0)
                ? (SystemClock.elapsedRealtime() - mStartTimeMillis)
                : IMMEDIATE_FAILURE_DURATION;
        mMetricsLog.log(mInterfaceName, new IpManagerEvent(type, duration));
    }

    // Record the DisconnectCode and transition to StoppingState.
    private void transitionToStoppingState(final DisconnectCode code) {
        mIpProvisioningMetrics.setDisconnectCode(code);
        transitionTo(mStoppingState);
    }

    // Convert reachability loss reason enum to a string.
    private static String reachabilityLossReasonToString(int reason) {
        switch (reason) {
            case ReachabilityLossReason.ROAM:
                return "reachability_loss_after_roam";
            case ReachabilityLossReason.CONFIRM:
                return "reachability_loss_after_confirm";
            case ReachabilityLossReason.ORGANIC:
                return "reachability_loss_organic";
            default:
                return "unknown";
        }
    }

    private static boolean hasIpv6LinkLocalInterfaceRoute(final LinkProperties lp) {
        for (RouteInfo r : lp.getRoutes()) {
            if (r.getDestination().equals(new IpPrefix("fe80::/64"))
                    && r.getGateway().isAnyLocalAddress()) {
                return true;
            }
        }
        return false;
    }

    private static boolean hasIpv6LinkLocalAddress(final LinkProperties lp) {
        for (LinkAddress address : lp.getLinkAddresses()) {
            if (address.isIpv6() && address.getAddress().isLinkLocalAddress()) {
                return true;
            }
        }
        return false;
    }

    // LinkProperties has a link-local (fe80::xxx) IPv6 address and route to fe80::/64 destination.
    private boolean isIpv6LinkLocalProvisioned(final LinkProperties lp) {
        if (mConfiguration == null
                || mConfiguration.mIPv6ProvisioningMode != PROV_IPV6_LINKLOCAL) return false;
        if (hasIpv6LinkLocalAddress(lp) && hasIpv6LinkLocalInterfaceRoute(lp)) return true;
        return false;
    }

    // For now: use WifiStateMachine's historical notion of provisioned.
    @VisibleForTesting
    boolean isProvisioned(final LinkProperties lp, final InitialConfiguration config) {
        // For historical reasons, we should connect even if all we have is an IPv4
        // address and nothing else. If IPv6 link-local only mode is enabled and
        // it's provisioned without IPv4, then still connecting once IPv6 link-local
        // address is ready to use and route to fe80::/64 destination is up.
        if (lp.hasIpv4Address() || lp.isProvisioned() || isIpv6LinkLocalProvisioned(lp)) {
            return true;
        }
        if (config == null) {
            return false;
        }

        // When an InitialConfiguration is specified, ignore any difference with previous
        // properties and instead check if properties observed match the desired properties.
        return config.isProvisionedBy(lp.getLinkAddresses(), lp.getRoutes());
    }

    // Set "/proc/sys/net/ipv6/conf/${iface}/${name}" with the given specific value.
    private void setIpv6Sysctl(@NonNull final String name, int value) {
        try {
            mNetd.setProcSysNet(INetd.IPV6, INetd.CONF, mInterfaceName,
                    name, Integer.toString(value));
        } catch (Exception e) {
            Log.e(mTag, "Failed to set " + name + " to " + value + ": " + e);
        }
    }

    // Read "/proc/sys/net/ipv6/conf/${iface}/${name}".
    private Integer getIpv6Sysctl(@NonNull final String name) {
        try {
            return Integer.parseInt(mNetd.getProcSysNet(INetd.IPV6, INetd.CONF,
                    mInterfaceName, name));
        } catch (RemoteException | ServiceSpecificException e) {
            logError("Couldn't read " + name + " on " + mInterfaceName, e);
            return null;
        }
    }

    // TODO: Investigate folding all this into the existing static function
    // LinkProperties.compareProvisioning() or some other single function that
    // takes two LinkProperties objects and returns a ProvisioningChange
    // object that is a correct and complete assessment of what changed, taking
    // account of the asymmetries described in the comments in this function.
    // Then switch to using it everywhere (IpReachabilityMonitor, etc.).
    private int compareProvisioning(LinkProperties oldLp, LinkProperties newLp) {
        int delta;
        InitialConfiguration config = mConfiguration != null ? mConfiguration.mInitialConfig : null;
        final boolean wasProvisioned = isProvisioned(oldLp, config);
        final boolean isProvisioned = isProvisioned(newLp, config);

        if (!wasProvisioned && isProvisioned) {
            delta = PROV_CHANGE_GAINED_PROVISIONING;
        } else if (wasProvisioned && isProvisioned) {
            delta = PROV_CHANGE_STILL_PROVISIONED;
        } else if (!wasProvisioned && !isProvisioned) {
            delta = PROV_CHANGE_STILL_NOT_PROVISIONED;
        } else {
            // (wasProvisioned && !isProvisioned)
            //
            // Note that this is true even if we lose a configuration element
            // (e.g., a default gateway) that would not be required to advance
            // into provisioned state. This is intended: if we have a default
            // router and we lose it, that's a sure sign of a problem, but if
            // we connect to a network with no IPv4 DNS servers, we consider
            // that to be a network without DNS servers and connect anyway.
            //
            // See the comment below.
            delta = PROV_CHANGE_LOST_PROVISIONING;
        }

        final boolean lostIPv6 = oldLp.isIpv6Provisioned() && !newLp.isIpv6Provisioned();
        final boolean lostIPv4Address = oldLp.hasIpv4Address() && !newLp.hasIpv4Address();
        final boolean lostIPv6Router = oldLp.hasIpv6DefaultRoute() && !newLp.hasIpv6DefaultRoute();

        // If bad wifi avoidance is disabled, then ignore IPv6 loss of
        // provisioning. Otherwise, when a hotspot that loses Internet
        // access sends out a 0-lifetime RA to its clients, the clients
        // will disconnect and then reconnect, avoiding the bad hotspot,
        // instead of getting stuck on the bad hotspot. http://b/31827713 .
        //
        // This is incorrect because if the hotspot then regains Internet
        // access with a different prefix, TCP connections on the
        // deprecated addresses will remain stuck.
        //
        // Note that we can still be disconnected by IpReachabilityMonitor
        // if the IPv6 default gateway (but not the IPv6 DNS servers; see
        // accompanying code in IpReachabilityMonitor) is unreachable.
        final boolean ignoreIPv6ProvisioningLoss = mHasDisabledAcceptRaDefrtrOnProvLoss
                || (mConfiguration != null && mConfiguration.mUsingMultinetworkPolicyTracker
                        && !mCm.shouldAvoidBadWifi());

        // Additionally:
        //
        // Partial configurations (e.g., only an IPv4 address with no DNS
        // servers and no default route) are accepted as long as DHCPv4
        // succeeds. On such a network, isProvisioned() will always return
        // false, because the configuration is not complete, but we want to
        // connect anyway. It might be a disconnected network such as a
        // Chromecast or a wireless printer, for example.
        //
        // Because on such a network isProvisioned() will always return false,
        // delta will never be LOST_PROVISIONING. So check for loss of
        // provisioning here too.
        if (lostIPv4Address || (lostIPv6 && !ignoreIPv6ProvisioningLoss)) {
            delta = PROV_CHANGE_LOST_PROVISIONING;
        }

        // Additionally:
        //
        // If the previous link properties had a global IPv6 address and an
        // IPv6 default route then also consider the loss of that default route
        // to be a loss of provisioning. See b/27962810.
        if (oldLp.hasGlobalIpv6Address() && (lostIPv6Router && !ignoreIPv6ProvisioningLoss)) {
            // Although link properties have lost IPv6 default route in this case, if IPv4 is still
            // working with appropriate routes and DNS servers, we can keep the current connection
            // without disconnecting from the network, just disable accept_ra_defrtr sysctl on that
            // given network until to the next provisioning.
            //
            // Disabling IPv6 stack will result in all IPv6 connectivity torn down and all IPv6
            // sockets being closed, the non-routable IPv6 DNS servers will be stripped out, so
            // applications will be able to reconnect immediately over IPv4. See b/131781810.
            //
            // Sometimes disabling IPv6 stack can cause other problems(see b/179222860), conversely,
            // disabling accept_ra_defrtr can still keep the interface IPv6 capable, but no longer
            // learns the default router from incoming RA, partial IPv6 connectivity will remain on
            // the interface, through which applications can still communicate locally.
            if (newLp.isIpv4Provisioned()) {
                // Restart ipv6 with accept_ra_defrtr set to 0.
                mInterfaceCtrl.disableIPv6();
                startIPv6(0 /* accept_ra_defrtr */);

                mNetworkQuirkMetrics.setEvent(NetworkQuirkEvent.QE_IPV6_PROVISIONING_ROUTER_LOST);
                mNetworkQuirkMetrics.statsWrite();
                mHasDisabledAcceptRaDefrtrOnProvLoss = true;
                delta = PROV_CHANGE_STILL_PROVISIONED;
                mLog.log("Disabled accept_ra_defrtr sysctl on loss of IPv6 default router");
            } else {
                delta = PROV_CHANGE_LOST_PROVISIONING;
            }
        }

        return delta;
    }

    private void dispatchCallback(int delta, LinkProperties newLp) {
        switch (delta) {
            case PROV_CHANGE_GAINED_PROVISIONING:
                if (DBG) {
                    Log.d(mTag, "onProvisioningSuccess()");
                }
                recordMetric(IpManagerEvent.PROVISIONING_OK);
                mCallback.onProvisioningSuccess(newLp);
                break;

            case PROV_CHANGE_LOST_PROVISIONING:
                if (DBG) {
                    Log.d(mTag, "onProvisioningFailure()");
                }
                recordMetric(IpManagerEvent.PROVISIONING_FAIL);
                mCallback.onProvisioningFailure(newLp);
                break;

            default:
                if (DBG) {
                    Log.d(mTag, "onLinkPropertiesChange()");
                }
                mCallback.onLinkPropertiesChange(newLp);
                break;
        }
    }

    // Updates all IpClient-related state concerned with LinkProperties.
    // Returns a ProvisioningChange for possibly notifying other interested
    // parties that are not fronted by IpClient.
    private int setLinkProperties(LinkProperties newLp) {
        if (mApfFilter != null) {
            mApfFilter.setLinkProperties(newLp);
        }
        if (mIpReachabilityMonitor != null) {
            mIpReachabilityMonitor.updateLinkProperties(newLp);
        }

        int delta = compareProvisioning(mLinkProperties, newLp);
        mLinkProperties = new LinkProperties(newLp);

        if (delta == PROV_CHANGE_GAINED_PROVISIONING) {
            // TODO: Add a proper ProvisionedState and cancel the alarm in
            // its enter() method.
            mProvisioningTimeoutAlarm.cancel();
        }

        return delta;
    }

    private LinkProperties assembleLinkProperties() {
        // [1] Create a new LinkProperties object to populate.
        LinkProperties newLp = new LinkProperties();
        newLp.setInterfaceName(mInterfaceName);

        // [2] Pull in data from netlink:
        //         - IPv4 addresses
        //         - IPv6 addresses
        //         - IPv6 routes
        //         - IPv6 DNS servers
        //
        // N.B.: this is fundamentally race-prone and should be fixed by
        // changing IpClientLinkObserver from a hybrid edge/level model to an
        // edge-only model, or by giving IpClient its own netlink socket(s)
        // so as to track all required information directly.
        LinkProperties netlinkLinkProperties = mLinkObserver.getLinkProperties();
        newLp.setLinkAddresses(netlinkLinkProperties.getLinkAddresses());
        for (RouteInfo route : netlinkLinkProperties.getRoutes()) {
            newLp.addRoute(route);
        }
        addAllReachableDnsServers(newLp, netlinkLinkProperties.getDnsServers());
        mShim.setNat64Prefix(newLp, mShim.getNat64Prefix(netlinkLinkProperties));

        // Check if any link address update from netlink.
        final CompareResult<LinkAddress> results =
                LinkPropertiesUtils.compareAddresses(mLinkProperties, newLp);
        // In the case that there are multiple netlink update events about a global IPv6 address
        // derived from the delegated prefix, a flag-only change event(e.g. due to the duplicate
        // address detection) will cause an identical IP address to be put into both Added and
        // Removed list based on the CompareResult implementation. To prevent a prefix from being
        // mistakenly removed from the delegate prefix list, it is better to always check the
        // removed list before checking the added list(e.g. anyway we can add the removed prefix
        // back again).
        for (LinkAddress la : results.removed) {
            if (mDhcp6PrefixDelegationEnabled && isIpv6StableDelegatedAddress(la)) {
                final IpPrefix prefix = new IpPrefix(la.getAddress(), RFC7421_PREFIX_LENGTH);
                mDelegatedPrefixes.remove(prefix);
            }
            // TODO: remove onIpv6AddressRemoved callback.
        }

        for (LinkAddress la : results.added) {
            if (mDhcp6PrefixDelegationEnabled && isIpv6StableDelegatedAddress(la)) {
                final IpPrefix prefix = new IpPrefix(la.getAddress(), RFC7421_PREFIX_LENGTH);
                mDelegatedPrefixes.add(prefix);
            }
        }

        // [3] Add in data from DHCPv4, if available.
        //
        // mDhcpResults is never shared with any other owner so we don't have
        // to worry about concurrent modification.
        if (mDhcpResults != null) {
            final List<RouteInfo> routes =
                    mDhcpResults.toStaticIpConfiguration().getRoutes(mInterfaceName);
            for (RouteInfo route : routes) {
                newLp.addRoute(route);
            }
            addAllReachableDnsServers(newLp, mDhcpResults.dnsServers);
            if (mDhcpResults.dmnsrchList.size() == 0) {
                newLp.setDomains(mDhcpResults.domains);
            } else {
                final String domainsString = mDhcpResults.appendDomainsSearchList();
                newLp.setDomains(TextUtils.isEmpty(domainsString) ? null : domainsString);
            }

            if (mDhcpResults.mtu != 0) {
                newLp.setMtu(mDhcpResults.mtu);
            }

            if (mDhcpResults.serverAddress != null) {
                mShim.setDhcpServerAddress(newLp, mDhcpResults.serverAddress);
            }

            final String capportUrl = mDhcpResults.captivePortalApiUrl;
            // Uri.parse does no syntax check; do a simple check to eliminate garbage.
            // If the URL is still incorrect data fetching will fail later, which is fine.
            if (isParseableUrl(capportUrl)) {
                NetworkInformationShimImpl.newInstance()
                        .setCaptivePortalApiUrl(newLp, Uri.parse(capportUrl));
            }
            // TODO: also look at the IPv6 RA (netlink) for captive portal URL
        }

        // [4] Add route with delegated prefix according to the global address update.
        if (mDhcp6PrefixDelegationEnabled) {
            for (IpPrefix destination : mDelegatedPrefixes) {
                // Direct-connected route to delegated prefix. Add RTN_UNREACHABLE to
                // this route based on the delegated prefix. To prevent the traffic loop
                // between host and upstream delegated router. Because we specify the
                // IFA_F_NOPREFIXROUTE when adding the IPv6 address, the kernel does not
                // create a delegated prefix route, as a result, the user space won't
                // receive any RTM_NEWROUTE message about the delegated prefix, we still
                // need to install an unreachable route for the delegated prefix manually
                // in LinkProperties to notify the caller this update.
                // TODO: support RTN_BLACKHOLE in netd and use that on newer Android
                // versions.
                final RouteInfo route = new RouteInfo(destination,
                        null /* gateway */, mInterfaceName, RTN_UNREACHABLE);
                newLp.addRoute(route);
            }
        }

        // [5] Add in TCP buffer sizes and HTTP Proxy config, if available.
        if (!TextUtils.isEmpty(mTcpBufferSizes)) {
            newLp.setTcpBufferSizes(mTcpBufferSizes);
        }
        if (mHttpProxy != null) {
            newLp.setHttpProxy(mHttpProxy);
        }

        // [6] Add data from InitialConfiguration
        if (mConfiguration != null && mConfiguration.mInitialConfig != null) {
            InitialConfiguration config = mConfiguration.mInitialConfig;
            // Add InitialConfiguration routes and dns server addresses once all addresses
            // specified in the InitialConfiguration have been observed with Netlink.
            if (config.isProvisionedBy(newLp.getLinkAddresses(), null)) {
                for (IpPrefix prefix : config.directlyConnectedRoutes) {
                    newLp.addRoute(new RouteInfo(prefix, null, mInterfaceName, RTN_UNICAST));
                }
            }
            addAllReachableDnsServers(newLp, config.dnsServers);
        }
        final LinkProperties oldLp = mLinkProperties;
        if (DBG) {
            Log.d(mTag, String.format("Netlink-seen LPs: %s, new LPs: %s; old LPs: %s",
                    netlinkLinkProperties, newLp, oldLp));
        }

        // TODO: also learn via netlink routes specified by an InitialConfiguration and specified
        // from a static IP v4 config instead of manually patching them in in steps [3] and [5].
        return newLp;
    }

    private static boolean isParseableUrl(String url) {
        // Verify that a URL has a reasonable format that can be parsed as per the URL constructor.
        // This does not use Patterns.WEB_URL as that pattern excludes URLs without TLDs, such as on
        // localhost.
        if (url == null) return false;
        try {
            new URL(url);
            return true;
        } catch (MalformedURLException e) {
            return false;
        }
    }

    private static void addAllReachableDnsServers(
            LinkProperties lp, Iterable<InetAddress> dnses) {
        // TODO: Investigate deleting this reachability check.  We should be
        // able to pass everything down to netd and let netd do evaluation
        // and RFC6724-style sorting.
        for (InetAddress dns : dnses) {
            if (!dns.isAnyLocalAddress() && lp.isReachable(dns)) {
                lp.addDnsServer(dns);
            }
        }
    }

    private void transmitPacket(final ByteBuffer packet, final SocketAddress sockAddress,
            final String msg) {
        FileDescriptor sock = null;
        try {
            sock = Os.socket(AF_PACKET, SOCK_RAW | SOCK_NONBLOCK, 0 /* protocol */);
            Os.sendto(sock, packet.array(), 0 /* byteOffset */, packet.limit() /* byteCount */,
                    0 /* flags */, sockAddress);
        } catch (SocketException | ErrnoException e) {
            logError(msg, e);
        } finally {
            SocketUtils.closeSocketQuietly(sock);
        }
    }

    private void sendGratuitousNA(final Inet6Address srcIp, final Inet6Address targetIp) {
        final int flags = 0; // R=0, S=0, O=0
        final Inet6Address dstIp = IPV6_ADDR_ALL_ROUTERS_MULTICAST;
        // Ethernet multicast destination address: 33:33:00:00:00:02.
        final MacAddress dstMac = NetworkStackUtils.ipv6MulticastToEthernetMulticast(dstIp);
        final ByteBuffer packet = NeighborAdvertisement.build(mInterfaceParams.macAddr, dstMac,
                srcIp, dstIp, flags, targetIp);
        final SocketAddress sockAddress =
                SocketUtilsShimImpl.newInstance().makePacketSocketAddress(ETH_P_IPV6,
                        mInterfaceParams.index, dstMac.toByteArray());

        transmitPacket(packet, sockAddress, "Failed to send Gratuitous Neighbor Advertisement");
    }

    private void sendGratuitousARP(final Inet4Address srcIp) {
        final ByteBuffer packet = ArpPacket.buildArpPacket(ETHER_BROADCAST /* dstMac */,
                mInterfaceParams.macAddr.toByteArray() /* srcMac */,
                srcIp.getAddress() /* targetIp */,
                ETHER_BROADCAST /* targetHwAddress */,
                srcIp.getAddress() /* senderIp */, (short) ARP_REPLY);
        final SocketAddress sockAddress =
                makePacketSocketAddress(ETH_P_ARP, mInterfaceParams.index);

        transmitPacket(packet, sockAddress, "Failed to send GARP");
    }

    private void sendMulticastNs(final Inet6Address srcIp, final Inet6Address dstIp,
            final Inet6Address targetIp) {
        final MacAddress dstMac = NetworkStackUtils.ipv6MulticastToEthernetMulticast(dstIp);
        final ByteBuffer packet = NeighborSolicitation.build(mInterfaceParams.macAddr, dstMac,
                srcIp, dstIp, targetIp);
        final SocketAddress sockAddress =
                SocketUtilsShimImpl.newInstance().makePacketSocketAddress(ETH_P_IPV6,
                        mInterfaceParams.index, dstMac.toByteArray());

        if (DBG) {
            mLog.log("send multicast NS from " + srcIp.getHostAddress() + " to "
                    + dstIp.getHostAddress() + " , target IP: " + targetIp.getHostAddress());
        }
        transmitPacket(packet, sockAddress, "Failed to send multicast Neighbor Solicitation");
    }

    @Nullable
    private static Inet6Address getIpv6LinkLocalAddress(final LinkProperties newLp) {
        for (LinkAddress la : newLp.getLinkAddresses()) {
            if (!la.isIpv6()) continue;
            final Inet6Address ip = (Inet6Address) la.getAddress();
            if (ip.isLinkLocalAddress()) return ip;
        }
        return null;
    }

    private void maybeSendGratuitousNAs(final LinkProperties lp, boolean afterRoaming) {
        if (!lp.hasGlobalIpv6Address()) return;

        final Inet6Address srcIp = getIpv6LinkLocalAddress(lp);
        if (srcIp == null) return;

        // TODO: add experiment with sending only one gratuitous NA packet instead of one
        // packet per address.
        for (LinkAddress la : lp.getLinkAddresses()) {
            if (!NetworkStackUtils.isIPv6GUA(la)) continue;
            final Inet6Address targetIp = (Inet6Address) la.getAddress();
            // Already sent gratuitous NA with this target global IPv6 address. But for
            // the L2 roaming case, device should always (re)transmit Gratuitous NA for
            // each IPv6 global unicast address respectively after roaming.
            if (!afterRoaming && mGratuitousNaTargetAddresses.contains(targetIp)) continue;
            if (DBG) {
                mLog.log("send Gratuitous NA from " + srcIp.getHostAddress() + " for "
                        + targetIp.getHostAddress() + (afterRoaming ? " after roaming" : ""));
            }
            sendGratuitousNA(srcIp, targetIp);
            if (!afterRoaming) {
                mGratuitousNaTargetAddresses.add(targetIp);
            }
        }
    }

    private void maybeSendGratuitousARP(final LinkProperties lp) {
        for (LinkAddress address : lp.getLinkAddresses()) {
            if (address.getAddress() instanceof Inet4Address) {
                final Inet4Address srcIp = (Inet4Address) address.getAddress();
                if (DBG) {
                    mLog.log("send GARP for " + srcIp.getHostAddress() + " HW address: "
                            + mInterfaceParams.macAddr);
                }
                sendGratuitousARP(srcIp);
            }
        }
    }

    @Nullable
    private static Inet6Address getIPv6DefaultGateway(final LinkProperties lp) {
        for (RouteInfo r : lp.getRoutes()) {
            // TODO: call {@link RouteInfo#isIPv6Default} directly after core networking modules
            // are consolidated.
            if (r.getType() == RTN_UNICAST && r.getDestination().getPrefixLength() == 0
                    && r.getDestination().getAddress() instanceof Inet6Address) {
                // Check if it's IPv6 default route, if yes, return the gateway address
                // (i.e. default router's IPv6 link-local address)
                return (Inet6Address) r.getGateway();
            }
        }
        return null;
    }

    private void maybeSendMulticastNSes(final LinkProperties lp) {
        if (!(lp.hasGlobalIpv6Address() && lp.hasIpv6DefaultRoute())) return;

        // Get the default router's IPv6 link-local address.
        final Inet6Address targetIp = getIPv6DefaultGateway(lp);
        if (targetIp == null) return;
        final Inet6Address dstIp = NetworkStackUtils.ipv6AddressToSolicitedNodeMulticast(targetIp);
        if (dstIp == null) return;

        for (LinkAddress la : lp.getLinkAddresses()) {
            if (!NetworkStackUtils.isIPv6GUA(la)) continue;
            final Inet6Address srcIp = (Inet6Address) la.getAddress();
            if (mMulticastNsSourceAddresses.contains(srcIp)) continue;
            sendMulticastNs(srcIp, dstIp, targetIp);
            mMulticastNsSourceAddresses.add(srcIp);
        }
    }

    private static boolean hasFlag(@NonNull final LinkAddress la, final int flags) {
        return (la.getFlags() & flags) == flags;

    }

    // Check whether a global IPv6 stable address is derived from DHCPv6 prefix delegation.
    // Address derived from delegated prefix should be:
    // - unicast global routable address
    // - with prefix length of 64
    // - has IFA_F_MANAGETEMPADDR, IFA_F_NOPREFIXROUTE and IFA_F_NODAD flags
    private static boolean isIpv6StableDelegatedAddress(@NonNull final LinkAddress la) {
        return la.isIpv6()
                && !ConnectivityUtils.isIPv6ULA(la.getAddress())
                && (la.getPrefixLength() == RFC7421_PREFIX_LENGTH)
                && (la.getScope() == (byte) RT_SCOPE_UNIVERSE)
                && hasFlag(la, DHCPV6_PREFIX_DELEGATION_ADDRESS_FLAGS);
    }

    // Returns false if we have lost provisioning, true otherwise.
    private boolean handleLinkPropertiesUpdate(boolean sendCallbacks) {
        final LinkProperties newLp = assembleLinkProperties();

        // We need to call mApfFilter.setLinkProperties(newLp) every time there is a LinkAddress
        // change because ApfFilter needs to know when addresses change from tentative to
        // non-tentative. setLinkProperties() inside IpClient won't be called if the
        // LinkProperties.equal() check returns true. The LinkProperties.equal() check does not
        // currently take into account the LinkAddress flag change.
        // It is OK to call mApfFilter.setLinkProperties() multiple times because if IP
        // addresses are not updated, ApfFilter won't generate new program.
        if (mApfFilter != null) {
            mApfFilter.setLinkProperties(newLp);
        }

        if (Objects.equals(newLp, mLinkProperties)) {
            return true;
        }

        // Set an alarm to wait for IPv6 autoconf via SLAAC to succeed after receiving an RA,
        // if we don't see global IPv6 address within timeout then start DHCPv6 Prefix Delegation
        // for provisioning. We cannot just check if there is an available on-link IPv6 DNS server
        // in the LinkProperties, because on-link IPv6 DNS server won't be updated to LP until
        // we have a global IPv6 address via PD. Instead, we have to check if the IPv6 default
        // route exists and start DHCPv6 Prefix Delegation process if IPv6 provisioning still
        // doesn't complete with success after timeout. This check also handles IPv6-only link
        // local mode case, since there will be no IPv6 default route in that mode even with Prefix
        // Delegation experiment flag enabled.
        if (mDhcp6PrefixDelegationEnabled
                && newLp.hasIpv6DefaultRoute()
                && mIpv6AutoconfTimeoutAlarm == null) {
            mIpv6AutoconfTimeoutAlarm = new WakeupMessage(mContext, getHandler(),
                    mTag + ".EVENT_IPV6_AUTOCONF_TIMEOUT", EVENT_IPV6_AUTOCONF_TIMEOUT);
            final long alarmTime = SystemClock.elapsedRealtime()
                    + mDependencies.getDeviceConfigPropertyInt(CONFIG_IPV6_AUTOCONF_TIMEOUT,
                            DEFAULT_IPV6_AUTOCONF_TIMEOUT_MS);
            mIpv6AutoconfTimeoutAlarm.schedule(alarmTime);
        }

        // Check if new assigned IPv6 GUA is available in the LinkProperties now. If so, initiate
        // gratuitous multicast unsolicited Neighbor Advertisements as soon as possible to inform
        // first-hop routers that the new GUA host is goning to use.
        maybeSendGratuitousNAs(newLp, false /* isGratuitousNaAfterRoaming */);

        // Sending multicast NS from each new assigned IPv6 GUAs to the solicited-node multicast
        // address based on the default router's IPv6 link-local address should trigger default
        // router response with NA, and update the neighbor cache entry immediately, that would
        // help speed up the connection to an IPv6-only network.
        //
        // TODO: stop sending this multicast NS after deployment of RFC9131 in the field, leverage
        // the gratuitous NA to update the first-hop router's neighbor cache entry.
        maybeSendMulticastNSes(newLp);

        final boolean gainedV6 = !mLinkProperties.isIpv6Provisioned() && newLp.isIpv6Provisioned();
        // mDelegatedPrefixes is updated as part of the call to assembleLinkProperties() above.
        if (gainedV6 && !mDelegatedPrefixes.isEmpty()) {
            mNetworkQuirkMetrics.setEvent(QE_DHCP6_PD_PROVISIONED);
            mNetworkQuirkMetrics.statsWrite();
        }

        // Either success IPv4 or IPv6 provisioning triggers new LinkProperties update,
        // wait for the provisioning completion and record the latency.
        mIpProvisioningMetrics.setIPv4ProvisionedLatencyOnFirstTime(newLp.isIpv4Provisioned());
        mIpProvisioningMetrics.setIPv6ProvisionedLatencyOnFirstTime(newLp.isIpv6Provisioned());

        final int delta = setLinkProperties(newLp);
        // Most of the attributes stored in the memory store are deduced from
        // the link properties, therefore when the properties update the memory
        // store record should be updated too.
        maybeSaveNetworkToIpMemoryStore();
        if (sendCallbacks) {
            dispatchCallback(delta, newLp);
            // We cannot do this along with onProvisioningSuccess callback, because the network
            // can become dual-stack after a success IPv6 provisioning, and the multiplier also
            // needs to be updated upon the loss of IPv4 and/or IPv6 provisioning. The multiplier
            // has been initialized with DTIM_MULTIPLIER_RESET before starting provisioning, it
            // gets updated on the first LinkProperties update (which usually happens when the
            // IPv6 link-local address appears).
            updateMaxDtimMultiplier();
        }
        return (delta != PROV_CHANGE_LOST_PROVISIONING);
    }

    @VisibleForTesting
    static String removeDoubleQuotes(@NonNull String ssid) {
        final int length = ssid.length();
        if ((length > 1) && (ssid.charAt(0) == '"') && (ssid.charAt(length - 1) == '"')) {
            return ssid.substring(1, length - 1);
        }
        return ssid;
    }

    private static List<ByteBuffer> getVendorSpecificIEs(@NonNull ScanResultInfo scanResultInfo) {
        ArrayList<ByteBuffer> vendorSpecificPayloadList = new ArrayList<>();
        for (InformationElement ie : scanResultInfo.getInformationElements()) {
            if (ie.getId() == VENDOR_SPECIFIC_IE_ID) {
                vendorSpecificPayloadList.add(ie.getPayload());
            }
        }
        return vendorSpecificPayloadList;
    }

    private boolean checkIfOuiAndTypeMatched(@NonNull ScanResultInfo scanResultInfo,
            @NonNull List<byte[]> patternList) {
        final List<ByteBuffer> vendorSpecificPayloadList = getVendorSpecificIEs(scanResultInfo);

        for (ByteBuffer payload : vendorSpecificPayloadList) {
            byte[] ouiAndType = new byte[4];
            try {
                payload.get(ouiAndType);
            } catch (BufferUnderflowException e) {
                Log.e(mTag, "Couldn't parse vendor specific IE, buffer underflow");
                return false;
            }
            for (byte[] pattern : patternList) {
                if (Arrays.equals(pattern, ouiAndType)) {
                    if (DBG) {
                        Log.d(mTag, "match pattern: " + HexDump.toHexString(ouiAndType));
                    }
                    return true;
                }
            }
        }
        return false;
    }

    private boolean detectUpstreamHotspotFromVendorIe() {
        final ScanResultInfo scanResultInfo = mConfiguration.mScanResultInfo;
        if (scanResultInfo == null) return false;
        final String ssid = scanResultInfo.getSsid();

        if (mConfiguration.mDisplayName == null
                || !removeDoubleQuotes(mConfiguration.mDisplayName).equals(ssid)) {
            return false;
        }
        return checkIfOuiAndTypeMatched(scanResultInfo, METERED_IE_PATTERN_LIST);
    }

    private void handleIPv4Success(DhcpResults dhcpResults) {
        mDhcpResults = new DhcpResults(dhcpResults);
        final LinkProperties newLp = assembleLinkProperties();
        final int delta = setLinkProperties(newLp);

        if (mDhcpResults.vendorInfo == null && detectUpstreamHotspotFromVendorIe()) {
            mDhcpResults.vendorInfo = DhcpPacket.VENDOR_INFO_ANDROID_METERED;
        }

        if (DBG) {
            Log.d(mTag, "onNewDhcpResults(" + Objects.toString(mDhcpResults) + ")");
            Log.d(mTag, "handleIPv4Success newLp{" + newLp + "}");
        }
        mCallback.onNewDhcpResults(mDhcpResults);
        maybeSaveNetworkToIpMemoryStore();

        dispatchCallback(delta, newLp);
    }

    private void handleIPv4Failure() {
        // TODO: Investigate deleting this clearIPv4Address() call.
        //
        // DhcpClient will send us CMD_CLEAR_LINKADDRESS in all circumstances
        // that could trigger a call to this function. If we missed handling
        // that message in StartedState for some reason we would still clear
        // any addresses upon entry to StoppedState.
        mInterfaceCtrl.clearIPv4Address();
        mDhcpResults = null;
        if (DBG) {
            Log.d(mTag, "onNewDhcpResults(null)");
        }
        mCallback.onNewDhcpResults(null);

        handleProvisioningFailure(DisconnectCode.DC_PROVISIONING_FAIL);
    }

    private void handleProvisioningFailure(final DisconnectCode code) {
        final LinkProperties newLp = assembleLinkProperties();
        int delta = setLinkProperties(newLp);
        // If we've gotten here and we're still not provisioned treat that as
        // a total loss of provisioning.
        //
        // Either (a) static IP configuration failed or (b) DHCPv4 failed AND
        // there was no usable IPv6 obtained before a non-zero provisioning
        // timeout expired.
        //
        // Regardless: GAME OVER.
        if (delta == PROV_CHANGE_STILL_NOT_PROVISIONED) {
            delta = PROV_CHANGE_LOST_PROVISIONING;
        }

        dispatchCallback(delta, newLp);
        if (delta == PROV_CHANGE_LOST_PROVISIONING) {
            transitionToStoppingState(code);
        }
    }

    private void doImmediateProvisioningFailure(int failureType) {
        logError("onProvisioningFailure(): %s", failureType);
        recordMetric(failureType);
        mCallback.onProvisioningFailure(mLinkProperties);
    }

    @SuppressLint("NewApi") // TODO: b/193460475 remove once fixed
    private boolean startIPv4() {
        // If we have a StaticIpConfiguration attempt to apply it and
        // handle the result accordingly.
        if (mConfiguration.mStaticIpConfig != null) {
            if (mInterfaceCtrl.setIPv4Address(mConfiguration.mStaticIpConfig.getIpAddress())) {
                handleIPv4Success(new DhcpResults(mConfiguration.mStaticIpConfig));
            } else {
                return false;
            }
        } else {
            if (mDhcpClient != null) {
                Log.wtf(mTag, "DhcpClient should never be non-null in startIPv4()");
            }
            startDhcpClient();
        }

        return true;
    }

    private boolean shouldDisableDad() {
        return mConfiguration.mUniqueEui64AddressesOnly
                && mConfiguration.mIPv6ProvisioningMode == PROV_IPV6_LINKLOCAL
                && mConfiguration.mIPv6AddrGenMode
                        == ProvisioningConfiguration.IPV6_ADDR_GEN_MODE_EUI64;
    }

    private boolean startIPv6(int acceptRaDefrtr) {
        setIpv6Sysctl(ACCEPT_RA,
                mConfiguration.mIPv6ProvisioningMode == PROV_IPV6_LINKLOCAL ? 0 : 2);
        setIpv6Sysctl(ACCEPT_RA_DEFRTR, acceptRaDefrtr);
        if (shouldDisableDad()) {
            final Integer dadTransmits = getIpv6Sysctl(DAD_TRANSMITS);
            if (dadTransmits != null) {
                mDadTransmits = dadTransmits;
                setIpv6Sysctl(DAD_TRANSMITS, 0 /* dad_transmits */);
            }
        }
        return mInterfaceCtrl.setIPv6PrivacyExtensions(true)
                && mInterfaceCtrl.setIPv6AddrGenModeIfSupported(mConfiguration.mIPv6AddrGenMode)
                && mInterfaceCtrl.enableIPv6();
    }

    private void startDhcp6PrefixDelegation() {
        if (!mDhcp6PrefixDelegationEnabled) return;
        if (mDhcp6Client != null) {
            Log.wtf(mTag, "Dhcp6Client should never be non-null in startDhcp6PrefixDelegation");
            return;
        }
        mDhcp6Client = mDependencies.makeDhcp6Client(mContext, IpClient.this, mInterfaceParams,
                mDependencies.getDhcp6ClientDependencies());
        mDhcp6Client.sendMessage(Dhcp6Client.CMD_START_DHCP6);
    }

    private boolean applyInitialConfig(InitialConfiguration config) {
        // TODO: also support specifying a static IPv4 configuration in InitialConfiguration.
        for (LinkAddress addr : findAll(config.ipAddresses, LinkAddress::isIpv6)) {
            if (!mInterfaceCtrl.addAddress(addr)) return false;
        }

        return true;
    }

    // In order to avoid overflowing the database (the maximum is 10MB) in case of a NUD failure
    // happens frequently (e.g, every 30s in a broken network), we stop writing the NUD failure
    // event to database if the total event count in past 6h, plus the number of events written
    // since IpClient was started, has exceeded the daily threshold.
    //
    // The code also counts the number of events written since this IpClient was last started.
    // Otherwise, if NUD failures are already being ignored due to a (daily or weekly) threshold
    // being hit by events that happened more than 6 hours ago, but there have been no failures in
    // the last 6 hours, the code would never stop logging failures (filling up the memory store)
    // until IpClient is restarted and queries the memory store again.
    //
    // The 6-hour count is still useful, even though the code looks at the number of NUD failures
    // since IpClient was last started, because it ensures that even if the network disconnects and
    // reconnects frequently for any other reason, the code will never store more than 10 NUD
    // failures every 6 hours.
    private boolean shouldStopWritingNudFailureEventToDatabase() {
        // NUD failure query has not completed yet.
        if (mNudFailureEventCounts == null) return true;
        return mNudFailureEventCounts[2] + mNudFailuresStoredSinceStart
                >= mNudFailureCountDailyThreshold;
    }

    private void maybeStoreNudFailureToDatabase(final NudEventType type) {
        if (!mIgnoreNudFailureEnabled) return;
        final int event = IpReachabilityMonitor.nudEventTypeToNetworkEvent(type);
        // So far only NUD failure events due to organic kernel check are stored, which can be
        // expanded to other causes later if necessary.
        if (event != NETWORK_EVENT_NUD_FAILURE_ORGANIC) return;
        if (shouldStopWritingNudFailureEventToDatabase()) return;

        final long now = System.currentTimeMillis();
        final long expiry = now + ONE_WEEK_IN_MS;
        mIpMemoryStore.storeNetworkEvent(mCluster, now, expiry, event,
                status -> {
                    if (!status.isSuccess()) {
                        Log.e(TAG, "Failed to store NUD failure event");
                    }
                });
        mNudFailuresStoredSinceStart++;
        if (DBG) {
            Log.d(TAG, "store network event " + type
                    + " at " + now
                    + " expire at " + expiry
                    + " with cluster " + mCluster);
        }
    }

    private boolean startIpReachabilityMonitor() {
        try {
            mIpReachabilityMonitor = mDependencies.getIpReachabilityMonitor(
                    mContext,
                    mInterfaceParams,
                    getHandler(),
                    mLog,
                    new IpReachabilityMonitor.Callback() {
                        @Override
                        public void notifyLost(String logMsg, NudEventType type) {
                            maybeStoreNudFailureToDatabase(type);
                            if (mIgnoreNudFailure) {
                                Counter.logIncrement("core_networking.value_nud_failure_ignored");
                                return;
                            }
                            final int version = mCallback.getInterfaceVersion();
                            if (version >= VERSION_ADDED_REACHABILITY_FAILURE) {
                                final int reason = nudEventTypeToInt(type);
                                if (reason == INVALID_REACHABILITY_LOSS_TYPE) return;
                                final ReachabilityLossInfoParcelable lossInfo =
                                        new ReachabilityLossInfoParcelable(logMsg, reason);
                                mCallback.onReachabilityFailure(lossInfo);
                            } else {
                                mCallback.onReachabilityLost(logMsg);
                            }
                        }
                    },
                    mConfiguration.mUsingMultinetworkPolicyTracker,
                    mDependencies.getIpReachabilityMonitorDeps(mContext, mInterfaceParams.name),
                    mNetd);
        } catch (IllegalArgumentException iae) {
            // Failed to start IpReachabilityMonitor. Log it and call
            // onProvisioningFailure() immediately.
            //
            // See http://b/31038971.
            logError("IpReachabilityMonitor failure: %s", iae);
            mIpReachabilityMonitor = null;
        }

        return (mIpReachabilityMonitor != null);
    }

    private void stopAllIP() {
        // We don't need to worry about routes, just addresses, because:
        //     - disableIpv6() will clear autoconf IPv6 routes as well, and
        //     - we don't get IPv4 routes from netlink
        // so we neither react to nor need to wait for changes in either.
        mInterfaceCtrl.disableIPv6();
        mInterfaceCtrl.clearAllAddresses();

        // Reset IPv6 sysctls to their initial state. It's better to restore
        // sysctls after IPv6 stack is disabled, which prevents a potential
        // race where receiving an RA between restoring accept_ra and disabling
        // IPv6 stack, although it's unlikely.
        setIpv6Sysctl(ACCEPT_RA, 2);
        setIpv6Sysctl(ACCEPT_RA_DEFRTR, 1);
        maybeRestoreDadTransmits();
        if (mUseNewApfFilter && mIsAcceptRaMinLftEnabled
                && mDependencies.hasIpv6Sysctl(mInterfaceName, ACCEPT_RA_MIN_LFT)) {
            setIpv6Sysctl(ACCEPT_RA_MIN_LFT, 0 /* sysctl default */);
        }
    }

    private void maybeSaveNetworkToIpMemoryStore() {
        // TODO : implement this
    }

    private void maybeRestoreInterfaceMtu() {
        InterfaceParams params = mDependencies.getInterfaceParams(mInterfaceName);
        if (params == null) {
            Log.w(mTag, "interface: " + mInterfaceName + " is gone");
            return;
        }

        // Check whether "mInterfaceParams" is null or not to prevent the potential NPE
        // introduced if the interface was initially not found, but came back before this
        // method was called. See b/162808916 for more details. TODO: query the new interface
        // parameters by the interface index instead and check that the index has not changed.
        if (mInterfaceParams == null || params.index != mInterfaceParams.index) {
            Log.w(mTag, "interface: " + mInterfaceName + " has a different index: " + params.index);
            return;
        }

        if (params.defaultMtu == mInterfaceParams.defaultMtu) return;

        try {
            mNetd.interfaceSetMtu(mInterfaceName, mInterfaceParams.defaultMtu);
        } catch (RemoteException | ServiceSpecificException e) {
            logError("Couldn't reset MTU on " + mInterfaceName + " from "
                    + params.defaultMtu + " to " + mInterfaceParams.defaultMtu, e);
        }
    }

    private void maybeRestoreDadTransmits() {
        if (mDadTransmits == null) return;

        setIpv6Sysctl(DAD_TRANSMITS, mDadTransmits);
        mDadTransmits = null;
    }

    private void handleUpdateL2Information(@NonNull Layer2InformationParcelable info) {
        mL2Key = info.l2Key;
        mCluster = info.cluster;

        // Sometimes the wifi code passes in a null BSSID. Don't use Log.wtf in R because
        // it's a known bug that will not be fixed in R.
        if (info.bssid == null || mCurrentBssid == null) {
            final String msg = "bssid in the parcelable: " + info.bssid + " or "
                    + "current tracked bssid: " + mCurrentBssid + " is null";
            if (ShimUtils.isAtLeastS()) {
                Log.wtf(mTag, msg);
            } else {
                Log.w(mTag, msg);
            }
            return;
        }

        // If the BSSID has not changed, there is nothing to do.
        if (info.bssid.equals(mCurrentBssid)) return;

        // Before trigger probing to the critical neighbors, send Gratuitous ARP
        // and Neighbor Advertisment in advance to propgate host's IPv4/v6 addresses.
        if (isGratuitousArpNaRoamingEnabled()) {
            maybeSendGratuitousARP(mLinkProperties);
            maybeSendGratuitousNAs(mLinkProperties, true /* isGratuitousNaAfterRoaming */);
        }

        // Check whether attempting to refresh previous IP lease on specific networks or need to
        // probe the critical neighbors proactively on L2 roaming happened. The NUD probe on the
        // specific networks is cancelled because otherwise the probe will happen in parallel with
        // DHCP refresh, it will be difficult to understand what happened exactly and error-prone
        // to introduce race condition.
        final String ssid = removeDoubleQuotes(mConfiguration.mDisplayName);
        if (DHCP_ROAMING_SSID_SET.contains(ssid) && mDhcpClient != null) {
            if (DBG) {
                Log.d(mTag, "L2 roaming happened from " + mCurrentBssid
                        + " to " + info.bssid
                        + " , SSID: " + ssid
                        + " , starting refresh leased IP address");
            }
            mDhcpClient.sendMessage(DhcpClient.CMD_REFRESH_LINKADDRESS);
        } else if (mIpReachabilityMonitor != null) {
            mIpReachabilityMonitor.probeAll(true /* dueToRoam */);
        }
        mCurrentBssid = info.bssid;
    }

    @Nullable
    private AndroidPacketFilter maybeCreateApfFilter(final ApfCapabilities apfCaps) {
        ApfFilter.ApfConfiguration apfConfig = new ApfFilter.ApfConfiguration();
        if (apfCaps == null) {
            return null;
        }
        // For now only support generating programs for Ethernet frames. If this restriction is
        // lifted the program generator will need its offsets adjusted.
        if (apfCaps.apfPacketFormat != ARPHRD_ETHER) return null;
        if (SdkLevel.isAtLeastS()) {
            apfConfig.apfVersionSupported = apfCaps.apfVersionSupported;
        } else {
            // In Android R, ApfCapabilities#hasDataAccess() can be modified by OEMs. The
            // ApfFilter logic uses ApfCapabilities.apfVersionSupported to determine whether
            // data region access is supported. Therefore, we need to recalculate
            // ApfCapabilities.apfVersionSupported based on the return value of
            // ApfCapabilities#hasDataAccess().
            apfConfig.apfVersionSupported = apfCaps.hasDataAccess() ? 3 : 2;
        }
        apfConfig.apfRamSize = apfCaps.maximumApfProgramSize;
        if (!SdkLevel.isAtLeastV() && apfConfig.apfVersionSupported <= 4) {
            apfConfig.installableProgramSizeClamp = 1024;
        }
        apfConfig.multicastFilter = mMulticastFiltering;
        // Get the Configuration for ApfFilter from Context
        // Resource settings were moved from ApfCapabilities APIs to NetworkStack resources in S
        if (ShimUtils.isAtLeastS()) {
            final Resources res = mContext.getResources();
            apfConfig.ieee802_3Filter = res.getBoolean(R.bool.config_apfDrop802_3Frames);
            apfConfig.ethTypeBlackList = res.getIntArray(R.array.config_apfEthTypeDenyList);
        } else {
            apfConfig.ieee802_3Filter = ApfCapabilities.getApfDrop8023Frames();
            apfConfig.ethTypeBlackList = ApfCapabilities.getApfEtherTypeBlackList();
        }

        // The RDNSS option is not processed by the kernel, so lifetime filtering
        // can occur independent of kernel support for accept_ra_min_lft.
        apfConfig.minRdnssLifetimeSec = mAcceptRaMinLft;
        // Check the feature flag first before reading IPv6 sysctl, which can prevent from
        // triggering a potential kernel bug about the sysctl.
        // TODO: add unit test to check if the setIpv6Sysctl() is called or not.
        if (mIsAcceptRaMinLftEnabled && mUseNewApfFilter
                && mDependencies.hasIpv6Sysctl(mInterfaceName, ACCEPT_RA_MIN_LFT)) {
            setIpv6Sysctl(ACCEPT_RA_MIN_LFT, mAcceptRaMinLft);
            final Integer acceptRaMinLft = getIpv6Sysctl(ACCEPT_RA_MIN_LFT);
            apfConfig.acceptRaMinLft = acceptRaMinLft == null ? 0 : acceptRaMinLft;
        } else {
            apfConfig.acceptRaMinLft = 0;
        }
        apfConfig.shouldHandleArpOffload = mApfShouldHandleArpOffload;
        apfConfig.shouldHandleNdOffload = mApfShouldHandleNdOffload;
        apfConfig.shouldHandleMdnsOffload = mApfShouldHandleMdnsOffload;
        apfConfig.minMetricsSessionDurationMs = mApfCounterPollingIntervalMs;
        apfConfig.hasClatInterface = mHasSeenClatInterface;
        return mDependencies.maybeCreateApfFilter(getHandler(), mContext, apfConfig,
                mInterfaceParams, mCallback, mNetworkQuirkMetrics, mUseNewApfFilter);
    }

    private boolean handleUpdateApfCapabilities(@NonNull final ApfCapabilities apfCapabilities) {
        // For the use case where the wifi interface switches from secondary to primary, the
        // secondary interface does not support APF by default see the overlay config about
        // {@link config_wifiEnableApfOnNonPrimarySta}. so we should see empty ApfCapabilities
        // in {@link ProvisioningConfiguration} when wifi starts provisioning on the secondary
        // interface. For other cases, we should not accept the updateApfCapabilities call.
        if (mCurrentApfCapabilities != null || apfCapabilities == null) {
            Log.wtf(mTag, "current ApfCapabilities " + mCurrentApfCapabilities
                    + " is not null or new ApfCapabilities " + apfCapabilities + " is null");
            return false;
        }
        if (mApfFilter != null) {
            mApfFilter.shutdown();
        }
        mCurrentApfCapabilities = apfCapabilities;
        return apfCapabilities != null;
    }

    class StoppedState extends State {
        @Override
        public void enter() {
            stopAllIP();
            mHasDisabledAcceptRaDefrtrOnProvLoss = false;
            mGratuitousNaTargetAddresses.clear();
            mMulticastNsSourceAddresses.clear();
            mDelegatedPrefixes.clear();
            mNudFailureEventCounts = null;
            mNudFailuresStoredSinceStart = 0;

            resetLinkProperties();
            if (mStartTimeMillis > 0) {
                // Completed a life-cycle; send a final empty LinkProperties
                // (cleared in resetLinkProperties() above) and record an event.
                mCallback.onLinkPropertiesChange(mLinkProperties);
                recordMetric(IpManagerEvent.COMPLETE_LIFECYCLE);
                mStartTimeMillis = 0;
            }
        }

        @Override
        public boolean processMessage(Message msg) {
            switch (msg.what) {
                case CMD_TERMINATE_AFTER_STOP:
                    stopStateMachineUpdaters();
                    quit();
                    break;

                case CMD_STOP:
                    break;

                case CMD_START:
                    mConfiguration = (android.net.shared.ProvisioningConfiguration) msg.obj;
                    transitionTo(mIgnoreNudFailureEnabled
                            ? mNudFailureQueryState
                            : mClearingIpAddressesState);
                    break;

                case EVENT_NETLINK_LINKPROPERTIES_CHANGED:
                    handleLinkPropertiesUpdate(NO_CALLBACKS);
                    break;

                case CMD_UPDATE_TCP_BUFFER_SIZES:
                    mTcpBufferSizes = (String) msg.obj;
                    handleLinkPropertiesUpdate(NO_CALLBACKS);
                    break;

                case CMD_UPDATE_HTTP_PROXY:
                    mHttpProxy = (ProxyInfo) msg.obj;
                    handleLinkPropertiesUpdate(NO_CALLBACKS);
                    break;

                case CMD_SET_MULTICAST_FILTER:
                    mMulticastFiltering = (boolean) msg.obj;
                    break;

                case DhcpClient.CMD_ON_QUIT:
                case Dhcp6Client.CMD_ON_QUIT:
                    // Everything is already stopped.
                    logError("Unexpected CMD_ON_QUIT (already stopped).");
                    break;

                default:
                    return NOT_HANDLED;
            }

            mMsgStateLogger.handled(this, getCurrentState());
            return HANDLED;
        }
    }

    class StoppingState extends State {
        @Override
        public void enter() {
            if (mDhcpClient == null && mDhcp6Client == null) {
                // There's no DHCPv4 as well as DHCPv6 for which to wait; proceed to stopped
                deferMessage(obtainMessage(CMD_JUMP_STOPPING_TO_STOPPED));
            } else {
                if (mDhcpClient != null) {
                    mDhcpClient.sendMessage(DhcpClient.CMD_STOP_DHCP);
                    mDhcpClient.doQuit();
                }
                if (mDhcp6Client != null) {
                    mDhcp6Client.sendMessage(Dhcp6Client.CMD_STOP_DHCP6);
                    mDhcp6Client.doQuit();
                }
            }

            // Restore the interface MTU to initial value if it has changed.
            maybeRestoreInterfaceMtu();
            // Reset DTIM multiplier to default value if changed.
            if (mMaxDtimMultiplier != DTIM_MULTIPLIER_RESET) {
                mCallback.setMaxDtimMultiplier(DTIM_MULTIPLIER_RESET);
                mMaxDtimMultiplier = DTIM_MULTIPLIER_RESET;
                mIPv6ProvisioningDtimGracePeriodMillis = 0;
            }
        }

        @Override
        public boolean processMessage(Message msg) {
            switch (msg.what) {
                case CMD_JUMP_STOPPING_TO_STOPPED:
                    transitionTo(mStoppedState);
                    break;

                case CMD_STOP:
                    break;

                case DhcpClient.CMD_CLEAR_LINKADDRESS:
                    mInterfaceCtrl.clearIPv4Address();
                    break;

                case DhcpClient.CMD_ON_QUIT:
                    mDhcpClient = null;
                    // DhcpClient always starts no matter of target network type, however, we have
                    // to make sure both of DHCPv4 and DHCPv6 client have quit from state machine
                    // before transition to StoppedState, otherwise, we may miss CMD_ON_QUIT cmd
                    // that arrives later and transit to StoppedState before that.
                    if (mDhcp6Client == null) {
                        transitionTo(mStoppedState);
                    }
                    break;

                case Dhcp6Client.CMD_ON_QUIT:
                    mDhcp6Client = null;
                    if (mDhcpClient == null) {
                        transitionTo(mStoppedState);
                    }
                    break;

                default:
                    deferMessage(msg);
            }

            mMsgStateLogger.handled(this, getCurrentState());
            return HANDLED;
        }
    }

    private boolean isUsingPreconnection() {
        return mConfiguration.mEnablePreconnection && mConfiguration.mStaticIpConfig == null;
    }

    /**
     * Check if the customized DHCP client options passed from Wi-Fi are allowed to be put
     * in PRL or in the DHCP packet.
     */
    private List<DhcpOption> maybeFilterCustomizedDhcpOptions() {
        final List<DhcpOption> options = new ArrayList<DhcpOption>();
        if (mConfiguration.mDhcpOptions == null
                || mConfiguration.mScanResultInfo == null) return options; // empty DhcpOption list

        for (DhcpOption option : mConfiguration.mDhcpOptions) {
            final List<byte[]> patternList = DHCP_OPTIONS_ALLOWED.get(option.type);
            // requested option won't be added if no vendor-specific IE oui/type allows this option.
            if (patternList == null) continue;
            if (checkIfOuiAndTypeMatched(mConfiguration.mScanResultInfo, patternList)) {
                options.add(option);
            }
        }
        Collections.sort(options, (o1, o2) ->
                Integer.compare(Byte.toUnsignedInt(o1.type), Byte.toUnsignedInt(o2.type)));
        return options;
    }

    private void startDhcpClient() {
        // Start DHCPv4.
        mDhcpClient = mDependencies.makeDhcpClient(mContext, IpClient.this, mInterfaceParams,
                mDependencies.getDhcpClientDependencies(mIpMemoryStore, mIpProvisioningMetrics));

        // Check if the vendor-specific IE oui/type matches and filters the customized DHCP options.
        final List<DhcpOption> options = maybeFilterCustomizedDhcpOptions();

        // If preconnection is enabled, there is no need to ask Wi-Fi to disable powersaving
        // during DHCP, because the DHCP handshake will happen during association. In order to
        // ensure that future renews still do the DHCP action (if configured),
        // registerForPreDhcpNotification is called later when processing the CMD_*_PRECONNECTION
        // messages.
        if (!isUsingPreconnection()) mDhcpClient.registerForPreDhcpNotification();
        boolean isManagedWifiProfile = false;
        if (mDependencies.getSendDomainSearchListOption(mContext)
                && (mCreatorUid > 0) && (isDeviceOwnerNetwork(mCreatorUid)
                || isProfileOwner(mCreatorUid))) {
            isManagedWifiProfile = true;
        }
        mDhcpClient.sendMessage(DhcpClient.CMD_START_DHCP, new DhcpClient.Configuration(mL2Key,
                isUsingPreconnection(), options, isManagedWifiProfile,
                mConfiguration.mHostnameSetting, mPopulateLinkAddressLifetime));
    }

    private boolean hasPermission(String permissionName) {
        return (mContext.checkCallingOrSelfPermission(permissionName)
                == PackageManager.PERMISSION_GRANTED);
    }

    private boolean isDeviceOwnerNetwork(int creatorUid) {
        if (mDevicePolicyManager == null) return false;
        if (!hasPermission(android.Manifest.permission.MANAGE_USERS)) return false;
        final ComponentName devicecmpName = mDevicePolicyManager.getDeviceOwnerComponentOnAnyUser();
        if (devicecmpName == null) return false;
        final String deviceOwnerPackageName = devicecmpName.getPackageName();
        if (deviceOwnerPackageName == null) return false;

        final String[] packages = mContext.getPackageManager().getPackagesForUid(creatorUid);

        for (String pkg : packages) {
            if (pkg.equals(deviceOwnerPackageName)) {
                return true;
            }
        }
        return false;
    }

    @Nullable
    private Context createPackageContextAsUser(int uid) {
        Context userContext = null;
        try {
            userContext = mContext.createPackageContextAsUser(mContext.getPackageName(), 0,
                    UserHandle.getUserHandleForUid(uid));
        } catch (PackageManager.NameNotFoundException e) {
            Log.e(TAG, "Unknown package name");
            return null;
        }
        return userContext;
    }

    /**
     * Returns the DevicePolicyManager from context
     */
    private DevicePolicyManager retrieveDevicePolicyManagerFromContext(Context context) {
        DevicePolicyManager devicePolicyManager =
                context.getSystemService(DevicePolicyManager.class);
        if (devicePolicyManager == null
                && context.getPackageManager().hasSystemFeature(
                PackageManager.FEATURE_DEVICE_ADMIN)) {
            Log.wtf(TAG, "Error retrieving DPM service");
        }
        return devicePolicyManager;
    }

    private DevicePolicyManager retrieveDevicePolicyManagerFromUserContext(int uid) {
        Context userContext = createPackageContextAsUser(uid);
        if (userContext == null) return null;
        return retrieveDevicePolicyManagerFromContext(userContext);
    }

    /**
     * Returns {@code true} if the calling {@code uid} is the profile owner
     *
     */

    private boolean isProfileOwner(int uid) {
        DevicePolicyManager devicePolicyManager = retrieveDevicePolicyManagerFromUserContext(uid);
        if (devicePolicyManager == null) return false;
        String[] packages = mContext.getPackageManager().getPackagesForUid(uid);
        if (packages == null) {
            Log.w(TAG, "isProfileOwner: could not find packages for uid="
                    + uid);
            return false;
        }
        for (String packageName : packages) {
            if (devicePolicyManager.isProfileOwnerApp(packageName)) {
                return true;
            }
        }
        return false;
    }

    class ClearingIpAddressesState extends State {
        @Override
        public void enter() {
            // Ensure that interface parameters are fetched on the handler thread so they are
            // properly ordered with other events, such as restoring the interface MTU on teardown.
            mInterfaceParams = mDependencies.getInterfaceParams(mInterfaceName);
            if (mInterfaceParams == null) {
                logError("Failed to find InterfaceParams for " + mInterfaceName);
                doImmediateProvisioningFailure(IpManagerEvent.ERROR_INTERFACE_NOT_FOUND);
                deferMessage(obtainMessage(CMD_STOP,
                        DisconnectCode.DC_INTERFACE_NOT_FOUND.getNumber()));
                return;
            }

            mLinkObserver.setInterfaceParams(mInterfaceParams);

            if (readyToProceed()) {
                deferMessage(obtainMessage(CMD_ADDRESSES_CLEARED));
            } else {
                // Clear all IPv4 and IPv6 before proceeding to RunningState.
                // Clean up any leftover state from an abnormal exit from
                // tethering or during an IpClient restart.
                stopAllIP();
            }

            mCallback.setNeighborDiscoveryOffload(true);
        }

        @Override
        public boolean processMessage(Message msg) {
            switch (msg.what) {
                case CMD_ADDRESSES_CLEARED:
                    transitionTo(isUsingPreconnection() ? mPreconnectingState : mRunningState);
                    break;

                case EVENT_NETLINK_LINKPROPERTIES_CHANGED:
                    handleLinkPropertiesUpdate(NO_CALLBACKS);
                    if (readyToProceed()) {
                        transitionTo(isUsingPreconnection() ? mPreconnectingState : mRunningState);
                    }
                    break;

                case CMD_STOP:
                case EVENT_PROVISIONING_TIMEOUT:
                    // Fall through to StartedState.
                    return NOT_HANDLED;

                default:
                    // It's safe to process messages out of order because the
                    // only message that can both
                    //     a) be received at this time and
                    //     b) affect provisioning state
                    // is EVENT_NETLINK_LINKPROPERTIES_CHANGED (handled above).
                    deferMessage(msg);
            }
            return HANDLED;
        }

        private boolean readyToProceed() {
            return !mLinkProperties.hasIpv4Address() && !mLinkProperties.hasGlobalIpv6Address();
        }
    }

    class PreconnectingState extends State {
        @Override
        public void enter() {
            startDhcpClient();
        }

        @Override
        public boolean processMessage(Message msg) {
            switch (msg.what) {
                case CMD_COMPLETE_PRECONNECTION:
                    boolean success = (msg.arg1 == 1);
                    mDhcpClient.registerForPreDhcpNotification();
                    if (!success) {
                        mDhcpClient.sendMessage(DhcpClient.CMD_ABORT_PRECONNECTION);
                    }
                    // The link is ready for use. Advance to running state, start IPv6, etc.
                    transitionTo(mRunningState);
                    break;

                case DhcpClient.CMD_START_PRECONNECTION:
                    final Layer2PacketParcelable l2Packet = (Layer2PacketParcelable) msg.obj;
                    mCallback.onPreconnectionStart(Collections.singletonList(l2Packet));
                    break;

                case CMD_STOP:
                case EVENT_PROVISIONING_TIMEOUT:
                    // Fall through to StartedState.
                    return NOT_HANDLED;

                default:
                    deferMessage(msg);
            }
            return HANDLED;
        }
    }

    class StartedState extends State {
        @Override
        public void enter() {
            mIpProvisioningMetrics.reset();
            mStartTimeMillis = SystemClock.elapsedRealtime();

            if (mConfiguration.mProvisioningTimeoutMs > 0) {
                final long alarmTime = SystemClock.elapsedRealtime()
                        + mConfiguration.mProvisioningTimeoutMs;
                mProvisioningTimeoutAlarm.schedule(alarmTime);
            }

            // There is no need to temporarlily lower the DTIM multiplier in IPv6 link-local
            // only mode or when IPv6 is disabled.
            if (mConfiguration.mIPv6ProvisioningMode == PROV_IPV6_SLAAC) {
                // Send a delay message to wait for IP provisioning to complete eventually and
                // set the specific DTIM multiplier by checking the target network type.
                final int delay = mDependencies.getDeviceConfigPropertyInt(
                        CONFIG_INITIAL_PROVISIONING_DTIM_DELAY_MS,
                        DEFAULT_INITIAL_PROVISIONING_DTIM_DELAY_MS);
                mIPv6ProvisioningDtimGracePeriodMillis = mStartTimeMillis + delay;
                sendMessageDelayed(CMD_SET_DTIM_MULTIPLIER_AFTER_DELAY, delay);
            }
        }

        @Override
        public void exit() {
            mProvisioningTimeoutAlarm.cancel();
            mCurrentApfCapabilities = null;

            // Record metrics information once this provisioning has completed due to certain
            // reason (normal termination, provisioning timeout, lost provisioning and etc).
            mIpProvisioningMetrics.statsWrite();
        }

        @Override
        public boolean processMessage(Message msg) {
            switch (msg.what) {
                case CMD_STOP:
                    transitionToStoppingState(DisconnectCode.forNumber(msg.arg1));
                    break;

                case CMD_UPDATE_L2INFORMATION:
                    handleUpdateL2Information((Layer2InformationParcelable) msg.obj);
                    break;

                // Only update the current ApfCapabilities but do not create and start APF
                // filter until transition to RunningState, actually we should always do that
                // in RunningState.
                case CMD_UPDATE_APF_CAPABILITIES:
                    handleUpdateApfCapabilities((ApfCapabilities) msg.obj);
                    break;

                case EVENT_PROVISIONING_TIMEOUT:
                    handleProvisioningFailure(DisconnectCode.DC_PROVISIONING_TIMEOUT);
                    break;

                default:
                    return NOT_HANDLED;
            }

            mMsgStateLogger.handled(this, getCurrentState());
            return HANDLED;
        }
    }

    private boolean isIpv6Enabled() {
        return mConfiguration.mIPv6ProvisioningMode != PROV_IPV6_DISABLED;
    }

    private boolean isIpv4Enabled() {
        return mConfiguration.mIPv4ProvisioningMode != PROV_IPV4_DISABLED;
    }

    private boolean shouldIgnoreNudFailure(@NonNull final int[] eventCounts) {
        if (!mIgnoreNudFailureEnabled) return false;
        if (eventCounts.length == 0) return false;

        final int countInPastOneWeek = eventCounts[0];
        final int countInPastOneDay = eventCounts[1];
        return countInPastOneDay >= mNudFailureCountDailyThreshold
                || countInPastOneWeek >= mNudFailureCountWeeklyThreshold;
    }

    class NudFailureQueryState extends State {
        // This listener runs in a different thread (the Executor used in the IpMemoryStoreService)
        // and it needs to be volatile to allow access by other threads than the IpClient state
        // machine handler, which should be fine since it only accesses the mListener and calls
        // sendMessage.
        private volatile OnNetworkEventCountRetrievedListener mListener =
                new OnNetworkEventCountRetrievedListener() {
                    @Override
                    public void onNetworkEventCountRetrieved(Status status, int[] counts) {
                        if (mListener != this) return;
                        if (counts.length == 0) {
                            if (!status.isSuccess()) {
                                Log.e(TAG, "Error retrieving NUD failure event count: " + status);
                            }
                            sendMessage(EVENT_NUD_FAILURE_QUERY_FAILURE);
                            return;
                        }
                        sendMessage(EVENT_NUD_FAILURE_QUERY_SUCCESS, counts);
                    }};

        @Override
        public void enter() {
            super.enter();
            // Set a timeout for retrieving NUD failure event counts.
            sendMessageDelayed(EVENT_NUD_FAILURE_QUERY_TIMEOUT, IPMEMORYSTORE_TIMEOUT_MS);
            final long now = System.currentTimeMillis();
            final long[] sinceTimes = new long[3];
            sinceTimes[0] = now - ONE_WEEK_IN_MS;
            sinceTimes[1] = now - ONE_DAY_IN_MS;
            sinceTimes[2] = now - SIX_HOURS_IN_MS;
            mIpMemoryStore.retrieveNetworkEventCount(mCluster, sinceTimes,
                    NETWORK_EVENT_NUD_FAILURE_TYPES, mListener);
            Counter.logIncrement("core_networking.value_nud_failure_queried");
        }

        @Override
        public boolean processMessage(Message message) {
            switch (message.what) {
                case EVENT_NUD_FAILURE_QUERY_FAILURE:
                case EVENT_NUD_FAILURE_QUERY_TIMEOUT:
                    // TODO: log query result with metrics.
                    transitionTo(mClearingIpAddressesState);
                    return HANDLED;

                case EVENT_NUD_FAILURE_QUERY_SUCCESS:
                    mNudFailureEventCounts = (int[]) message.obj;
                    mIgnoreNudFailure = shouldIgnoreNudFailure(mNudFailureEventCounts);
                    transitionTo(mClearingIpAddressesState);
                    return HANDLED;

                default:
                    deferMessage(message); // e.g. LP updated during this state.
                    return HANDLED;
            }
        }

        @Override
        public void exit() {
            super.exit();
            removeMessages(EVENT_NUD_FAILURE_QUERY_FAILURE);
            removeMessages(EVENT_NUD_FAILURE_QUERY_TIMEOUT);
            removeMessages(EVENT_NUD_FAILURE_QUERY_SUCCESS);
        }
    }

    class RunningState extends State {
        private ConnectivityPacketTracker mPacketTracker;
        private boolean mDhcpActionInFlight;

        @Override
        public void enter() {
            // While it's possible that a stale clat interface still exists when IpClient starts,
            // such an interface would not be used for the network that IpClient is currently
            // running on, so it's OK to ignore it. This means that there is no need to check
            // whether a clat interface exists when IpClient starts - even if one did exist, it's
            // guaranteed to be stale. As a result, mHasSeenClatInterface can always be set to false
            // at the beginning.
            mHasSeenClatInterface = false;
            mApfFilter = maybeCreateApfFilter(mCurrentApfCapabilities);
            // If Apf supports ND offload, then turn off the vendor ND offload feature.
            if (mApfFilter != null && mApfFilter.supportNdOffload()) {
                mCallback.setNeighborDiscoveryOffload(false);
            }
            // TODO: investigate the effects of any multicast filtering racing/interfering with the
            // rest of this IP configuration startup.
            if (mApfFilter == null) {
                mCallback.setFallbackMulticastFilter(mMulticastFiltering);
            }
            if (mEnableApfPollingCounters) {
                sendMessageDelayed(CMD_UPDATE_APF_DATA_SNAPSHOT, mApfCounterPollingIntervalMs);
            }

            mPacketTracker = createPacketTracker();
            if (mPacketTracker != null) mPacketTracker.start(mConfiguration.mDisplayName);

            if (isIpv6Enabled() && !startIPv6(1 /* acceptRaDefrtr */)) {
                doImmediateProvisioningFailure(IpManagerEvent.ERROR_STARTING_IPV6);
                enqueueJumpToStoppingState(DisconnectCode.DC_ERROR_STARTING_IPV6);
                return;
            }

            if (isIpv4Enabled() && !isUsingPreconnection() && !startIPv4()) {
                doImmediateProvisioningFailure(IpManagerEvent.ERROR_STARTING_IPV4);
                enqueueJumpToStoppingState(DisconnectCode.DC_ERROR_STARTING_IPV4);
                return;
            }

            final InitialConfiguration config = mConfiguration.mInitialConfig;
            if ((config != null) && !applyInitialConfig(config)) {
                // TODO introduce a new IpManagerEvent constant to distinguish this error case.
                doImmediateProvisioningFailure(IpManagerEvent.ERROR_INVALID_PROVISIONING);
                enqueueJumpToStoppingState(DisconnectCode.DC_INVALID_PROVISIONING);
                return;
            }

            if (mConfiguration.mUsingIpReachabilityMonitor && !startIpReachabilityMonitor()) {
                doImmediateProvisioningFailure(
                        IpManagerEvent.ERROR_STARTING_IPREACHABILITYMONITOR);
                enqueueJumpToStoppingState(DisconnectCode.DC_ERROR_STARTING_IPREACHABILITYMONITOR);
                return;
            }
        }

        @Override
        public void exit() {
            stopDhcpAction();

            if (mIpv6AutoconfTimeoutAlarm != null) {
                mIpv6AutoconfTimeoutAlarm.cancel();
                mIpv6AutoconfTimeoutAlarm = null;
            }

            if (mIpReachabilityMonitor != null) {
                mIpReachabilityMonitor.stop();
                mIpReachabilityMonitor = null;
            }

            if (mPacketTracker != null) {
                mPacketTracker.stop();
                mPacketTracker = null;
            }

            if (mApfFilter != null) {
                mApfFilter.shutdown();
                mApfFilter = null;
            }

            resetLinkProperties();

            removeMessages(CMD_UPDATE_APF_DATA_SNAPSHOT);
        }

        private void enqueueJumpToStoppingState(final DisconnectCode code) {
            deferMessage(obtainMessage(CMD_JUMP_RUNNING_TO_STOPPING, code.getNumber()));
        }

        private ConnectivityPacketTracker createPacketTracker() {
            try {
                return new ConnectivityPacketTracker(
                        getHandler(), mInterfaceParams, mConnectivityPacketLog);
            } catch (IllegalArgumentException e) {
                return null;
            }
        }

        private void ensureDhcpAction() {
            if (!mDhcpActionInFlight) {
                mCallback.onPreDhcpAction();
                mDhcpActionInFlight = true;
                final long alarmTime = SystemClock.elapsedRealtime()
                        + mConfiguration.mRequestedPreDhcpActionMs;
                mDhcpActionTimeoutAlarm.schedule(alarmTime);
            }
        }

        private void stopDhcpAction() {
            mDhcpActionTimeoutAlarm.cancel();
            if (mDhcpActionInFlight) {
                mCallback.onPostDhcpAction();
                mDhcpActionInFlight = false;
            }
        }

        private void deleteInterfaceAddress(final LinkAddress address) {
            if (!address.isIpv6()) {
                // NetlinkUtils.sendRtmDelAddressRequest does not support deleting IPv4 addresses.
                Log.wtf(TAG, "Deleting IPv4 address not supported " + address);
                return;
            }
            final Inet6Address in6addr = (Inet6Address) address.getAddress();
            final short plen = (short) address.getPrefixLength();
            if (!NetlinkUtils.sendRtmDelAddressRequest(mInterfaceParams.index, in6addr, plen)) {
                Log.e(TAG, "Failed to delete IPv6 address " + address);
            }
        }

        private void deleteIpv6PrefixDelegationAddresses(final IpPrefix prefix) {
            // b/290747921: some kernels require the mngtmpaddr to be deleted first, to prevent the
            // creation of a new tempaddr.
            final List<LinkAddress> linkAddresses = mLinkProperties.getLinkAddresses();
            // delete addresses with IFA_F_MANAGETEMPADDR contained in the prefix.
            for (LinkAddress la : linkAddresses) {
                if (hasFlag(la, IFA_F_MANAGETEMPADDR) && prefix.contains(la.getAddress())) {
                    deleteInterfaceAddress(la);
                }
            }
            // delete all other addresses contained in the prefix.
            for (LinkAddress la : linkAddresses) {
                if (!hasFlag(la, IFA_F_MANAGETEMPADDR) && prefix.contains(la.getAddress())) {
                    deleteInterfaceAddress(la);
                }
            }
        }

        private void addInterfaceAddress(@Nullable final Inet6Address address,
                @NonNull final IaPrefixOption ipo) {
            final int flags = IFA_F_NOPREFIXROUTE | IFA_F_MANAGETEMPADDR | IFA_F_NODAD;
            final long now = SystemClock.elapsedRealtime();
            // Per RFC8415 section 21.22 the preferred/valid lifetime in IA Prefix option
            // expressed in units of seconds.
            final long deprecationTime = now + ipo.preferred * 1000;
            final long expirationTime = now + ipo.valid * 1000;
            final LinkAddress la;
            try {
                la = new LinkAddress(address, RFC7421_PREFIX_LENGTH, flags,
                        RT_SCOPE_UNIVERSE /* scope */, deprecationTime, expirationTime);
            } catch (IllegalArgumentException e) {
                Log.e(TAG, "Invalid IPv6 link address " + e);
                return;
            }
            if (!la.isGlobalPreferred()) {
                Log.w(TAG, la + " is not a global IPv6 address");
                return;
            }
            if (!NetlinkUtils.sendRtmNewAddressRequest(mInterfaceParams.index, address,
                    (short) RFC7421_PREFIX_LENGTH,
                    flags, (byte) RT_SCOPE_UNIVERSE /* scope */,
                    ipo.preferred, ipo.valid)) {
                Log.e(TAG, "Failed to set IPv6 address on " + address.getHostAddress()
                        + "%" + mInterfaceParams.index);
            }
        }

        private void updateDelegatedAddresses(@NonNull final List<IaPrefixOption> valid) {
            if (valid.isEmpty()) return;
            final List<IpPrefix> zeroLifetimePrefixList = new ArrayList<>();
            for (IaPrefixOption ipo : valid) {
                final IpPrefix prefix = ipo.getIpPrefix();
                // The prefix with preferred/valid lifetime of 0 is considered as a valid prefix,
                // and can be passed to IpClient from Dhcp6Client, but client should stop using
                // the global addresses derived from this prefix asap. Deleting the associated
                // global IPv6 addresses immediately before adding another IPv6 address may result
                // in a race where the device throws the provisioning failure callback due to the
                // loss of all valid IPv6 addresses, however, IPv6 provisioning will soon complete
                // successfully when the user space sees the new IPv6 address update. To avoid this
                // race, temporarily store all prefix(es) with 0 preferred/valid lifetime and then
                // delete them after iterating through all valid IA prefix options.
                if (ipo.withZeroLifetimes()) {
                    zeroLifetimePrefixList.add(prefix);
                    continue;
                }
                // Otherwise, configure IPv6 addresses derived from the delegated prefix(es) on
                // the interface. We've checked that delegated prefix is valid upon receiving the
                // response from DHCPv6 server, and the server may assign a prefix with length less
                // than 64. So for SLAAC use case we always set the prefix length to 64 even if the
                // delegated prefix length is less than 64.
                final Inet6Address address = createInet6AddressFromEui64(prefix,
                        macAddressToEui64(mInterfaceParams.macAddr));
                addInterfaceAddress(address, ipo);
            }

            // Delete global IPv6 addresses derived from prefix with 0 preferred/valid lifetime.
            if (!zeroLifetimePrefixList.isEmpty()) {
                for (IpPrefix prefix : zeroLifetimePrefixList) {
                    Log.d(TAG, "Delete IPv6 address derived from prefix " + prefix
                            + " with 0 preferred/valid lifetime");
                    deleteIpv6PrefixDelegationAddresses(prefix);
                }
            }
        }

        private void removeExpiredDelegatedAddresses(@NonNull final List<IaPrefixOption> expired) {
            if (expired.isEmpty()) return;
            for (IaPrefixOption ipo : expired) {
                final IpPrefix prefix = ipo.getIpPrefix();
                Log.d(TAG, "Delete IPv6 address derived from expired prefix " + prefix);
                deleteIpv6PrefixDelegationAddresses(prefix);
            }
        }

        @Override
        public boolean processMessage(Message msg) {
            switch (msg.what) {
                case CMD_JUMP_RUNNING_TO_STOPPING:
                case CMD_STOP:
                    transitionToStoppingState(DisconnectCode.forNumber(msg.arg1));
                    break;

                case CMD_START:
                    logError("ALERT: START received in StartedState. Please fix caller.");
                    break;

                case CMD_CONFIRM:
                    // TODO: Possibly introduce a second type of confirmation
                    // that both probes (a) on-link neighbors and (b) does
                    // a DHCPv4 RENEW.  We used to do this on Wi-Fi framework
                    // roams.
                    if (mIpReachabilityMonitor != null) {
                        mIpReachabilityMonitor.probeAll(false /* dueToRoam */);
                    }
                    break;

                case EVENT_PRE_DHCP_ACTION_COMPLETE:
                    // It's possible to reach here if, for example, someone
                    // calls completedPreDhcpAction() after provisioning with
                    // a static IP configuration.
                    if (mDhcpClient != null) {
                        mDhcpClient.sendMessage(DhcpClient.CMD_PRE_DHCP_ACTION_COMPLETE);
                    }
                    break;

                case EVENT_NETLINK_LINKPROPERTIES_CHANGED:
                    // EVENT_NETLINK_LINKPROPERTIES_CHANGED message will be received in both of
                    // provisioning loss and normal user termination cases (e.g. turn off wifi or
                    // switch to another wifi ssid), hence, checking the current interface link
                    // state (down or up) helps distinguish the two cases: if the link state is
                    // down, provisioning is only lost because the link is being torn down (for
                    // example when turning off wifi), so treat it as a normal termination.
                    if (!handleLinkPropertiesUpdate(SEND_CALLBACKS)) {
                        final boolean linkStateUp = (msg.arg1 == ARG_LINKPROP_CHANGED_LINKSTATE_UP);
                        transitionToStoppingState(linkStateUp ? DisconnectCode.DC_PROVISIONING_FAIL
                                : DisconnectCode.DC_NORMAL_TERMINATION);
                    }
                    break;

                case CMD_UPDATE_TCP_BUFFER_SIZES:
                    mTcpBufferSizes = (String) msg.obj;
                    // This cannot possibly change provisioning state.
                    handleLinkPropertiesUpdate(SEND_CALLBACKS);
                    break;

                case CMD_UPDATE_HTTP_PROXY:
                    mHttpProxy = (ProxyInfo) msg.obj;
                    // This cannot possibly change provisioning state.
                    handleLinkPropertiesUpdate(SEND_CALLBACKS);
                    break;

                case CMD_SET_MULTICAST_FILTER: {
                    mMulticastFiltering = (boolean) msg.obj;
                    if (mApfFilter != null) {
                        mApfFilter.setMulticastFilter(mMulticastFiltering);
                    } else {
                        mCallback.setFallbackMulticastFilter(mMulticastFiltering);
                    }
                    updateMaxDtimMultiplier();
                    break;
                }

                case EVENT_READ_PACKET_FILTER_COMPLETE: {
                    if (mApfFilter != null) {
                        String snapShotStr = mApfFilter.setDataSnapshot((byte[]) msg.obj);
                        mLog.log("readPacketFilterComplete, ApfCounters: " + snapShotStr);
                    }
                    mApfDataSnapshotComplete.open();
                    break;
                }

                case CMD_ADD_KEEPALIVE_PACKET_FILTER_TO_APF: {
                    final int slot = msg.arg1;

                    if (mApfFilter != null) {
                        if (msg.obj instanceof NattKeepalivePacketDataParcelable) {
                            mApfFilter.addNattKeepalivePacketFilter(slot,
                                    (NattKeepalivePacketDataParcelable) msg.obj);
                        } else if (msg.obj instanceof TcpKeepalivePacketDataParcelable) {
                            mApfFilter.addTcpKeepalivePacketFilter(slot,
                                    (TcpKeepalivePacketDataParcelable) msg.obj);
                        }
                    }
                    break;
                }

                case CMD_REMOVE_KEEPALIVE_PACKET_FILTER_FROM_APF: {
                    final int slot = msg.arg1;
                    if (mApfFilter != null) {
                        mApfFilter.removeKeepalivePacketFilter(slot);
                    }
                    break;
                }

                case EVENT_DHCPACTION_TIMEOUT:
                    stopDhcpAction();
                    break;

                case EVENT_IPV6_AUTOCONF_TIMEOUT:
                    // Only enable DHCPv6 PD on networks that support IPv6 but not autoconf. The
                    // right way to do it is to use the P flag, once it's defined. For now, assume
                    // that the network doesn't support autoconf if it provides an IPv6 default
                    // route but no addresses via an RA.
                    // TODO: leverage the P flag in RA to determine if starting DHCPv6 PD or not,
                    // which is more clear and straightforward.
                    if (!hasIpv6Address(mLinkProperties)
                            && mLinkProperties.hasIpv6DefaultRoute()) {
                        Log.d(TAG, "Network supports IPv6 but not autoconf, starting DHCPv6 PD");
                        mNetworkQuirkMetrics.setEvent(QE_DHCP6_HEURISTIC_TRIGGERED);
                        mNetworkQuirkMetrics.statsWrite();
                        startDhcp6PrefixDelegation();
                    }
                    break;

                case DhcpClient.CMD_PRE_DHCP_ACTION:
                    if (mConfiguration.mRequestedPreDhcpActionMs > 0) {
                        ensureDhcpAction();
                    } else {
                        sendMessage(EVENT_PRE_DHCP_ACTION_COMPLETE);
                    }
                    break;

                case DhcpClient.CMD_CLEAR_LINKADDRESS:
                    mInterfaceCtrl.clearIPv4Address();
                    break;

                case DhcpClient.CMD_CONFIGURE_LINKADDRESS: {
                    final LinkAddress ipAddress = (LinkAddress) msg.obj;
                    final boolean success;
                    if (mPopulateLinkAddressLifetime) {
                        // For IPv4 link addresses, there is no concept of preferred/valid
                        // lifetimes. Populate the ifa_cacheinfo attribute in the netlink
                        // message with the DHCP lease duration, which is used by the kernel
                        // to maintain the validity of the IP addresses.
                        final int leaseDuration = msg.arg1;
                        success = NetlinkUtils.sendRtmNewAddressRequest(mInterfaceParams.index,
                                ipAddress.getAddress(),
                                (short) ipAddress.getPrefixLength(),
                                0 /* flags */,
                                (byte) RT_SCOPE_UNIVERSE /* scope */,
                                leaseDuration /* preferred */,
                                leaseDuration /* valid */);
                    } else {
                        success = mInterfaceCtrl.setIPv4Address(ipAddress);
                    }
                    if (success) {
                        // Although it's impossible to happen that DHCP client becomes null in
                        // RunningState and then NPE is thrown when it attempts to send a message
                        // on an null object, sometimes it's found during stress tests. If this
                        // issue does happen, log the terrible failure, that would be helpful to
                        // see how often this case occurs on fields and the log trace would be
                        // also useful for debugging(see b/203174383).
                        if (mDhcpClient == null) {
                            Log.wtf(mTag, "DhcpClient should never be null in RunningState.");
                        }
                        mDhcpClient.sendMessage(DhcpClient.EVENT_LINKADDRESS_CONFIGURED);
                    } else {
                        logError("Failed to set IPv4 address.");
                        dispatchCallback(PROV_CHANGE_LOST_PROVISIONING, mLinkProperties);
                        transitionToStoppingState(DisconnectCode.DC_PROVISIONING_FAIL);
                    }
                    break;
                }

                // This message is only received when:
                //
                //     a) initial address acquisition succeeds,
                //     b) renew succeeds or is NAK'd,
                //     c) rebind succeeds or is NAK'd, or
                //     d) the lease expires, or
                //     e) the IPv6-only preferred option is enabled and entering Ipv6OnlyWaitState.
                //
                // but never when initial address acquisition fails. The latter
                // condition is now governed by the provisioning timeout.
                case DhcpClient.CMD_POST_DHCP_ACTION:
                    stopDhcpAction();

                    switch (msg.arg1) {
                        case DhcpClient.DHCP_SUCCESS:
                            handleIPv4Success((DhcpResults) msg.obj);
                            break;
                        case DhcpClient.DHCP_FAILURE:
                            handleIPv4Failure();
                            break;
                        case DhcpClient.DHCP_IPV6_ONLY:
                            break;
                        case DhcpClient.DHCP_REFRESH_FAILURE:
                            // This case should only happen on the receipt of DHCPNAK when
                            // refreshing IP address post L2 roaming on some specific networks.
                            // WiFi should try to restart a new provisioning immediately without
                            // disconnecting L2 when it receives DHCP roaming failure event. IPv4
                            // link address still will be cleared when DhcpClient transits to
                            // StoppedState from RefreshingAddress State, although it will result
                            // in a following onProvisioningFailure then, WiFi should ignore this
                            // failure and start a new DHCP reconfiguration from INIT state.
                            final ReachabilityLossInfoParcelable lossInfo =
                                    new ReachabilityLossInfoParcelable("DHCP refresh failure",
                                            ReachabilityLossReason.ROAM);
                            mCallback.onReachabilityFailure(lossInfo);
                            break;
                        default:
                            logError("Unknown CMD_POST_DHCP_ACTION status: %s", msg.arg1);
                    }
                    break;

                case Dhcp6Client.CMD_DHCP6_RESULT:
                    switch(msg.arg1) {
                        case Dhcp6Client.DHCP6_PD_SUCCESS:
                            final List<IaPrefixOption> toBeUpdated = (List<IaPrefixOption>) msg.obj;
                            updateDelegatedAddresses(toBeUpdated);
                            handleLinkPropertiesUpdate(SEND_CALLBACKS);
                            break;

                        case Dhcp6Client.DHCP6_PD_PREFIX_EXPIRED:
                            final List<IaPrefixOption> toBeRemoved = (List<IaPrefixOption>) msg.obj;
                            removeExpiredDelegatedAddresses(toBeRemoved);
                            handleLinkPropertiesUpdate(SEND_CALLBACKS);
                            break;

                        default:
                            logError("Unknown CMD_DHCP6_RESULT status: %s", msg.arg1);
                    }
                    break;

                case DhcpClient.CMD_ON_QUIT:
                    // DHCPv4 quit early for some reason.
                    logError("Unexpected CMD_ON_QUIT from DHCPv4.");
                    mDhcpClient = null;
                    break;

                case Dhcp6Client.CMD_ON_QUIT:
                    // DHCPv6 quit early for some reason.
                    logError("Unexpected CMD_ON_QUIT from DHCPv6.");
                    mDhcp6Client = null;
                    break;

                case CMD_SET_DTIM_MULTIPLIER_AFTER_DELAY:
                    updateMaxDtimMultiplier();
                    break;

                case CMD_UPDATE_APF_CAPABILITIES:
                    final ApfCapabilities apfCapabilities = (ApfCapabilities) msg.obj;
                    if (handleUpdateApfCapabilities(apfCapabilities)) {
                        mApfFilter = maybeCreateApfFilter(apfCapabilities);
                        // If Apf supports ND offload, then turn off the vendor ND offload feature.
                        if (mApfFilter != null && mApfFilter.supportNdOffload()) {
                            mCallback.setNeighborDiscoveryOffload(false);
                        }
                    }
                    break;

                case CMD_UPDATE_APF_DATA_SNAPSHOT:
                    mCallback.startReadPacketFilter("polling");
                    sendMessageDelayed(CMD_UPDATE_APF_DATA_SNAPSHOT, mApfCounterPollingIntervalMs);
                    break;

                default:
                    return NOT_HANDLED;
            }

            mMsgStateLogger.handled(this, getCurrentState());
            return HANDLED;
        }
    }

    /**
     * Set the maximum DTIM multiplier to hardware driver per network condition. Any multiplier
     * larger than the maximum value must not be accepted, it will cause packet loss higher than
     * what the system can accept, which will cause unexpected behavior for apps, and may interrupt
     * the network connection.
     *
     * When Wifi STA is in the power saving mode and the system is suspended, the wakeup interval
     * will be set to:
     *    1) multiplier * AP's DTIM period if multiplier > 0.
     *    2) the driver default value if multiplier <= 0.
     * Some implementations may apply an additional cap to wakeup interval in the case of 1).
     */
    private void updateMaxDtimMultiplier() {
        int multiplier = deriveDtimMultiplier();
        if (mMaxDtimMultiplier == multiplier) return;

        mMaxDtimMultiplier = multiplier;
        log("set max DTIM multiplier to " + multiplier);
        mCallback.setMaxDtimMultiplier(multiplier);
    }

    /**
     * Check if current LinkProperties has either global IPv6 address or ULA (i.e. non IPv6
     * link-local addres).
     *
     * This function can be used to derive the DTIM multiplier per current network situation or
     * decide if we should start DHCPv6 Prefix Delegation when no IPv6 addresses are available
     * after autoconf timeout(5s).
     */
    private static boolean hasIpv6Address(@NonNull final LinkProperties lp) {
        return CollectionUtils.any(lp.getLinkAddresses(),
                la -> {
                    final InetAddress address = la.getAddress();
                    return (address instanceof Inet6Address) && !address.isLinkLocalAddress();
                });
    }

    private int deriveDtimMultiplier() {
        final boolean hasIpv4Addr = mLinkProperties.hasIpv4Address();
        // For a host in the network that has only ULA and link-local but no GUA, consider
        // that it also has IPv6 connectivity. LinkProperties#isIpv6Provisioned only returns
        // true when it has a GUA, so we cannot use it for IPv6-only network case.
        final boolean hasIpv6Addr = hasIpv6Address(mLinkProperties);

        final int multiplier;
        if (!mMulticastFiltering) {
            multiplier = mDependencies.getDeviceConfigPropertyInt(
                    CONFIG_MULTICAST_LOCK_MAX_DTIM_MULTIPLIER,
                    DEFAULT_MULTICAST_LOCK_MAX_DTIM_MULTIPLIER);
        } else if (!hasIpv6Addr
                && (SystemClock.elapsedRealtime() < mIPv6ProvisioningDtimGracePeriodMillis)) {
            // IPv6 provisioning may or may not complete soon in the future, we don't know when
            // it will complete, however, setting multiplier to a high value will cause higher
            // RA packet loss, that increases the overall IPv6 provisioning latency. So just set
            // multiplier to 1 before device gains the IPv6 provisioning, make sure device won't
            // miss any RA packet later.
            multiplier = mDependencies.getDeviceConfigPropertyInt(
                    CONFIG_BEFORE_IPV6_PROV_MAX_DTIM_MULTIPLIER,
                    DEFAULT_BEFORE_IPV6_PROV_MAX_DTIM_MULTIPLIER);
        } else if (hasIpv6Addr && !hasIpv4Addr) {
            multiplier = mDependencies.getDeviceConfigPropertyInt(
                    CONFIG_IPV6_ONLY_NETWORK_MAX_DTIM_MULTIPLIER,
                    DEFAULT_IPV6_ONLY_NETWORK_MAX_DTIM_MULTIPLIER);
        } else if (hasIpv4Addr && !hasIpv6Addr) {
            multiplier = mDependencies.getDeviceConfigPropertyInt(
                    CONFIG_IPV4_ONLY_NETWORK_MAX_DTIM_MULTIPLIER,
                    DEFAULT_IPV4_ONLY_NETWORK_MAX_DTIM_MULTIPLIER);
        } else if (hasIpv6Addr && hasIpv4Addr) {
            multiplier = mDependencies.getDeviceConfigPropertyInt(
                    CONFIG_DUAL_STACK_MAX_DTIM_MULTIPLIER,
                    DEFAULT_DUAL_STACK_MAX_DTIM_MULTIPLIER);
        } else {
            multiplier = DTIM_MULTIPLIER_RESET;
        }
        return multiplier;
    }

    private static class MessageHandlingLogger {
        public String processedInState;
        public String receivedInState;

        public void reset() {
            processedInState = null;
            receivedInState = null;
        }

        public void handled(State processedIn, IState receivedIn) {
            processedInState = processedIn.getClass().getSimpleName();
            receivedInState = receivedIn.getName();
        }

        public String toString() {
            return String.format("rcvd_in=%s, proc_in=%s",
                                 receivedInState, processedInState);
        }
    }

    // TODO: extract out into CollectionUtils.
    static <T> boolean any(Iterable<T> coll, Predicate<T> fn) {
        for (T t : coll) {
            if (fn.test(t)) {
                return true;
            }
        }
        return false;
    }

    static <T> boolean all(Iterable<T> coll, Predicate<T> fn) {
        return !any(coll, not(fn));
    }

    static <T> Predicate<T> not(Predicate<T> fn) {
        return (t) -> !fn.test(t);
    }

    static <T> String join(String delimiter, Collection<T> coll) {
        return coll.stream().map(Object::toString).collect(Collectors.joining(delimiter));
    }

    static <T> T find(Iterable<T> coll, Predicate<T> fn) {
        for (T t: coll) {
            if (fn.test(t)) {
                return t;
            }
        }
        return null;
    }

    static <T> List<T> findAll(Collection<T> coll, Predicate<T> fn) {
        return coll.stream().filter(fn).collect(Collectors.toList());
    }
}
