Merge "Injecting network ip provision stats into statsd" into rvc-dev
diff --git a/src/android/net/dhcp/DhcpClient.java b/src/android/net/dhcp/DhcpClient.java
index 4404273..4fedf30 100644
--- a/src/android/net/dhcp/DhcpClient.java
+++ b/src/android/net/dhcp/DhcpClient.java
@@ -80,6 +80,7 @@
 import android.os.PowerManager;
 import android.os.SystemClock;
 import android.provider.Settings;
+import android.stats.connectivity.DhcpFeature;
 import android.system.ErrnoException;
 import android.system.Os;
 import android.util.EventLog;
@@ -101,6 +102,7 @@
 import com.android.networkstack.apishim.SocketUtilsShimImpl;
 import com.android.networkstack.apishim.common.ShimUtils;
 import com.android.networkstack.arp.ArpPacket;
+import com.android.networkstack.metrics.IpProvisioningMetrics;
 
 import java.io.FileDescriptor;
 import java.io.IOException;
@@ -305,6 +307,8 @@
     private final Context mContext;
     private final Random mRandom;
     private final IpConnectivityLog mMetricsLog = new IpConnectivityLog();
+    @NonNull
+    private final IpProvisioningMetrics mMetrics;
 
     // We use a UDP socket to send, so the kernel handles ARP and routing for us (DHCP servers can
     // be off-link as well as on-link).
@@ -378,9 +382,11 @@
      */
     public static class Dependencies {
         private final NetworkStackIpMemoryStore mNetworkStackIpMemoryStore;
+        private final IpProvisioningMetrics mMetrics;
 
-        public Dependencies(NetworkStackIpMemoryStore store) {
+        public Dependencies(NetworkStackIpMemoryStore store, IpProvisioningMetrics metrics) {
             mNetworkStackIpMemoryStore = store;
+            mMetrics = metrics;
         }
 
         /**
@@ -407,6 +413,13 @@
         }
 
         /**
+         * Get a IpProvisioningMetrics instance.
+         */
+        public IpProvisioningMetrics getIpProvisioningMetrics() {
+            return mMetrics;
+        }
+
+        /**
          * Return whether a feature guarded by a feature flag is enabled.
          * @see NetworkStackUtils#isFeatureEnabled(Context, String, String)
          */
@@ -444,6 +457,7 @@
         mController = controller;
         mIfaceName = iface;
         mIpMemoryStore = deps.getIpMemoryStore();
+        mMetrics = deps.getIpProvisioningMetrics();
 
         // CHECKSTYLE:OFF IndentationCheck
         addState(mStoppedState);
@@ -484,6 +498,7 @@
         final boolean sendHostname = deps.getSendHostnameOption(context);
         mHostname = sendHostname ? new HostnameTransliterator().transliterate(
                 deps.getDeviceName(mContext)) : null;
+        mMetrics.setHostnameTransinfo(sendHostname, mHostname != null);
     }
 
     public void registerForPreDhcpNotification() {
@@ -529,6 +544,15 @@
                 false /* defaultEnabled */);
     }
 
+    private void recordMetricEnabledFeatures() {
+        if (isDhcpLeaseCacheEnabled()) mMetrics.setDhcpEnabledFeature(DhcpFeature.DF_INITREBOOT);
+        if (isDhcpRapidCommitEnabled()) mMetrics.setDhcpEnabledFeature(DhcpFeature.DF_RAPIDCOMMIT);
+        if (isDhcpIpConflictDetectEnabled()) mMetrics.setDhcpEnabledFeature(DhcpFeature.DF_DAD);
+        if (mConfiguration.isPreconnectionEnabled) {
+            mMetrics.setDhcpEnabledFeature(DhcpFeature.DF_FILS);
+        }
+    }
+
     private void confirmDhcpLease(DhcpPacket packet, DhcpResults results) {
         setDhcpLeaseExpiry(packet);
         acceptDhcpResults(results, "Confirmed");
@@ -610,6 +634,7 @@
                     EventLog.writeEvent(snetTagId, bugId, uid, data);
                 }
                 mMetricsLog.log(mIfaceName, new DhcpErrorEvent(e.errorCode));
+                mMetrics.addDhcpErrorCode(e.errorCode);
             }
         }
 
@@ -687,6 +712,7 @@
         final ByteBuffer packet = DhcpPacket.buildDiscoverPacket(
                 DhcpPacket.ENCAP_L2, mTransactionId, getSecs(), mHwAddr,
                 DO_UNICAST, getRequestedParams(), isDhcpRapidCommitEnabled(), mHostname);
+        mMetrics.incrementCountForDiscover();
         return transmitPacket(packet, "DHCPDISCOVER", DhcpPacket.ENCAP_L2, INADDR_BROADCAST);
     }
 
@@ -705,6 +731,7 @@
         String description = "DHCPREQUEST ciaddr=" + clientAddress.getHostAddress() +
                              " request=" + requestedAddress.getHostAddress() +
                              " serverid=" + serverStr;
+        mMetrics.incrementCountForRequest();
         return transmitPacket(packet, description, encap, to);
     }
 
@@ -937,6 +964,7 @@
                     } else {
                         startInitRebootOrInit();
                     }
+                    recordMetricEnabledFeatures();
                     return HANDLED;
                 default:
                     return NOT_HANDLED;
@@ -1422,6 +1450,7 @@
             try {
                 final ArpPacket packet = ArpPacket.parseArpPacket(recvbuf, length);
                 if (hasIpAddressConflict(packet, mTargetIp)) {
+                    mMetrics.incrementCountForIpConflict();
                     sendMessage(EVENT_IP_CONFLICT);
                 }
             } catch (ArpPacket.ParseException e) {
diff --git a/src/android/net/ip/IpClient.java b/src/android/net/ip/IpClient.java
index 5d5b025..eeff157 100644
--- a/src/android/net/ip/IpClient.java
+++ b/src/android/net/ip/IpClient.java
@@ -60,6 +60,7 @@
 import android.os.RemoteException;
 import android.os.ServiceSpecificException;
 import android.os.SystemClock;
+import android.stats.connectivity.DisconnectCode;
 import android.text.TextUtils;
 import android.util.LocalLog;
 import android.util.Log;
@@ -79,6 +80,7 @@
 import com.android.networkstack.apishim.NetworkInformationShimImpl;
 import com.android.networkstack.apishim.common.NetworkInformationShim;
 import com.android.networkstack.apishim.common.ShimUtils;
+import com.android.networkstack.metrics.IpProvisioningMetrics;
 import com.android.server.NetworkObserverRegistry;
 import com.android.server.NetworkStackService.NetworkStackServiceManager;
 
@@ -129,6 +131,7 @@
     private static final ConcurrentHashMap<String, LocalLog> sPktLogs = new ConcurrentHashMap<>();
     private final NetworkStackIpMemoryStore mIpMemoryStore;
     private final NetworkInformationShim mShim = NetworkInformationShimImpl.newInstance();
+    private final IpProvisioningMetrics mIpProvisioningMetrics = new IpProvisioningMetrics();
 
     /**
      * Dump all state machine and connectivity packet logs to the specified writer.
@@ -527,8 +530,8 @@
          * Get a DhcpClient Dependencies instance.
          */
         public DhcpClient.Dependencies getDhcpClientDependencies(
-                NetworkStackIpMemoryStore ipMemoryStore) {
-            return new DhcpClient.Dependencies(ipMemoryStore);
+                NetworkStackIpMemoryStore ipMemoryStore, IpProvisioningMetrics metrics) {
+            return new DhcpClient.Dependencies(ipMemoryStore, metrics);
         }
 
         /**
@@ -818,7 +821,10 @@
      * <p>This does not shut down the StateMachine itself, which is handled by {@link #shutdown()}.
      */
     public void stop() {
-        sendMessage(CMD_STOP);
+        // 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.
+        sendMessage(CMD_STOP, DisconnectCode.DC_NORMAL_TERMINATION.getNumber());
     }
 
     /**
@@ -1072,6 +1078,14 @@
         mMetricsLog.log(mInterfaceName, new IpManagerEvent(type, duration));
     }
 
+    // Record the DisconnectCode and transition to StoppingState.
+    // When jumping to mStoppingState This function will ensure
+    // that you will not forget to fill in DisconnectCode.
+    private void transitionToStoppingState(final DisconnectCode code) {
+        mIpProvisioningMetrics.setDisconnectCode(code);
+        transitionTo(mStoppingState);
+    }
+
     // For now: use WifiStateMachine's historical notion of provisioned.
     @VisibleForTesting
     static boolean isProvisioned(LinkProperties lp, InitialConfiguration config) {
@@ -1352,6 +1366,12 @@
         if (Objects.equals(newLp, mLinkProperties)) {
             return true;
         }
+
+        // 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
@@ -1447,10 +1467,10 @@
         }
         mCallback.onNewDhcpResults(null);
 
-        handleProvisioningFailure();
+        handleProvisioningFailure(DisconnectCode.DC_PROVISIONING_FAIL);
     }
 
-    private void handleProvisioningFailure() {
+    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
@@ -1467,7 +1487,7 @@
 
         dispatchCallback(delta, newLp);
         if (delta == PROV_CHANGE_LOST_PROVISIONING) {
-            transitionTo(mStoppingState);
+            transitionToStoppingState(code);
         }
     }
 
@@ -1723,7 +1743,7 @@
     private void startDhcpClient() {
         // Start DHCPv4.
         mDhcpClient = mDependencies.makeDhcpClient(mContext, IpClient.this, mInterfaceParams,
-                mDependencies.getDhcpClientDependencies(mIpMemoryStore));
+                mDependencies.getDhcpClientDependencies(mIpMemoryStore, mIpProvisioningMetrics));
 
         // 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
@@ -1744,7 +1764,8 @@
             if (mInterfaceParams == null) {
                 logError("Failed to find InterfaceParams for " + mInterfaceName);
                 doImmediateProvisioningFailure(IpManagerEvent.ERROR_INTERFACE_NOT_FOUND);
-                deferMessage(obtainMessage(CMD_STOP));
+                deferMessage(obtainMessage(CMD_STOP,
+                        DisconnectCode.DC_INTERFACE_NOT_FOUND.getNumber()));
                 return;
             }
 
@@ -1836,6 +1857,7 @@
     class StartedState extends State {
         @Override
         public void enter() {
+            mIpProvisioningMetrics.reset();
             mStartTimeMillis = SystemClock.elapsedRealtime();
             if (mConfiguration.mProvisioningTimeoutMs > 0) {
                 final long alarmTime = SystemClock.elapsedRealtime()
@@ -1847,13 +1869,17 @@
         @Override
         public void exit() {
             mProvisioningTimeoutAlarm.cancel();
+
+            // 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:
-                    transitionTo(mStoppingState);
+                    transitionToStoppingState(DisconnectCode.forNumber(msg.arg1));
                     break;
 
                 case CMD_UPDATE_L2KEY_CLUSTER: {
@@ -1875,7 +1901,7 @@
                     break;
 
                 case EVENT_PROVISIONING_TIMEOUT:
-                    handleProvisioningFailure();
+                    handleProvisioningFailure(DisconnectCode.DC_PROVISIONING_TIMEOUT);
                     break;
 
                 default:
@@ -1912,13 +1938,13 @@
 
             if (mConfiguration.mEnableIPv6 && !startIPv6()) {
                 doImmediateProvisioningFailure(IpManagerEvent.ERROR_STARTING_IPV6);
-                enqueueJumpToStoppingState();
+                enqueueJumpToStoppingState(DisconnectCode.DC_ERROR_STARTING_IPV6);
                 return;
             }
 
             if (mConfiguration.mEnableIPv4 && !isUsingPreconnection() && !startIPv4()) {
                 doImmediateProvisioningFailure(IpManagerEvent.ERROR_STARTING_IPV4);
-                enqueueJumpToStoppingState();
+                enqueueJumpToStoppingState(DisconnectCode.DC_ERROR_STARTING_IPV4);
                 return;
             }
 
@@ -1926,14 +1952,14 @@
             if ((config != null) && !applyInitialConfig(config)) {
                 // TODO introduce a new IpManagerEvent constant to distinguish this error case.
                 doImmediateProvisioningFailure(IpManagerEvent.ERROR_INVALID_PROVISIONING);
-                enqueueJumpToStoppingState();
+                enqueueJumpToStoppingState(DisconnectCode.DC_INVALID_PROVISIONING);
                 return;
             }
 
             if (mConfiguration.mUsingIpReachabilityMonitor && !startIpReachabilityMonitor()) {
                 doImmediateProvisioningFailure(
                         IpManagerEvent.ERROR_STARTING_IPREACHABILITYMONITOR);
-                enqueueJumpToStoppingState();
+                enqueueJumpToStoppingState(DisconnectCode.DC_ERROR_STARTING_IPREACHABILITYMONITOR);
                 return;
             }
         }
@@ -1965,8 +1991,8 @@
             resetLinkProperties();
         }
 
-        private void enqueueJumpToStoppingState() {
-            deferMessage(obtainMessage(CMD_JUMP_RUNNING_TO_STOPPING));
+        private void enqueueJumpToStoppingState(final DisconnectCode code) {
+            deferMessage(obtainMessage(CMD_JUMP_RUNNING_TO_STOPPING, code.getNumber()));
         }
 
         private ConnectivityPacketTracker createPacketTracker() {
@@ -2001,7 +2027,7 @@
             switch (msg.what) {
                 case CMD_JUMP_RUNNING_TO_STOPPING:
                 case CMD_STOP:
-                    transitionTo(mStoppingState);
+                    transitionToStoppingState(DisconnectCode.forNumber(msg.arg1));
                     break;
 
                 case CMD_START:
@@ -2028,8 +2054,14 @@
                     break;
 
                 case EVENT_NETLINK_LINKPROPERTIES_CHANGED:
+                    // EVENT_NETLINK_LINKPROPERTIES_CHANGED message will be received in both of
+                    // provisioning loss and normal user termination case (e.g. turn off wifi or
+                    // switch to another wifi ssid), hence, checking current interface change
+                    // status (down or up) would help distinguish.
+                    final boolean ifUp = (msg.arg1 != 0);
                     if (!handleLinkPropertiesUpdate(SEND_CALLBACKS)) {
-                        transitionTo(mStoppingState);
+                        transitionToStoppingState(ifUp ? DisconnectCode.DC_PROVISIONING_FAIL
+                                : DisconnectCode.DC_NORMAL_TERMINATION);
                     }
                     break;
 
@@ -2109,7 +2141,7 @@
                     } else {
                         logError("Failed to set IPv4 address.");
                         dispatchCallback(PROV_CHANGE_LOST_PROVISIONING, mLinkProperties);
-                        transitionTo(mStoppingState);
+                        transitionToStoppingState(DisconnectCode.DC_PROVISIONING_FAIL);
                     }
                     break;
                 }
diff --git a/src/android/net/util/NetworkStackUtils.java b/src/android/net/util/NetworkStackUtils.java
index 99563ee..19ca4b5 100755
--- a/src/android/net/util/NetworkStackUtils.java
+++ b/src/android/net/util/NetworkStackUtils.java
@@ -436,4 +436,22 @@
         return addr instanceof Inet6Address
                 && ((addr.getAddress()[0] & 0xfe) == 0xfc);
     }
+
+    /**
+     * Returns the {@code int} nearest in value to {@code value}.
+     *
+     * @param value any {@code long} value
+     * @return the same value cast to {@code int} if it is in the range of the {@code int}
+     * type, {@link Integer#MAX_VALUE} if it is too large, or {@link Integer#MIN_VALUE} if
+     * it is too small
+     */
+    public static int saturatedCast(long value) {
+        if (value > Integer.MAX_VALUE) {
+            return Integer.MAX_VALUE;
+        }
+        if (value < Integer.MIN_VALUE) {
+            return Integer.MIN_VALUE;
+        }
+        return (int) value;
+    }
 }
diff --git a/src/android/net/util/Stopwatch.java b/src/android/net/util/Stopwatch.java
index 07618e9..33653dd 100644
--- a/src/android/net/util/Stopwatch.java
+++ b/src/android/net/util/Stopwatch.java
@@ -49,6 +49,14 @@
     }
 
     /**
+     * Retart the Stopwatch.
+     */
+    public Stopwatch restart() {
+        mStartTimeNs = SystemClock.elapsedRealtimeNanos();
+        return this;
+    }
+
+    /**
      * Stop the Stopwatch.
      * @return the total time recorded, in microseconds, or 0 if not started.
      */
diff --git a/src/com/android/networkstack/metrics/IpProvisioningMetrics.java b/src/com/android/networkstack/metrics/IpProvisioningMetrics.java
new file mode 100644
index 0000000..1f969d4
--- /dev/null
+++ b/src/com/android/networkstack/metrics/IpProvisioningMetrics.java
@@ -0,0 +1,163 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.metrics;
+
+import android.net.util.NetworkStackUtils;
+import android.net.util.Stopwatch;
+import android.stats.connectivity.DhcpErrorCode;
+import android.stats.connectivity.DhcpFeature;
+import android.stats.connectivity.DisconnectCode;
+import android.stats.connectivity.HostnameTransResult;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * Class to record the network IpProvisioning into statsd.
+ * 1. Fill in NetworkIpProvisioningReported proto.
+ * 2. Write the NetworkIpProvisioningReported proto into statsd.
+ * 3. This class is not thread-safe, and should always be accessed from the same thread.
+ * @hide
+ */
+
+public class IpProvisioningMetrics {
+    private static final String TAG = IpProvisioningMetrics.class.getSimpleName();
+    private final NetworkIpProvisioningReported.Builder mStatsBuilder =
+            NetworkIpProvisioningReported.newBuilder();
+    private final DhcpSession.Builder mDhcpSessionBuilder = DhcpSession.newBuilder();
+    private final Stopwatch mIpv4Watch = new Stopwatch().start();
+    private final Stopwatch mIpv6Watch = new Stopwatch().start();
+    private final Stopwatch mWatch = new Stopwatch().start();
+    private final Set<DhcpFeature> mDhcpFeatures = new HashSet<DhcpFeature>();
+
+    // Define a maximum number of the DhcpErrorCode.
+    public static final int MAX_DHCP_ERROR_COUNT = 20;
+
+    /**
+     *  reset this all metrics members
+     */
+    public void reset() {
+        mStatsBuilder.clear();
+        mDhcpSessionBuilder.clear();
+        mDhcpFeatures.clear();
+        mIpv4Watch.restart();
+        mIpv6Watch.restart();
+        mWatch.restart();
+    }
+
+    /**
+     * Write the TransportType into mStatsBuilder.
+     * TODO: implement this
+     */
+    public void setTransportType() {}
+
+    /**
+     * Write the IPv4Provisioned latency into mStatsBuilder.
+     */
+    public void setIPv4ProvisionedLatencyOnFirstTime(final boolean isIpv4Provisioned) {
+        if (isIpv4Provisioned && !mStatsBuilder.hasIpv4LatencyMicros()) {
+            mStatsBuilder.setIpv4LatencyMicros(NetworkStackUtils.saturatedCast(mIpv4Watch.stop()));
+        }
+    }
+
+    /**
+     * Write the IPv6Provisioned latency into mStatsBuilder.
+     */
+    public void setIPv6ProvisionedLatencyOnFirstTime(final boolean isIpv6Provisioned) {
+        if (isIpv6Provisioned && !mStatsBuilder.hasIpv6LatencyMicros()) {
+            mStatsBuilder.setIpv6LatencyMicros(NetworkStackUtils.saturatedCast(mIpv6Watch.stop()));
+        }
+    }
+
+    /**
+     * Write the DhcpFeature proto into mStatsBuilder.
+     */
+    public void setDhcpEnabledFeature(final DhcpFeature feature) {
+        if (feature == DhcpFeature.DF_UNKNOWN) return;
+        mDhcpFeatures.add(feature);
+    }
+
+    /**
+     * Write the DHCPDISCOVER transmission count into DhcpSession.
+     */
+    public void incrementCountForDiscover() {
+        mDhcpSessionBuilder.setDiscoverCount(mDhcpSessionBuilder.getDiscoverCount() + 1);
+    }
+
+    /**
+     * Write the DHCPREQUEST transmission count into DhcpSession.
+     */
+    public void incrementCountForRequest() {
+        mDhcpSessionBuilder.setRequestCount(mDhcpSessionBuilder.getRequestCount() + 1);
+    }
+
+    /**
+     * Write the IPv4 address conflict count into DhcpSession.
+     */
+    public void incrementCountForIpConflict() {
+        mDhcpSessionBuilder.setConflictCount(mDhcpSessionBuilder.getConflictCount() + 1);
+    }
+
+    /**
+     * Write the hostname transliteration result into DhcpSession.
+     */
+    public void setHostnameTransinfo(final boolean isOptionEnabled, final boolean transSuccess) {
+        mDhcpSessionBuilder.setHtResult(!isOptionEnabled ? HostnameTransResult.HTR_DISABLE :
+                transSuccess ? HostnameTransResult.HTR_SUCCESS : HostnameTransResult.HTR_FAILURE);
+    }
+
+    /**
+     * write the DHCP error code into DhcpSession.
+     */
+    public void addDhcpErrorCode(final int errorCode) {
+        if (mDhcpSessionBuilder.getErrorCodeCount() >= MAX_DHCP_ERROR_COUNT) return;
+        mDhcpSessionBuilder.addErrorCode(DhcpErrorCode.forNumber(errorCode));
+    }
+
+    /**
+     * Write the IP provision disconnect code into DhcpSession.
+     */
+    public void setDisconnectCode(final DisconnectCode disconnectCode) {
+        if (mStatsBuilder.hasDisconnectCode()) return;
+        mStatsBuilder.setDisconnectCode(disconnectCode);
+    }
+
+    /**
+     * Write the NetworkIpProvisioningReported proto into statsd.
+     */
+    public NetworkIpProvisioningReported statsWrite() {
+        if (!mWatch.isStarted()) return null;
+        for (DhcpFeature feature : mDhcpFeatures) {
+            mDhcpSessionBuilder.addUsedFeatures(feature);
+        }
+        mStatsBuilder.setDhcpSession(mDhcpSessionBuilder);
+        mStatsBuilder.setProvisioningDurationMicros(mWatch.stop());
+        mStatsBuilder.setRandomNumber((int) (Math.random() * 1000));
+        final NetworkIpProvisioningReported Stats = mStatsBuilder.build();
+        final byte[] DhcpSession = Stats.getDhcpSession().toByteArray();
+        NetworkStackStatsLog.write(NetworkStackStatsLog.NETWORK_IP_PROVISIONING_REPORTED,
+                Stats.getTransportType().getNumber(),
+                Stats.getIpv4LatencyMicros(),
+                Stats.getIpv6LatencyMicros(),
+                Stats.getProvisioningDurationMicros(),
+                Stats.getDisconnectCode().getNumber(),
+                DhcpSession,
+                Stats.getRandomNumber());
+        mWatch.reset();
+        return Stats;
+    }
+}
diff --git a/tests/integration/src/android/net/ip/IpClientIntegrationTest.java b/tests/integration/src/android/net/ip/IpClientIntegrationTest.java
index 150fd00..38eb84e 100644
--- a/tests/integration/src/android/net/ip/IpClientIntegrationTest.java
+++ b/tests/integration/src/android/net/ip/IpClientIntegrationTest.java
@@ -136,6 +136,7 @@
 import com.android.networkstack.apishim.ConstantsShim;
 import com.android.networkstack.apishim.common.ShimUtils;
 import com.android.networkstack.arp.ArpPacket;
+import com.android.networkstack.metrics.IpProvisioningMetrics;
 import com.android.server.NetworkObserver;
 import com.android.server.NetworkObserverRegistry;
 import com.android.server.NetworkStackService.NetworkStackServiceManager;
@@ -318,8 +319,8 @@
 
         @Override
         public DhcpClient.Dependencies getDhcpClientDependencies(
-                NetworkStackIpMemoryStore ipMemoryStore) {
-            return new DhcpClient.Dependencies(ipMemoryStore) {
+                NetworkStackIpMemoryStore ipMemoryStore, IpProvisioningMetrics metrics) {
+            return new DhcpClient.Dependencies(ipMemoryStore, metrics) {
                 @Override
                 public boolean isFeatureEnabled(final Context context, final String name,
                         final boolean defaultEnabled) {
diff --git a/tests/unit/src/com/android/networkstack/metrics/NetworkIpProvisioningMetricsTest.java b/tests/unit/src/com/android/networkstack/metrics/NetworkIpProvisioningMetricsTest.java
new file mode 100644
index 0000000..39906e2
--- /dev/null
+++ b/tests/unit/src/com/android/networkstack/metrics/NetworkIpProvisioningMetricsTest.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.networkstack.metrics;
+
+import android.net.metrics.DhcpErrorEvent;
+import android.stats.connectivity.DhcpErrorCode;
+import android.stats.connectivity.DhcpFeature;
+import android.stats.connectivity.DisconnectCode;
+import android.stats.connectivity.HostnameTransResult;
+
+import static org.junit.Assert.assertEquals;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+
+/**
+ * Tests for IpProvisioningMetrics.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class NetworkIpProvisioningMetricsTest {
+    @Test
+    public void testIpProvisioningMetrics_setHostnameTransinfo() throws Exception {
+        NetworkIpProvisioningReported mStats;
+        final IpProvisioningMetrics mMetrics = new IpProvisioningMetrics();
+
+        mMetrics.reset();
+        mMetrics.setHostnameTransinfo(false /* isOptionEnabled */, false /* transSuccess */);
+        mStats = mMetrics.statsWrite();
+        assertEquals(HostnameTransResult.HTR_DISABLE, mStats.getDhcpSession().getHtResult());
+
+        mMetrics.reset();
+        mMetrics.setHostnameTransinfo(true /* isOptionEnabled */, false /* transSuccess */);
+        mStats = mMetrics.statsWrite();
+        assertEquals(HostnameTransResult.HTR_FAILURE, mStats.getDhcpSession().getHtResult());
+
+        mMetrics.reset();
+        mMetrics.setHostnameTransinfo(true /* isOptionEnabled */, true /* transSuccess */);
+        mStats = mMetrics.statsWrite();
+        assertEquals(HostnameTransResult.HTR_SUCCESS, mStats.getDhcpSession().getHtResult());
+    }
+
+    @Test
+    public void testIpProvisioningMetrics_addDhcpErrorCode() throws Exception {
+        final NetworkIpProvisioningReported mStats;
+        final IpProvisioningMetrics mMetrics = new IpProvisioningMetrics();
+        mMetrics.reset();
+        mMetrics.addDhcpErrorCode(DhcpErrorEvent.DHCP_ERROR);
+        mMetrics.addDhcpErrorCode(DhcpErrorEvent.L2_WRONG_ETH_TYPE);
+        mMetrics.addDhcpErrorCode(DhcpErrorEvent.L3_INVALID_IP);
+        mMetrics.addDhcpErrorCode(DhcpErrorEvent.L4_WRONG_PORT);
+        mMetrics.addDhcpErrorCode(DhcpErrorEvent.BOOTP_TOO_SHORT);
+        mMetrics.addDhcpErrorCode(DhcpErrorEvent.DHCP_NO_COOKIE);
+        for (int i = 0; i < mMetrics.MAX_DHCP_ERROR_COUNT; i++) {
+            mMetrics.addDhcpErrorCode(DhcpErrorEvent.PARSING_ERROR);
+        }
+        mStats = mMetrics.statsWrite();
+        assertEquals(DhcpErrorCode.ET_DHCP_ERROR, mStats.getDhcpSession().getErrorCode(0));
+        assertEquals(DhcpErrorCode.ET_L2_WRONG_ETH_TYPE, mStats.getDhcpSession().getErrorCode(1));
+        assertEquals(DhcpErrorCode.ET_L3_INVALID_IP, mStats.getDhcpSession().getErrorCode(2));
+        assertEquals(DhcpErrorCode.ET_L4_WRONG_PORT, mStats.getDhcpSession().getErrorCode(3));
+        assertEquals(DhcpErrorCode.ET_BOOTP_TOO_SHORT, mStats.getDhcpSession().getErrorCode(4));
+        assertEquals(DhcpErrorCode.ET_DHCP_NO_COOKIE, mStats.getDhcpSession().getErrorCode(5));
+        // Check can record the same error code
+        assertEquals(DhcpErrorCode.ET_PARSING_ERROR, mStats.getDhcpSession().getErrorCode(6));
+        assertEquals(DhcpErrorCode.ET_PARSING_ERROR, mStats.getDhcpSession().getErrorCode(6));
+        // The maximum number of DHCP error code counts is MAX_DHCP_ERROR_COUNT
+        assertEquals(mMetrics.MAX_DHCP_ERROR_COUNT, mStats.getDhcpSession().getErrorCodeCount());
+    }
+    @Test
+    public void testIpProvisioningMetrics_CollectMetrics() throws Exception {
+        final NetworkIpProvisioningReported mStats;
+        final IpProvisioningMetrics mMetrics = new IpProvisioningMetrics();
+        mMetrics.reset();
+        // Entering 3 DISCOVER_SEND_COUNTs
+        mMetrics.incrementCountForDiscover();
+        mMetrics.incrementCountForDiscover();
+        mMetrics.incrementCountForDiscover();
+
+        // Entering 2 SEND_REQUEST_COUNTs
+        mMetrics.incrementCountForRequest();
+        mMetrics.incrementCountForRequest();
+
+        // Entering 1 IP_CONFLICT_COUNT
+        mMetrics.incrementCountForIpConflict();
+
+        // Entering 4 DhcpFeatures and one is repeated, so it should only count to 3
+        mMetrics.setDhcpEnabledFeature(DhcpFeature.DF_INITREBOOT);
+        mMetrics.setDhcpEnabledFeature(DhcpFeature.DF_RAPIDCOMMIT);
+        mMetrics.setDhcpEnabledFeature(DhcpFeature.DF_DAD);
+        mMetrics.setDhcpEnabledFeature(DhcpFeature.DF_DAD);
+
+        // Entering 6 DhcpErrorCodes
+        mMetrics.addDhcpErrorCode(DhcpErrorEvent.L3_TOO_SHORT);
+        mMetrics.addDhcpErrorCode(DhcpErrorEvent.DHCP_INVALID_OPTION_LENGTH);
+        mMetrics.addDhcpErrorCode(DhcpErrorEvent.RECEIVE_ERROR);
+        mMetrics.addDhcpErrorCode(DhcpErrorEvent.RECEIVE_ERROR);
+        mMetrics.addDhcpErrorCode(DhcpErrorEvent.PARSING_ERROR);
+        mMetrics.addDhcpErrorCode(DhcpErrorEvent.PARSING_ERROR);
+
+        mMetrics.setHostnameTransinfo(true /* isOptionEnabled */, true /* transSuccess */);
+
+        // Only the first IP provisioning disconnect code is recorded.
+        mMetrics.setDisconnectCode(DisconnectCode.DC_PROVISIONING_TIMEOUT);
+        mMetrics.setDisconnectCode(DisconnectCode.DC_ERROR_STARTING_IPV4);
+
+        // Writing the metrics into statsd
+        mStats = mMetrics.statsWrite();
+
+        // Verifing the result of the metrics.
+        assertEquals(3, mStats.getDhcpSession().getDiscoverCount());
+        assertEquals(2, mStats.getDhcpSession().getRequestCount());
+        assertEquals(1, mStats.getDhcpSession().getConflictCount());
+        assertEquals(3, mStats.getDhcpSession().getUsedFeaturesCount());
+        assertEquals(6, mStats.getDhcpSession().getErrorCodeCount());
+        assertEquals(HostnameTransResult.HTR_SUCCESS, mStats.getDhcpSession().getHtResult());
+        assertEquals(DisconnectCode.DC_PROVISIONING_TIMEOUT, mStats.getDisconnectCode());
+    }
+}