Merge changes from topic "androidx-settings-aosp"

* changes:
  Migrated various apps under frameworks/base/packages/Shell  to androidx
  Migrated SystemUI & SettingsLib to androidx
diff --git a/Android.bp b/Android.bp
index ecbe249..f11341e 100644
--- a/Android.bp
+++ b/Android.bp
@@ -1171,6 +1171,48 @@
     create_stubs: false,
 }
 
+metalava_framework_docs_args = "--manifest $(location core/res/AndroidManifest.xml) " +
+    "--hide-package com.android.okhttp " +
+    "--hide-package com.android.org.conscrypt --hide-package com.android.server " +
+    "--hide RequiresPermission " +
+    "--hide MissingPermission --hide BroadcastBehavior " +
+    "--hide HiddenSuperclass --hide DeprecationMismatch --hide UnavailableSymbol " +
+    "--hide SdkConstant --hide HiddenTypeParameter --hide Todo --hide Typo"
+
+doc_defaults {
+    name: "metalava-api-stubs-default",
+    srcs: [
+        ":opt-telephony-srcs",
+        ":opt-net-voip-srcs",
+        ":openjdk_javadoc_files",
+        ":non_openjdk_javadoc_files",
+        ":android_icu4j_src_files_for_docs",
+    ],
+    srcs_lib: "framework",
+    srcs_lib_whitelist_dirs: frameworks_base_subdirs,
+    srcs_lib_whitelist_pkgs: packages_to_document,
+    libs: [
+        "core-oj",
+        "core-libart",
+        "conscrypt",
+        "bouncycastle",
+        "okhttp",
+        "ext",
+        "framework",
+        "voip-common",
+        "android.test.mock",
+    ],
+    local_sourcepaths: frameworks_base_subdirs,
+    installable: false,
+    metalava_enabled: true,
+    metalava_annotations_enabled: true,
+    metalava_previous_api: ":last-released-public-api",
+    metalava_merge_annotations_dirs: [
+        "metalava-manual",
+        "ojluni-annotated-stubs",
+    ],
+}
+
 droiddoc {
     name: "doc-comment-check-docs",
     defaults: ["framework-docs-default"],
@@ -1398,26 +1440,21 @@
 }
 
 droiddoc {
-    name: "hiddenapi-lists",
-    defaults: ["api-stubs-default"],
+    name: "hiddenapi-lists-docs",
+    defaults: ["metalava-api-stubs-default"],
     arg_files: [
         "core/res/AndroidManifest.xml",
-        ":api-version-xml",
-        "core/java/overview.html",
-        ":current-support-api",
-        "api/current.txt",
     ],
     dex_api_filename: "public-dex.txt",
     private_dex_api_filename: "private-dex.txt",
     removed_dex_api_filename: "removed-dex.txt",
-    args: framework_docs_args +
-          " -referenceonly" +
-          " -nodocs" +
-          " -showUnannotated" +
-          " -showAnnotation android.annotation.SystemApi" +
-          " -showAnnotation android.annotation.TestApi",
+    args: metalava_framework_docs_args +
+        " --show-unannotated " +
+        " --show-annotation android.annotation.SystemApi " +
+        " --show-annotation android.annotation.TestApi "
 }
 
+
 droiddoc {
     name: "hiddenapi-mappings",
     defaults: ["api-stubs-default"],
@@ -1461,48 +1498,6 @@
     ],
 }
 
-metalava_framework_docs_args = "--manifest $(location core/res/AndroidManifest.xml) " +
-    "--hide-package com.android.okhttp " +
-    "--hide-package com.android.org.conscrypt --hide-package com.android.server " +
-    "--hide RequiresPermission " +
-    "--hide MissingPermission --hide BroadcastBehavior " +
-    "--hide HiddenSuperclass --hide DeprecationMismatch --hide UnavailableSymbol " +
-    "--hide SdkConstant --hide HiddenTypeParameter --hide Todo --hide Typo"
-
-doc_defaults {
-    name: "metalava-api-stubs-default",
-    srcs: [
-        ":opt-telephony-srcs",
-        ":opt-net-voip-srcs",
-        ":openjdk_javadoc_files",
-        ":non_openjdk_javadoc_files",
-        ":android_icu4j_src_files_for_docs",
-    ],
-    srcs_lib: "framework",
-    srcs_lib_whitelist_dirs: frameworks_base_subdirs,
-    srcs_lib_whitelist_pkgs: packages_to_document,
-    libs: [
-        "core-oj",
-        "core-libart",
-        "conscrypt",
-        "bouncycastle",
-        "okhttp",
-        "ext",
-        "framework",
-        "voip-common",
-        "android.test.mock",
-    ],
-    local_sourcepaths: frameworks_base_subdirs,
-    installable: false,
-    metalava_enabled: true,
-    metalava_annotations_enabled: true,
-    metalava_previous_api: ":last-released-public-api",
-    metalava_merge_annotations_dirs: [
-        "metalava-manual",
-        "ojluni-annotated-stubs",
-    ],
-}
-
 droiddoc {
     name: "api-stubs-docs",
     defaults: ["metalava-api-stubs-default"],
diff --git a/api/current.txt b/api/current.txt
index 3c7dcc8b..0ef9d30 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -8689,6 +8689,8 @@
     method public byte[] getServiceData();
     method public byte[] getServiceDataMask();
     method public android.os.ParcelUuid getServiceDataUuid();
+    method public android.os.ParcelUuid getServiceSolicitationUuid();
+    method public android.os.ParcelUuid getServiceSolicitationUuidMask();
     method public android.os.ParcelUuid getServiceUuid();
     method public android.os.ParcelUuid getServiceUuidMask();
     method public boolean matches(android.bluetooth.le.ScanResult);
@@ -8705,6 +8707,8 @@
     method public android.bluetooth.le.ScanFilter.Builder setManufacturerData(int, byte[], byte[]);
     method public android.bluetooth.le.ScanFilter.Builder setServiceData(android.os.ParcelUuid, byte[]);
     method public android.bluetooth.le.ScanFilter.Builder setServiceData(android.os.ParcelUuid, byte[], byte[]);
+    method public android.bluetooth.le.ScanFilter.Builder setServiceSolicitationUuid(android.os.ParcelUuid);
+    method public android.bluetooth.le.ScanFilter.Builder setServiceSolicitationUuid(android.os.ParcelUuid, android.os.ParcelUuid);
     method public android.bluetooth.le.ScanFilter.Builder setServiceUuid(android.os.ParcelUuid);
     method public android.bluetooth.le.ScanFilter.Builder setServiceUuid(android.os.ParcelUuid, android.os.ParcelUuid);
   }
@@ -8717,6 +8721,7 @@
     method public byte[] getManufacturerSpecificData(int);
     method public java.util.Map<android.os.ParcelUuid, byte[]> getServiceData();
     method public byte[] getServiceData(android.os.ParcelUuid);
+    method public java.util.List<android.os.ParcelUuid> getServiceSolicitationUuids();
     method public java.util.List<android.os.ParcelUuid> getServiceUuids();
     method public int getTxPowerLevel();
   }
diff --git a/core/java/android/bluetooth/le/ScanFilter.java b/core/java/android/bluetooth/le/ScanFilter.java
index c3fae7d..c5d435b 100644
--- a/core/java/android/bluetooth/le/ScanFilter.java
+++ b/core/java/android/bluetooth/le/ScanFilter.java
@@ -58,6 +58,11 @@
     private final ParcelUuid mServiceUuidMask;
 
     @Nullable
+    private final ParcelUuid mServiceSolicitationUuid;
+    @Nullable
+    private final ParcelUuid mServiceSolicitationUuidMask;
+
+    @Nullable
     private final ParcelUuid mServiceDataUuid;
     @Nullable
     private final byte[] mServiceData;
@@ -75,12 +80,15 @@
 
 
     private ScanFilter(String name, String deviceAddress, ParcelUuid uuid,
-            ParcelUuid uuidMask, ParcelUuid serviceDataUuid,
+            ParcelUuid uuidMask, ParcelUuid solicitationUuid,
+            ParcelUuid solicitationUuidMask, ParcelUuid serviceDataUuid,
             byte[] serviceData, byte[] serviceDataMask,
             int manufacturerId, byte[] manufacturerData, byte[] manufacturerDataMask) {
         mDeviceName = name;
         mServiceUuid = uuid;
         mServiceUuidMask = uuidMask;
+        mServiceSolicitationUuid = solicitationUuid;
+        mServiceSolicitationUuidMask = solicitationUuidMask;
         mDeviceAddress = deviceAddress;
         mServiceDataUuid = serviceDataUuid;
         mServiceData = serviceData;
@@ -113,6 +121,14 @@
                 dest.writeParcelable(mServiceUuidMask, flags);
             }
         }
+        dest.writeInt(mServiceSolicitationUuid == null ? 0 : 1);
+        if (mServiceSolicitationUuid != null) {
+            dest.writeParcelable(mServiceSolicitationUuid, flags);
+            dest.writeInt(mServiceSolicitationUuidMask == null ? 0 : 1);
+            if (mServiceSolicitationUuidMask != null) {
+                dest.writeParcelable(mServiceSolicitationUuidMask, flags);
+            }
+        }
         dest.writeInt(mServiceDataUuid == null ? 0 : 1);
         if (mServiceDataUuid != null) {
             dest.writeParcelable(mServiceDataUuid, flags);
@@ -172,6 +188,17 @@
                 }
             }
             if (in.readInt() == 1) {
+                ParcelUuid solicitationUuid = in.readParcelable(
+                        ParcelUuid.class.getClassLoader());
+                builder.setServiceSolicitationUuid(solicitationUuid);
+                if (in.readInt() == 1) {
+                    ParcelUuid solicitationUuidMask = in.readParcelable(
+                            ParcelUuid.class.getClassLoader());
+                    builder.setServiceSolicitationUuid(solicitationUuid,
+                            solicitationUuidMask);
+                }
+            }
+            if (in.readInt() == 1) {
                 ParcelUuid servcieDataUuid =
                         in.readParcelable(ParcelUuid.class.getClassLoader());
                 if (in.readInt() == 1) {
@@ -231,6 +258,22 @@
         return mServiceUuidMask;
     }
 
+    /**
+     * Returns the filter set on the service Solicitation uuid.
+     */
+    @Nullable
+    public ParcelUuid getServiceSolicitationUuid() {
+        return mServiceSolicitationUuid;
+    }
+
+    /**
+     * Returns the filter set on the service Solicitation uuid mask.
+     */
+    @Nullable
+    public ParcelUuid getServiceSolicitationUuidMask() {
+        return mServiceSolicitationUuidMask;
+    }
+
     @Nullable
     public String getDeviceAddress() {
         return mDeviceAddress;
@@ -288,7 +331,7 @@
         // Scan record is null but there exist filters on it.
         if (scanRecord == null
                 && (mDeviceName != null || mServiceUuid != null || mManufacturerData != null
-                || mServiceData != null)) {
+                || mServiceData != null || mServiceSolicitationUuid != null)) {
             return false;
         }
 
@@ -303,6 +346,13 @@
             return false;
         }
 
+        // solicitation UUID match.
+        if (mServiceSolicitationUuid != null && !matchesServiceSolicitationUuids(
+                mServiceSolicitationUuid, mServiceSolicitationUuidMask,
+                scanRecord.getServiceSolicitationUuids())) {
+            return false;
+        }
+
         // Service data match
         if (mServiceDataUuid != null) {
             if (!matchesPartialData(mServiceData, mServiceDataMask,
@@ -350,6 +400,36 @@
         return BitUtils.maskedEquals(data, uuid, mask);
     }
 
+    /**
+     * Check if the solicitation uuid pattern is contained in a list of parcel uuids.
+     *
+     */
+    private static boolean matchesServiceSolicitationUuids(ParcelUuid solicitationUuid,
+            ParcelUuid parcelSolicitationUuidMask, List<ParcelUuid> solicitationUuids) {
+        if (solicitationUuid == null) {
+            return true;
+        }
+        if (solicitationUuids == null) {
+            return false;
+        }
+
+        for (ParcelUuid parcelSolicitationUuid : solicitationUuids) {
+            UUID solicitationUuidMask = parcelSolicitationUuidMask == null
+                    ? null : parcelSolicitationUuidMask.getUuid();
+            if (matchesServiceUuid(solicitationUuid.getUuid(), solicitationUuidMask,
+                    parcelSolicitationUuid.getUuid())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    // Check if the solicitation uuid pattern matches the particular service solicitation uuid.
+    private static boolean matchesServiceSolicitationUuid(UUID solicitationUuid,
+            UUID solicitationUuidMask, UUID data) {
+        return BitUtils.maskedEquals(data, solicitationUuid, solicitationUuidMask);
+    }
+
     // Check whether the data pattern matches the parsed data.
     private boolean matchesPartialData(byte[] data, byte[] dataMask, byte[] parsedData) {
         if (parsedData == null || parsedData.length < data.length) {
@@ -376,6 +456,8 @@
         return "BluetoothLeScanFilter [mDeviceName=" + mDeviceName + ", mDeviceAddress="
                 + mDeviceAddress
                 + ", mUuid=" + mServiceUuid + ", mUuidMask=" + mServiceUuidMask
+                + ", mServiceSolicitationUuid=" + mServiceSolicitationUuid
+                + ", mServiceSolicitationUuidMask=" + mServiceSolicitationUuidMask
                 + ", mServiceDataUuid=" + Objects.toString(mServiceDataUuid) + ", mServiceData="
                 + Arrays.toString(mServiceData) + ", mServiceDataMask="
                 + Arrays.toString(mServiceDataMask) + ", mManufacturerId=" + mManufacturerId
@@ -391,7 +473,8 @@
                 mServiceDataUuid,
                 Arrays.hashCode(mServiceData),
                 Arrays.hashCode(mServiceDataMask),
-                mServiceUuid, mServiceUuidMask);
+                mServiceUuid, mServiceUuidMask,
+                mServiceSolicitationUuid, mServiceSolicitationUuidMask);
     }
 
     @Override
@@ -412,7 +495,10 @@
                 && Objects.deepEquals(mServiceData, other.mServiceData)
                 && Objects.deepEquals(mServiceDataMask, other.mServiceDataMask)
                 && Objects.equals(mServiceUuid, other.mServiceUuid)
-                && Objects.equals(mServiceUuidMask, other.mServiceUuidMask);
+                && Objects.equals(mServiceUuidMask, other.mServiceUuidMask)
+                && Objects.equals(mServiceSolicitationUuid, other.mServiceSolicitationUuid)
+                && Objects.equals(mServiceSolicitationUuidMask,
+                        other.mServiceSolicitationUuidMask);
     }
 
     /**
@@ -435,6 +521,9 @@
         private ParcelUuid mServiceUuid;
         private ParcelUuid mUuidMask;
 
+        private ParcelUuid mServiceSolicitationUuid;
+        private ParcelUuid mServiceSolicitationUuidMask;
+
         private ParcelUuid mServiceDataUuid;
         private byte[] mServiceData;
         private byte[] mServiceDataMask;
@@ -493,6 +582,36 @@
             return this;
         }
 
+
+        /**
+         * Set filter on service solicitation uuid.
+         */
+        public Builder setServiceSolicitationUuid(ParcelUuid serviceSolicitationUuid) {
+            mServiceSolicitationUuid = serviceSolicitationUuid;
+            return this;
+        }
+
+
+        /**
+         * Set filter on partial service Solicitation uuid. The {@code SolicitationUuidMask} is the
+         * bit mask for the {@code serviceSolicitationUuid}. Set any bit in the mask to 1 to
+         * indicate a match is needed for the bit in {@code serviceSolicitationUuid}, and 0 to
+         * ignore that bit.
+         *
+         * @throws IllegalArgumentException If {@code serviceSolicitationUuid} is {@code null} but
+         *             {@code serviceSolicitationUuidMask} is not {@code null}.
+         */
+        public Builder setServiceSolicitationUuid(ParcelUuid serviceSolicitationUuid,
+                ParcelUuid solicitationUuidMask) {
+            if (mServiceSolicitationUuidMask != null && mServiceSolicitationUuid == null) {
+                throw new IllegalArgumentException(
+                        "SolicitationUuid is null while SolicitationUuidMask is not null!");
+            }
+            mServiceSolicitationUuid = serviceSolicitationUuid;
+            mServiceSolicitationUuidMask = solicitationUuidMask;
+            return this;
+        }
+
         /**
          * Set filtering on service data.
          *
@@ -598,7 +717,8 @@
          */
         public ScanFilter build() {
             return new ScanFilter(mDeviceName, mDeviceAddress,
-                    mServiceUuid, mUuidMask,
+                    mServiceUuid, mUuidMask, mServiceSolicitationUuid,
+                    mServiceSolicitationUuidMask,
                     mServiceDataUuid, mServiceData, mServiceDataMask,
                     mManufacturerId, mManufacturerData, mManufacturerDataMask);
         }
diff --git a/core/java/android/bluetooth/le/ScanRecord.java b/core/java/android/bluetooth/le/ScanRecord.java
index 07ed18d..7988008 100644
--- a/core/java/android/bluetooth/le/ScanRecord.java
+++ b/core/java/android/bluetooth/le/ScanRecord.java
@@ -51,6 +51,9 @@
     private static final int DATA_TYPE_SERVICE_DATA_16_BIT = 0x16;
     private static final int DATA_TYPE_SERVICE_DATA_32_BIT = 0x20;
     private static final int DATA_TYPE_SERVICE_DATA_128_BIT = 0x21;
+    private static final int DATA_TYPE_SERVICE_SOLICITATION_UUIDS_16_BIT = 0x14;
+    private static final int DATA_TYPE_SERVICE_SOLICITATION_UUIDS_32_BIT = 0x1F;
+    private static final int DATA_TYPE_SERVICE_SOLICITATION_UUIDS_128_BIT = 0x15;
     private static final int DATA_TYPE_MANUFACTURER_SPECIFIC_DATA = 0xFF;
 
     // Flags of the advertising data.
@@ -58,6 +61,8 @@
 
     @Nullable
     private final List<ParcelUuid> mServiceUuids;
+    @Nullable
+    private final List<ParcelUuid> mServiceSolicitationUuids;
 
     private final SparseArray<byte[]> mManufacturerSpecificData;
 
@@ -89,6 +94,15 @@
     }
 
     /**
+     * Returns a list of service solicitation UUIDs within the advertisement that are used to
+     * identify the Bluetooth GATT services.
+     */
+    @Nullable
+    public List<ParcelUuid> getServiceSolicitationUuids() {
+        return mServiceSolicitationUuids;
+    }
+
+    /**
      * Returns a sparse array of manufacturer identifier and its corresponding manufacturer specific
      * data.
      */
@@ -151,10 +165,12 @@
     }
 
     private ScanRecord(List<ParcelUuid> serviceUuids,
+            List<ParcelUuid> serviceSolicitationUuids,
             SparseArray<byte[]> manufacturerData,
             Map<ParcelUuid, byte[]> serviceData,
             int advertiseFlags, int txPowerLevel,
             String localName, byte[] bytes) {
+        mServiceSolicitationUuids = serviceSolicitationUuids;
         mServiceUuids = serviceUuids;
         mManufacturerSpecificData = manufacturerData;
         mServiceData = serviceData;
@@ -184,6 +200,7 @@
         int currentPos = 0;
         int advertiseFlag = -1;
         List<ParcelUuid> serviceUuids = new ArrayList<ParcelUuid>();
+        List<ParcelUuid> serviceSolicitationUuids = new ArrayList<ParcelUuid>();
         String localName = null;
         int txPowerLevel = Integer.MIN_VALUE;
 
@@ -220,6 +237,18 @@
                         parseServiceUuid(scanRecord, currentPos, dataLength,
                                 BluetoothUuid.UUID_BYTES_128_BIT, serviceUuids);
                         break;
+                    case DATA_TYPE_SERVICE_SOLICITATION_UUIDS_16_BIT:
+                        parseServiceSolicitationUuid(scanRecord, currentPos, dataLength,
+                                BluetoothUuid.UUID_BYTES_16_BIT, serviceSolicitationUuids);
+                        break;
+                    case DATA_TYPE_SERVICE_SOLICITATION_UUIDS_32_BIT:
+                        parseServiceSolicitationUuid(scanRecord, currentPos, dataLength,
+                                BluetoothUuid.UUID_BYTES_32_BIT, serviceSolicitationUuids);
+                        break;
+                    case DATA_TYPE_SERVICE_SOLICITATION_UUIDS_128_BIT:
+                        parseServiceSolicitationUuid(scanRecord, currentPos, dataLength,
+                                BluetoothUuid.UUID_BYTES_128_BIT, serviceSolicitationUuids);
+                        break;
                     case DATA_TYPE_LOCAL_NAME_SHORT:
                     case DATA_TYPE_LOCAL_NAME_COMPLETE:
                         localName = new String(
@@ -265,19 +294,23 @@
             if (serviceUuids.isEmpty()) {
                 serviceUuids = null;
             }
-            return new ScanRecord(serviceUuids, manufacturerData, serviceData,
-                    advertiseFlag, txPowerLevel, localName, scanRecord);
+            if (serviceSolicitationUuids.isEmpty()) {
+                serviceSolicitationUuids = null;
+            }
+            return new ScanRecord(serviceUuids, serviceSolicitationUuids, manufacturerData,
+                    serviceData, advertiseFlag, txPowerLevel, localName, scanRecord);
         } catch (Exception e) {
             Log.e(TAG, "unable to parse scan record: " + Arrays.toString(scanRecord));
             // As the record is invalid, ignore all the parsed results for this packet
             // and return an empty record with raw scanRecord bytes in results
-            return new ScanRecord(null, null, null, -1, Integer.MIN_VALUE, null, scanRecord);
+            return new ScanRecord(null, null, null, null, -1, Integer.MIN_VALUE, null, scanRecord);
         }
     }
 
     @Override
     public String toString() {
         return "ScanRecord [mAdvertiseFlags=" + mAdvertiseFlags + ", mServiceUuids=" + mServiceUuids
+                + ", mServiceSolicitationUuids=" + mServiceSolicitationUuids
                 + ", mManufacturerSpecificData=" + BluetoothLeUtils.toString(
                 mManufacturerSpecificData)
                 + ", mServiceData=" + BluetoothLeUtils.toString(mServiceData)
@@ -297,6 +330,20 @@
         return currentPos;
     }
 
+    /**
+     * Parse service Solicitation UUIDs.
+     */
+    private static int parseServiceSolicitationUuid(byte[] scanRecord, int currentPos,
+            int dataLength, int uuidLength, List<ParcelUuid> serviceSolicitationUuids) {
+        while (dataLength > 0) {
+            byte[] uuidBytes = extractBytes(scanRecord, currentPos, uuidLength);
+            serviceSolicitationUuids.add(BluetoothUuid.parseUuidFrom(uuidBytes));
+            dataLength -= uuidLength;
+            currentPos += uuidLength;
+        }
+        return currentPos;
+    }
+
     // Helper method to extract bytes from byte array.
     private static byte[] extractBytes(byte[] scanRecord, int start, int length) {
         byte[] bytes = new byte[length];
diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java
index fb916d3..ce18796 100644
--- a/core/java/android/net/ConnectivityManager.java
+++ b/core/java/android/net/ConnectivityManager.java
@@ -3840,7 +3840,7 @@
     @UnsupportedAppUsage
     public static boolean setProcessDefaultNetworkForHostResolution(Network network) {
         return NetworkUtils.bindProcessToNetworkForHostResolution(
-                network == null ? NETID_UNSET : network.netId);
+                (network == null) ? NETID_UNSET : network.getNetIdForResolv());
     }
 
     /**
diff --git a/core/java/android/net/Network.java b/core/java/android/net/Network.java
index 142023d..bf2344d 100644
--- a/core/java/android/net/Network.java
+++ b/core/java/android/net/Network.java
@@ -100,21 +100,29 @@
     // anytime and (b) receivers should be explicit about attempts to bypass
     // Private DNS so that the intent of the code is easily determined and
     // code search audits are possible.
-    private boolean mPrivateDnsBypass = false;
+    private final transient boolean mPrivateDnsBypass;
 
     /**
      * @hide
      */
     @UnsupportedAppUsage
     public Network(int netId) {
+        this(netId, false);
+    }
+
+    /**
+     * @hide
+     */
+    public Network(int netId, boolean privateDnsBypass) {
         this.netId = netId;
+        this.mPrivateDnsBypass = privateDnsBypass;
     }
 
     /**
      * @hide
      */
     public Network(Network that) {
-        this.netId = that.netId;
+        this(that.netId, that.mPrivateDnsBypass);
     }
 
     /**
@@ -133,8 +141,7 @@
      * Operates the same as {@code InetAddress.getByName} except that host
      * resolution is done on this network.
      *
-     * @param host
-     *            the hostName to be resolved to an address or {@code null}.
+     * @param host the hostname to be resolved to an address or {@code null}.
      * @return the {@code InetAddress} instance representing the host.
      * @throws UnknownHostException
      *             if the address lookup fails.
@@ -144,14 +151,14 @@
     }
 
     /**
-     * Specify whether or not Private DNS should be bypassed when attempting
+     * Obtain a Network object for which Private DNS is to be bypassed when attempting
      * to use {@link #getAllByName(String)}/{@link #getByName(String)} methods on the given
      * instance for hostname resolution.
      *
      * @hide
      */
-    public void setPrivateDnsBypass(boolean bypass) {
-        mPrivateDnsBypass = bypass;
+    public Network getPrivateDnsBypassingCopy() {
+        return new Network(netId, true);
     }
 
     /**
diff --git a/core/java/android/net/SntpClient.java b/core/java/android/net/SntpClient.java
index 10c0ce2..b8d7cf1 100644
--- a/core/java/android/net/SntpClient.java
+++ b/core/java/android/net/SntpClient.java
@@ -85,19 +85,16 @@
      * @return true if the transaction was successful.
      */
     public boolean requestTime(String host, int timeout, Network network) {
-        // This flag only affects DNS resolution and not other socket semantics,
-        // therefore it's safe to set unilaterally rather than take more
-        // defensive measures like making a copy.
-        network.setPrivateDnsBypass(true);
+        final Network networkForResolv = network.getPrivateDnsBypassingCopy();
         InetAddress address = null;
         try {
-            address = network.getByName(host);
+            address = networkForResolv.getByName(host);
         } catch (Exception e) {
             EventLogTags.writeNtpFailure(host, e.toString());
             if (DBG) Log.d(TAG, "request time failed: " + e);
             return false;
         }
-        return requestTime(address, NTP_PORT, timeout, network);
+        return requestTime(address, NTP_PORT, timeout, networkForResolv);
     }
 
     public boolean requestTime(InetAddress address, int port, int timeout, Network network) {
diff --git a/core/java/android/net/TrafficStats.java b/core/java/android/net/TrafficStats.java
index 44f7e48..1b22911 100644
--- a/core/java/android/net/TrafficStats.java
+++ b/core/java/android/net/TrafficStats.java
@@ -139,6 +139,8 @@
     public static final int TAG_SYSTEM_GPS = 0xFFFFFF44;
     /** @hide */
     public static final int TAG_SYSTEM_PAC = 0xFFFFFF45;
+    /** @hide */
+    public static final int TAG_SYSTEM_DHCP_SERVER = 0xFFFFFF46;
 
     private static INetworkStatsService sStatsService;
 
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 88d09af..7a4ef5d 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -9466,6 +9466,17 @@
         public static final String TETHER_OFFLOAD_DISABLED = "tether_offload_disabled";
 
         /**
+         * Use the old dnsmasq DHCP server for tethering instead of the framework implementation.
+         *
+         * Integer values are interpreted as boolean, and the absence of an explicit setting
+         * is interpreted as |true|.
+         * TODO: make the default |false|
+         * @hide
+         */
+        public static final String TETHER_ENABLE_LEGACY_DHCP_SERVER =
+                "tether_enable_legacy_dhcp_server";
+
+        /**
          * List of certificate (hex string representation of the application's certificate - SHA-1
          * or SHA-256) and carrier app package pairs which are whitelisted to prompt the user for
          * install when a sim card with matching UICC carrier privilege rules is inserted.  The
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index ca204ec..632e439 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -3446,6 +3446,9 @@
          empty string is passed in -->
     <string name="config_wlan_network_service_package" translatable="false"></string>
 
+    <!-- Telephony qualified networks service package name to bind to by default. -->
+    <string name="config_qualified_networks_service_package" translatable="false"></string>
+
     <!-- Wear devices: Controls the radios affected by Activity Mode. -->
     <string-array name="config_wearActivityModeRadios">
         <item>"wifi"</item>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index f2539e7..de5dc43 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -273,6 +273,7 @@
   <java-symbol type="string" name="config_wlan_network_service_package" />
   <java-symbol type="string" name="config_wwan_data_service_package" />
   <java-symbol type="string" name="config_wlan_data_service_package" />
+  <java-symbol type="string" name="config_qualified_networks_service_package" />
   <java-symbol type="bool" name="config_networkSamplingWakesDevice" />
   <java-symbol type="bool" name="config_showMenuShortcutsWhenKeyboardPresent" />
   <java-symbol type="bool" name="config_sip_wifi_only" />
diff --git a/core/tests/coretests/src/android/provider/SettingsBackupTest.java b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
index 815294c..02e0d02 100644
--- a/core/tests/coretests/src/android/provider/SettingsBackupTest.java
+++ b/core/tests/coretests/src/android/provider/SettingsBackupTest.java
@@ -423,6 +423,7 @@
                     Settings.Global.TETHER_DUN_REQUIRED,
                     Settings.Global.TETHER_OFFLOAD_DISABLED,
                     Settings.Global.TETHER_SUPPORTED,
+                    Settings.Global.TETHER_ENABLE_LEGACY_DHCP_SERVER,
                     Settings.Global.TEXT_CLASSIFIER_CONSTANTS,
                     Settings.Global.THEATER_MODE_ON,
                     Settings.Global.TIME_ONLY_MODE_CONSTANTS,
diff --git a/packages/CaptivePortalLogin/src/com/android/captiveportallogin/CaptivePortalLoginActivity.java b/packages/CaptivePortalLogin/src/com/android/captiveportallogin/CaptivePortalLoginActivity.java
index 3630005..fd1212a 100644
--- a/packages/CaptivePortalLogin/src/com/android/captiveportallogin/CaptivePortalLoginActivity.java
+++ b/packages/CaptivePortalLogin/src/com/android/captiveportallogin/CaptivePortalLoginActivity.java
@@ -33,7 +33,6 @@
 import android.net.Proxy;
 import android.net.Uri;
 import android.net.captiveportal.CaptivePortalProbeSpec;
-import android.net.dns.ResolvUtil;
 import android.net.http.SslError;
 import android.net.wifi.WifiInfo;
 import android.os.Build;
@@ -132,9 +131,9 @@
         }
 
         // Also initializes proxy system properties.
+        mNetwork = mNetwork.getPrivateDnsBypassingCopy();
         mCm.bindProcessToNetwork(mNetwork);
-        mCm.setProcessDefaultNetworkForHostResolution(
-                ResolvUtil.getNetworkWithUseLocalNameserversFlag(mNetwork));
+        mCm.setProcessDefaultNetworkForHostResolution(mNetwork);
 
         // Proxy system properties must be initialized before setContentView is called because
         // setContentView initializes the WebView logic which in turn reads the system properties.
@@ -334,7 +333,6 @@
         // TODO: reuse NetworkMonitor facilities for consistent captive portal detection.
         new Thread(new Runnable() {
             public void run() {
-                final Network network = ResolvUtil.makeNetworkWithPrivateDnsBypass(mNetwork);
                 // Give time for captive portal to open.
                 try {
                     Thread.sleep(1000);
@@ -344,7 +342,7 @@
                 int httpResponseCode = 500;
                 String locationHeader = null;
                 try {
-                    urlConnection = (HttpURLConnection) network.openConnection(mUrl);
+                    urlConnection = (HttpURLConnection) mNetwork.openConnection(mUrl);
                     urlConnection.setInstanceFollowRedirects(false);
                     urlConnection.setConnectTimeout(SOCKET_TIMEOUT_MS);
                     urlConnection.setReadTimeout(SOCKET_TIMEOUT_MS);
diff --git a/packages/SettingsLib/src/com/android/settingslib/Utils.java b/packages/SettingsLib/src/com/android/settingslib/Utils.java
index 505cfea..aa8cae5 100644
--- a/packages/SettingsLib/src/com/android/settingslib/Utils.java
+++ b/packages/SettingsLib/src/com/android/settingslib/Utils.java
@@ -27,7 +27,7 @@
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.UserIcons;
 import com.android.settingslib.drawable.UserIconDrawable;
-import com.android.settingslib.wrapper.LocationManagerWrapper;
+
 import java.text.NumberFormat;
 
 public class Utils {
@@ -69,8 +69,7 @@
                 intent, UserHandle.of(userId), android.Manifest.permission.WRITE_SECURE_SETTINGS);
         LocationManager locationManager =
                 (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
-        LocationManagerWrapper wrapper = new LocationManagerWrapper(locationManager);
-        wrapper.setLocationEnabledForUser(enabled, UserHandle.of(userId));
+        locationManager.setLocationEnabledForUser(enabled, UserHandle.of(userId));
     }
 
     public static boolean updateLocationMode(Context context, int oldMode, int newMode, int userId,
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/A2dpProfile.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/A2dpProfile.java
index 65535b5..541877c 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/A2dpProfile.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/A2dpProfile.java
@@ -20,7 +20,6 @@
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothClass;
 import android.bluetooth.BluetoothCodecConfig;
-import android.bluetooth.BluetoothCodecStatus;
 import android.bluetooth.BluetoothDevice;
 import android.bluetooth.BluetoothProfile;
 import android.bluetooth.BluetoothUuid;
@@ -28,9 +27,7 @@
 import android.os.ParcelUuid;
 import android.util.Log;
 
-import com.android.internal.annotations.VisibleForTesting;
 import com.android.settingslib.R;
-import com.android.settingslib.wrapper.BluetoothA2dpWrapper;
 
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -43,7 +40,6 @@
     private Context mContext;
 
     private BluetoothA2dp mService;
-    private BluetoothA2dpWrapper mServiceWrapper;
     private boolean mIsProfileReady;
 
     private final LocalBluetoothAdapter mLocalAdapter;
@@ -67,7 +63,6 @@
         public void onServiceConnected(int profile, BluetoothProfile proxy) {
             if (V) Log.d(TAG,"Bluetooth service connected");
             mService = (BluetoothA2dp) proxy;
-            mServiceWrapper = new BluetoothA2dpWrapper(mService);
             // We just bound to the service, so refresh the UI for any connected A2DP devices.
             List<BluetoothDevice> deviceList = mService.getConnectedDevices();
             while (!deviceList.isEmpty()) {
@@ -110,11 +105,6 @@
                 BluetoothProfile.A2DP);
     }
 
-    @VisibleForTesting
-    void setBluetoothA2dpWrapper(BluetoothA2dpWrapper wrapper) {
-        mServiceWrapper = wrapper;
-    }
-
     public boolean isConnectable() {
         return true;
     }
@@ -194,12 +184,12 @@
     }
 
     public boolean supportsHighQualityAudio(BluetoothDevice device) {
-        int support = mServiceWrapper.supportsOptionalCodecs(device);
+        int support = mService.supportsOptionalCodecs(device);
         return support == BluetoothA2dp.OPTIONAL_CODECS_SUPPORTED;
     }
 
     public boolean isHighQualityAudioEnabled(BluetoothDevice device) {
-        int enabled = mServiceWrapper.getOptionalCodecsEnabled(device);
+        int enabled = mService.getOptionalCodecsEnabled(device);
         if (enabled != BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN) {
             return enabled == BluetoothA2dp.OPTIONAL_CODECS_PREF_ENABLED;
         } else if (getConnectionStatus(device) != BluetoothProfile.STATE_CONNECTED &&
@@ -210,8 +200,8 @@
             return true;
         }
         BluetoothCodecConfig codecConfig = null;
-        if (mServiceWrapper.getCodecStatus(device) != null) {
-            codecConfig = mServiceWrapper.getCodecStatus(device).getCodecConfig();
+        if (mService.getCodecStatus(device) != null) {
+            codecConfig = mService.getCodecStatus(device).getCodecConfig();
         }
         if (codecConfig != null)  {
             return !codecConfig.isMandatoryCodec();
@@ -224,7 +214,7 @@
         int prefValue = enabled
                 ? BluetoothA2dp.OPTIONAL_CODECS_PREF_ENABLED
                 : BluetoothA2dp.OPTIONAL_CODECS_PREF_DISABLED;
-        mServiceWrapper.setOptionalCodecsEnabled(device, prefValue);
+        mService.setOptionalCodecsEnabled(device, prefValue);
         if (getConnectionStatus(device) != BluetoothProfile.STATE_CONNECTED) {
             return;
         }
@@ -244,8 +234,8 @@
         // We want to get the highest priority codec, since that's the one that will be used with
         // this device, and see if it is high-quality (ie non-mandatory).
         BluetoothCodecConfig[] selectable = null;
-        if (mServiceWrapper.getCodecStatus(device) != null) {
-            selectable = mServiceWrapper.getCodecStatus(device).getCodecsSelectableCapabilities();
+        if (mService.getCodecStatus(device) != null) {
+            selectable = mService.getCodecStatus(device).getCodecsSelectableCapabilities();
             // To get the highest priority, we sort in reverse.
             Arrays.sort(selectable,
                     (a, b) -> {
diff --git a/packages/SettingsLib/src/com/android/settingslib/license/LicenseHtmlLoader.java b/packages/SettingsLib/src/com/android/settingslib/license/LicenseHtmlLoader.java
index 92044c3..307b025 100644
--- a/packages/SettingsLib/src/com/android/settingslib/license/LicenseHtmlLoader.java
+++ b/packages/SettingsLib/src/com/android/settingslib/license/LicenseHtmlLoader.java
@@ -36,7 +36,9 @@
             "/system/etc/NOTICE.xml.gz",
             "/vendor/etc/NOTICE.xml.gz",
             "/odm/etc/NOTICE.xml.gz",
-            "/oem/etc/NOTICE.xml.gz"};
+            "/oem/etc/NOTICE.xml.gz",
+            "/product/etc/NOTICE.xml.gz",
+            "/product_services/etc/NOTICE.xml.gz"};
     private static final String NOTICE_HTML_FILE_NAME = "NOTICE.html";
 
     private Context mContext;
diff --git a/packages/SettingsLib/src/com/android/settingslib/wrapper/BluetoothA2dpWrapper.java b/packages/SettingsLib/src/com/android/settingslib/wrapper/BluetoothA2dpWrapper.java
deleted file mode 100644
index 17e3401..0000000
--- a/packages/SettingsLib/src/com/android/settingslib/wrapper/BluetoothA2dpWrapper.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * 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 com.android.settingslib.wrapper;
-
-import android.bluetooth.BluetoothA2dp;
-import android.bluetooth.BluetoothCodecStatus;
-import android.bluetooth.BluetoothDevice;
-
-/**
- * This class replicates some methods of android.bluetooth.BluetoothA2dp that are new and not
- * yet available in our current version of Robolectric. It provides a thin wrapper to call the real
- * methods in production and a mock in tests.
- */
-public class BluetoothA2dpWrapper {
-
-    private BluetoothA2dp mService;
-
-    public BluetoothA2dpWrapper(BluetoothA2dp service) {
-        mService = service;
-    }
-
-    /**
-     * @return the real {@code BluetoothA2dp} object
-     */
-    public BluetoothA2dp getService() {
-        return mService;
-    }
-
-    /**
-     * Wraps {@code BluetoothA2dp.getCodecStatus}
-     */
-    public BluetoothCodecStatus getCodecStatus(BluetoothDevice device) {
-        return mService.getCodecStatus(device);
-    }
-
-    /**
-     * Wraps {@code BluetoothA2dp.supportsOptionalCodecs}
-     */
-    public int supportsOptionalCodecs(BluetoothDevice device) {
-        return mService.supportsOptionalCodecs(device);
-    }
-
-    /**
-     * Wraps {@code BluetoothA2dp.getOptionalCodecsEnabled}
-     */
-    public int getOptionalCodecsEnabled(BluetoothDevice device) {
-        return mService.getOptionalCodecsEnabled(device);
-    }
-
-    /**
-     * Wraps {@code BluetoothA2dp.setOptionalCodecsEnabled}
-     */
-    public void setOptionalCodecsEnabled(BluetoothDevice device, int value) {
-        mService.setOptionalCodecsEnabled(device, value);
-    }
-}
diff --git a/packages/SettingsLib/src/com/android/settingslib/wrapper/LocationManagerWrapper.java b/packages/SettingsLib/src/com/android/settingslib/wrapper/LocationManagerWrapper.java
deleted file mode 100644
index 1a268a6..0000000
--- a/packages/SettingsLib/src/com/android/settingslib/wrapper/LocationManagerWrapper.java
+++ /dev/null
@@ -1,64 +0,0 @@
-/*
- * 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 com.android.settingslib.wrapper;
-
-import android.location.LocationManager;
-import android.os.UserHandle;
-
-/**
- * This class replicates some methods of android.location.LocationManager that are new and not
- * yet available in our current version of Robolectric. It provides a thin wrapper to call the real
- * methods in production and a mock in tests.
- */
-public class LocationManagerWrapper {
-
-    private LocationManager mLocationManager;
-
-    public LocationManagerWrapper(LocationManager locationManager) {
-        mLocationManager = locationManager;
-    }
-
-    /** Returns the real {@code LocationManager} object */
-    public LocationManager getLocationManager() {
-        return mLocationManager;
-    }
-
-    /** Wraps {@code LocationManager.isProviderEnabled} method */
-    public boolean isProviderEnabled(String provider) {
-        return mLocationManager.isProviderEnabled(provider);
-    }
-
-    /** Wraps {@code LocationManager.setProviderEnabledForUser} method */
-    public void setProviderEnabledForUser(String provider, boolean enabled, UserHandle userHandle) {
-        mLocationManager.setProviderEnabledForUser(provider, enabled, userHandle);
-    }
-
-    /** Wraps {@code LocationManager.isLocationEnabled} method */
-    public boolean isLocationEnabled() {
-        return mLocationManager.isLocationEnabled();
-    }
-
-    /** Wraps {@code LocationManager.isLocationEnabledForUser} method */
-    public boolean isLocationEnabledForUser(UserHandle userHandle) {
-        return mLocationManager.isLocationEnabledForUser(userHandle);
-    }
-
-    /** Wraps {@code LocationManager.setLocationEnabledForUser} method */
-    public void setLocationEnabledForUser(boolean enabled, UserHandle userHandle) {
-        mLocationManager.setLocationEnabledForUser(enabled, userHandle);
-    }
-}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java
index a79f841..09a2bd2 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/UtilsTest.java
@@ -41,8 +41,6 @@
 import android.provider.Settings.Secure;
 import android.text.TextUtils;
 
-import com.android.settingslib.wrapper.LocationManagerWrapper;
-
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -63,7 +61,7 @@
 @RunWith(SettingsLibRobolectricTestRunner.class)
 @Config(shadows = {
             UtilsTest.ShadowSecure.class,
-            UtilsTest.ShadowLocationManagerWrapper.class})
+            UtilsTest.ShadowLocationManager.class})
 public class UtilsTest {
     private static final double[] TEST_PERCENTAGES = {0, 0.4, 0.5, 0.6, 49, 49.3, 49.8, 50, 100};
     private static final String PERCENTAGE_0 = "0%";
@@ -192,8 +190,8 @@
         }
     }
 
-    @Implements(value = LocationManagerWrapper.class)
-    public static class ShadowLocationManagerWrapper {
+    @Implements(value = LocationManager.class)
+    public static class ShadowLocationManager {
 
         @Implementation
         public void setLocationEnabledForUser(boolean enabled, UserHandle userHandle) {
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/A2dpProfileTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/A2dpProfileTest.java
index 334ea16..ef13a5f 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/A2dpProfileTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/A2dpProfileTest.java
@@ -31,7 +31,6 @@
 import android.content.res.Resources;
 
 import com.android.settingslib.R;
-import com.android.settingslib.wrapper.BluetoothA2dpWrapper;
 
 import org.junit.Before;
 import org.junit.Test;
@@ -49,7 +48,6 @@
     @Mock LocalBluetoothProfileManager mProfileManager;
     @Mock BluetoothDevice mDevice;
     @Mock BluetoothA2dp mBluetoothA2dp;
-    @Mock BluetoothA2dpWrapper mBluetoothA2dpWrapper;
     BluetoothProfile.ServiceListener mServiceListener;
 
     A2dpProfile mProfile;
@@ -68,31 +66,30 @@
 
         mProfile = new A2dpProfile(mContext, mAdapter, mDeviceManager, mProfileManager);
         mServiceListener.onServiceConnected(BluetoothProfile.A2DP, mBluetoothA2dp);
-        mProfile.setBluetoothA2dpWrapper(mBluetoothA2dpWrapper);
     }
 
     @Test
     public void supportsHighQualityAudio() {
-        when(mBluetoothA2dpWrapper.supportsOptionalCodecs(any())).thenReturn(
+        when(mBluetoothA2dp.supportsOptionalCodecs(any())).thenReturn(
                 BluetoothA2dp.OPTIONAL_CODECS_SUPPORTED);
         assertThat(mProfile.supportsHighQualityAudio(mDevice)).isTrue();
 
-        when(mBluetoothA2dpWrapper.supportsOptionalCodecs(any())).thenReturn(
+        when(mBluetoothA2dp.supportsOptionalCodecs(any())).thenReturn(
                 BluetoothA2dp.OPTIONAL_CODECS_NOT_SUPPORTED);
         assertThat(mProfile.supportsHighQualityAudio(mDevice)).isFalse();
 
-        when(mBluetoothA2dpWrapper.supportsOptionalCodecs(any())).thenReturn(
+        when(mBluetoothA2dp.supportsOptionalCodecs(any())).thenReturn(
                 BluetoothA2dp.OPTIONAL_CODECS_SUPPORT_UNKNOWN);
         assertThat(mProfile.supportsHighQualityAudio(mDevice)).isFalse();
     }
 
     @Test
     public void isHighQualityAudioEnabled() {
-        when(mBluetoothA2dpWrapper.getOptionalCodecsEnabled(any())).thenReturn(
+        when(mBluetoothA2dp.getOptionalCodecsEnabled(any())).thenReturn(
                 BluetoothA2dp.OPTIONAL_CODECS_PREF_ENABLED);
         assertThat(mProfile.isHighQualityAudioEnabled(mDevice)).isTrue();
 
-        when(mBluetoothA2dpWrapper.getOptionalCodecsEnabled(any())).thenReturn(
+        when(mBluetoothA2dp.getOptionalCodecsEnabled(any())).thenReturn(
                 BluetoothA2dp.OPTIONAL_CODECS_PREF_DISABLED);
         assertThat(mProfile.isHighQualityAudioEnabled(mDevice)).isFalse();
 
@@ -100,23 +97,23 @@
         // then isHighQualityAudioEnabled() should return true or false based on whether optional
         // codecs are supported. If the device is connected then we should ask it directly, but if
         // the device isn't connected then rely on the stored pref about such support.
-        when(mBluetoothA2dpWrapper.getOptionalCodecsEnabled(any())).thenReturn(
+        when(mBluetoothA2dp.getOptionalCodecsEnabled(any())).thenReturn(
                 BluetoothA2dp.OPTIONAL_CODECS_PREF_UNKNOWN);
         when(mBluetoothA2dp.getConnectionState(any())).thenReturn(
                 BluetoothProfile.STATE_DISCONNECTED);
 
-        when(mBluetoothA2dpWrapper.supportsOptionalCodecs(any())).thenReturn(
+        when(mBluetoothA2dp.supportsOptionalCodecs(any())).thenReturn(
                 BluetoothA2dp.OPTIONAL_CODECS_NOT_SUPPORTED);
         assertThat(mProfile.isHighQualityAudioEnabled(mDevice)).isFalse();
 
-        when(mBluetoothA2dpWrapper.supportsOptionalCodecs(any())).thenReturn(
+        when(mBluetoothA2dp.supportsOptionalCodecs(any())).thenReturn(
                 BluetoothA2dp.OPTIONAL_CODECS_SUPPORTED);
         assertThat(mProfile.isHighQualityAudioEnabled(mDevice)).isTrue();
 
         when(mBluetoothA2dp.getConnectionState(any())).thenReturn(
                 BluetoothProfile.STATE_CONNECTED);
         BluetoothCodecStatus status = mock(BluetoothCodecStatus.class);
-        when(mBluetoothA2dpWrapper.getCodecStatus(mDevice)).thenReturn(status);
+        when(mBluetoothA2dp.getCodecStatus(mDevice)).thenReturn(status);
         BluetoothCodecConfig config = mock(BluetoothCodecConfig.class);
         when(status.getCodecConfig()).thenReturn(config);
         when(config.isMandatoryCodec()).thenReturn(false);
@@ -151,14 +148,14 @@
 
         // Most tests want to simulate optional codecs being supported by the device, so do that
         // by default here.
-        when(mBluetoothA2dpWrapper.supportsOptionalCodecs(any())).thenReturn(
+        when(mBluetoothA2dp.supportsOptionalCodecs(any())).thenReturn(
                 BluetoothA2dp.OPTIONAL_CODECS_SUPPORTED);
     }
 
     @Test
     public void getLableCodecsNotSupported() {
         setupLabelTest();
-        when(mBluetoothA2dpWrapper.supportsOptionalCodecs(any())).thenReturn(
+        when(mBluetoothA2dp.supportsOptionalCodecs(any())).thenReturn(
                 BluetoothA2dp.OPTIONAL_CODECS_NOT_SUPPORTED);
         assertThat(mProfile.getHighQualityAudioOptionLabel(mDevice)).isEqualTo(UNKNOWN_CODEC_LABEL);
     }
@@ -179,7 +176,7 @@
         BluetoothCodecStatus status = mock(BluetoothCodecStatus.class);
         BluetoothCodecConfig config = mock(BluetoothCodecConfig.class);
         BluetoothCodecConfig[] configs = {config};
-        when(mBluetoothA2dpWrapper.getCodecStatus(mDevice)).thenReturn(status);
+        when(mBluetoothA2dp.getCodecStatus(mDevice)).thenReturn(status);
         when(status.getCodecsSelectableCapabilities()).thenReturn(configs);
 
         when(config.isMandatoryCodec()).thenReturn(true);
@@ -194,7 +191,7 @@
         BluetoothCodecStatus status = mock(BluetoothCodecStatus.class);
         BluetoothCodecConfig config = mock(BluetoothCodecConfig.class);
         BluetoothCodecConfig[] configs = {config};
-        when(mBluetoothA2dpWrapper.getCodecStatus(mDevice)).thenReturn(status);
+        when(mBluetoothA2dp.getCodecStatus(mDevice)).thenReturn(status);
         when(status.getCodecsSelectableCapabilities()).thenReturn(configs);
 
         when(config.isMandatoryCodec()).thenReturn(false);
diff --git a/services/core/java/com/android/server/connectivity/DnsManager.java b/services/core/java/com/android/server/connectivity/DnsManager.java
index c0beb37..b8f057d 100644
--- a/services/core/java/com/android/server/connectivity/DnsManager.java
+++ b/services/core/java/com/android/server/connectivity/DnsManager.java
@@ -35,7 +35,6 @@
 import android.net.Network;
 import android.net.NetworkUtils;
 import android.net.Uri;
-import android.net.dns.ResolvUtil;
 import android.os.Binder;
 import android.os.INetworkManagementService;
 import android.os.UserHandle;
@@ -174,15 +173,6 @@
         return new PrivateDnsConfig(useTls);
     }
 
-    public static PrivateDnsConfig tryBlockingResolveOf(Network network, String name) {
-        try {
-            final InetAddress[] ips = ResolvUtil.blockingResolveAllLocally(network, name);
-            return new PrivateDnsConfig(name, ips);
-        } catch (UnknownHostException uhe) {
-            return new PrivateDnsConfig(name, null);
-        }
-    }
-
     public static Uri[] getPrivateDnsSettingsUris() {
         return new Uri[]{
             Settings.Global.getUriFor(PRIVATE_DNS_DEFAULT_MODE),
diff --git a/services/core/java/com/android/server/connectivity/NetworkMonitor.java b/services/core/java/com/android/server/connectivity/NetworkMonitor.java
index 843ba2e..ca9b256 100644
--- a/services/core/java/com/android/server/connectivity/NetworkMonitor.java
+++ b/services/core/java/com/android/server/connectivity/NetworkMonitor.java
@@ -43,7 +43,6 @@
 import android.net.Uri;
 import android.net.captiveportal.CaptivePortalProbeResult;
 import android.net.captiveportal.CaptivePortalProbeSpec;
-import android.net.dns.ResolvUtil;
 import android.net.metrics.IpConnectivityLog;
 import android.net.metrics.NetworkEvent;
 import android.net.metrics.ValidationProbeEvent;
@@ -326,7 +325,7 @@
         mConnectivityServiceHandler = handler;
         mDependencies = deps;
         mNetworkAgentInfo = networkAgentInfo;
-        mNetwork = deps.getNetwork(networkAgentInfo);
+        mNetwork = deps.getNetwork(networkAgentInfo).getPrivateDnsBypassingCopy();
         mNetId = mNetwork.netId;
         mTelephonyManager = (TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE);
         mWifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
@@ -800,9 +799,7 @@
         private void resolveStrictModeHostname() {
             try {
                 // Do a blocking DNS resolution using the network-assigned nameservers.
-                // Do not set AI_ADDRCONFIG in ai_flags so we get all address families in advance.
-                final InetAddress[] ips = ResolvUtil.blockingResolveAllLocally(
-                        mNetwork, mPrivateDnsProviderHostname, 0 /* aiFlags */);
+                final InetAddress[] ips = mNetwork.getAllByName(mPrivateDnsProviderHostname);
                 mPrivateDnsConfig = new PrivateDnsConfig(mPrivateDnsProviderHostname, ips);
                 validationLog("Strict mode hostname resolved: " + mPrivateDnsConfig);
             } catch (UnknownHostException uhe) {
@@ -860,14 +857,13 @@
     // to complete, regardless of how many IP addresses a host has.
     private static class OneAddressPerFamilyNetwork extends Network {
         public OneAddressPerFamilyNetwork(Network network) {
-            super(network);
+            // Always bypass Private DNS.
+            super(network.getPrivateDnsBypassingCopy());
         }
 
         @Override
         public InetAddress[] getAllByName(String host) throws UnknownHostException {
-            // Always bypass Private DNS.
-            final List<InetAddress> addrs = Arrays.asList(
-                    ResolvUtil.blockingResolveAllLocally(this, host));
+            final List<InetAddress> addrs = Arrays.asList(super.getAllByName(host));
 
             // Ensure the address family of the first address is tried first.
             LinkedHashMap<Class, InetAddress> addressByFamily = new LinkedHashMap<>();
diff --git a/services/net/java/android/net/dhcp/DhcpLeaseRepository.java b/services/net/java/android/net/dhcp/DhcpLeaseRepository.java
index 7e57c9f..9f77ed0 100644
--- a/services/net/java/android/net/dhcp/DhcpLeaseRepository.java
+++ b/services/net/java/android/net/dhcp/DhcpLeaseRepository.java
@@ -29,7 +29,7 @@
 import android.net.IpPrefix;
 import android.net.MacAddress;
 import android.net.util.SharedLog;
-import android.os.SystemClock;
+import android.net.dhcp.DhcpServer.Clock;
 import android.util.ArrayMap;
 
 import java.net.Inet4Address;
@@ -73,15 +73,6 @@
     private int mNumAddresses;
     private long mLeaseTimeMs;
 
-    public static class Clock {
-        /**
-         * @see SystemClock#elapsedRealtime()
-         */
-        public long elapsedRealtime() {
-            return SystemClock.elapsedRealtime();
-        }
-    }
-
     /**
      * Next timestamp when committed or declined leases should be checked for expired ones. This
      * will always be lower than or equal to the time for the first lease to expire: it's OK not to
diff --git a/services/net/java/android/net/dhcp/DhcpPacket.java b/services/net/java/android/net/dhcp/DhcpPacket.java
index 888821a..175e27e 100644
--- a/services/net/java/android/net/dhcp/DhcpPacket.java
+++ b/services/net/java/android/net/dhcp/DhcpPacket.java
@@ -9,6 +9,7 @@
 import android.os.SystemProperties;
 import android.system.OsConstants;
 import android.text.TextUtils;
+
 import com.android.internal.annotations.VisibleForTesting;
 
 import java.io.UnsupportedEncodingException;
@@ -351,6 +352,14 @@
     }
 
     /**
+     * Convenience method to return the client ID if it was set explicitly, or null otherwise.
+     */
+    @Nullable
+    public byte[] getExplicitClientIdOrNull() {
+        return hasExplicitClientId() ? getClientId() : null;
+    }
+
+    /**
      * Returns the client ID. If not set explicitly, this follows RFC 2132 and creates a client ID
      * based on the hardware address.
      */
diff --git a/services/net/java/android/net/dhcp/DhcpPacketListener.java b/services/net/java/android/net/dhcp/DhcpPacketListener.java
index 498fd93..6f620c5 100644
--- a/services/net/java/android/net/dhcp/DhcpPacketListener.java
+++ b/services/net/java/android/net/dhcp/DhcpPacketListener.java
@@ -16,15 +16,14 @@
 
 package android.net.dhcp;
 
+import android.annotation.NonNull;
 import android.annotation.Nullable;
 import android.net.util.FdEventsReader;
-import android.net.util.PacketReader;
 import android.os.Handler;
 import android.system.Os;
 
 import java.io.FileDescriptor;
 import java.net.Inet4Address;
-import java.net.InetAddress;
 import java.net.InetSocketAddress;
 
 /**
@@ -35,19 +34,20 @@
     static final class Payload {
         final byte[] bytes = new byte[DhcpPacket.MAX_LENGTH];
         Inet4Address srcAddr;
+        int srcPort;
     }
 
-    public DhcpPacketListener(Handler handler) {
+    public DhcpPacketListener(@NonNull Handler handler) {
         super(handler, new Payload());
     }
 
     @Override
-    protected int recvBufSize(Payload buffer) {
+    protected int recvBufSize(@NonNull Payload buffer) {
         return buffer.bytes.length;
     }
 
     @Override
-    protected final void handlePacket(Payload recvbuf, int length) {
+    protected final void handlePacket(@NonNull Payload recvbuf, int length) {
         if (recvbuf.srcAddr == null) {
             return;
         }
@@ -55,30 +55,34 @@
         try {
             final DhcpPacket packet = DhcpPacket.decodeFullPacket(recvbuf.bytes, length,
                     DhcpPacket.ENCAP_BOOTP);
-            onReceive(packet, recvbuf.srcAddr);
+            onReceive(packet, recvbuf.srcAddr, recvbuf.srcPort);
         } catch (DhcpPacket.ParseException e) {
             logParseError(recvbuf.bytes, length, e);
         }
     }
 
     @Override
-    protected int readPacket(FileDescriptor fd, Payload packetBuffer) throws Exception {
+    protected int readPacket(@NonNull FileDescriptor fd, @NonNull Payload packetBuffer)
+            throws Exception {
         final InetSocketAddress addr = new InetSocketAddress();
         final int read = Os.recvfrom(
                 fd, packetBuffer.bytes, 0, packetBuffer.bytes.length, 0 /* flags */, addr);
 
         // Buffers with null srcAddr will be dropped in handlePacket()
         packetBuffer.srcAddr = inet4AddrOrNull(addr);
+        packetBuffer.srcPort = addr.getPort();
         return read;
     }
 
     @Nullable
-    private static Inet4Address inet4AddrOrNull(InetSocketAddress addr) {
+    private static Inet4Address inet4AddrOrNull(@NonNull InetSocketAddress addr) {
         return addr.getAddress() instanceof Inet4Address
                 ? (Inet4Address) addr.getAddress()
                 : null;
     }
 
-    protected abstract void onReceive(DhcpPacket packet, Inet4Address srcAddr);
-    protected abstract void logParseError(byte[] packet, int length, DhcpPacket.ParseException e);
+    protected abstract void onReceive(@NonNull DhcpPacket packet, @NonNull Inet4Address srcAddr,
+            int srcPort);
+    protected abstract void logParseError(@NonNull byte[] packet, int length,
+            @NonNull DhcpPacket.ParseException e);
 }
diff --git a/services/net/java/android/net/dhcp/DhcpServer.java b/services/net/java/android/net/dhcp/DhcpServer.java
new file mode 100644
index 0000000..095a5eb72
--- /dev/null
+++ b/services/net/java/android/net/dhcp/DhcpServer.java
@@ -0,0 +1,518 @@
+/*
+ * Copyright (C) 2018 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.dhcp;
+
+import static android.net.NetworkUtils.getBroadcastAddress;
+import static android.net.NetworkUtils.getPrefixMaskAsInet4Address;
+import static android.net.TrafficStats.TAG_SYSTEM_DHCP_SERVER;
+import static android.net.dhcp.DhcpPacket.DHCP_CLIENT;
+import static android.net.dhcp.DhcpPacket.DHCP_SERVER;
+import static android.net.dhcp.DhcpPacket.ENCAP_BOOTP;
+import static android.net.dhcp.DhcpPacket.INFINITE_LEASE;
+import static android.system.OsConstants.AF_INET;
+import static android.system.OsConstants.IPPROTO_UDP;
+import static android.system.OsConstants.SOCK_DGRAM;
+import static android.system.OsConstants.SOL_SOCKET;
+import static android.system.OsConstants.SO_BINDTODEVICE;
+import static android.system.OsConstants.SO_BROADCAST;
+import static android.system.OsConstants.SO_REUSEADDR;
+
+import static java.lang.Integer.toUnsignedLong;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.MacAddress;
+import android.net.NetworkUtils;
+import android.net.TrafficStats;
+import android.net.util.InterfaceParams;
+import android.net.util.SharedLog;
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+import android.os.SystemClock;
+import android.system.ErrnoException;
+import android.system.Os;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.HexDump;
+
+import java.io.FileDescriptor;
+import java.io.IOException;
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.nio.ByteBuffer;
+import java.util.ArrayList;
+
+/**
+ * A DHCPv4 server.
+ *
+ * <p>This server listens for and responds to packets on a single interface. It considers itself
+ * authoritative for all leases on the subnet, which means that DHCP requests for unknown leases of
+ * unknown hosts receive a reply instead of being ignored.
+ *
+ * <p>The server is single-threaded (including send/receive operations): all internal operations are
+ * done on the provided {@link Looper}. Public methods are thread-safe and will schedule operations
+ * on the looper asynchronously.
+ * @hide
+ */
+public class DhcpServer {
+    private static final String REPO_TAG = "Repository";
+
+    // Lease time to transmit to client instead of a negative time in case a lease expired before
+    // the server could send it (if the server process is suspended for example).
+    private static final int EXPIRED_FALLBACK_LEASE_TIME_SECS = 120;
+
+    private static final int CMD_START_DHCP_SERVER = 1;
+    private static final int CMD_STOP_DHCP_SERVER = 2;
+    private static final int CMD_UPDATE_PARAMS = 3;
+
+    @NonNull
+    private final ServerHandler mHandler;
+    @NonNull
+    private final InterfaceParams mIface;
+    @NonNull
+    private final DhcpLeaseRepository mLeaseRepo;
+    @NonNull
+    private final SharedLog mLog;
+    @NonNull
+    private final Dependencies mDeps;
+    @NonNull
+    private final Clock mClock;
+    @NonNull
+    private final DhcpPacketListener mPacketListener;
+
+    @Nullable
+    private FileDescriptor mSocket;
+    @NonNull
+    private DhcpServingParams mServingParams;
+
+    public static class Clock {
+        /**
+         * @see SystemClock#elapsedRealtime()
+         */
+        public long elapsedRealtime() {
+            return SystemClock.elapsedRealtime();
+        }
+    }
+
+    public interface Dependencies {
+        void sendPacket(@NonNull FileDescriptor fd, @NonNull ByteBuffer buffer,
+                @NonNull InetAddress dst) throws ErrnoException, IOException;
+        DhcpLeaseRepository makeLeaseRepository(@NonNull DhcpServingParams servingParams,
+                @NonNull SharedLog log, @NonNull Clock clock);
+        DhcpPacketListener makePacketListener();
+        Clock makeClock();
+        void addArpEntry(@NonNull Inet4Address ipv4Addr, @NonNull MacAddress ethAddr,
+                @NonNull String ifname, @NonNull FileDescriptor fd) throws IOException;
+    }
+
+    private class DependenciesImpl implements Dependencies {
+        @Override
+        public void sendPacket(@NonNull FileDescriptor fd, @NonNull ByteBuffer buffer,
+                @NonNull InetAddress dst) throws ErrnoException, IOException {
+            Os.sendto(fd, buffer, 0, dst, DhcpPacket.DHCP_CLIENT);
+        }
+
+        @Override
+        public DhcpLeaseRepository makeLeaseRepository(@NonNull DhcpServingParams servingParams,
+                @NonNull SharedLog log, @NonNull Clock clock) {
+            return new DhcpLeaseRepository(
+                    DhcpServingParams.makeIpPrefix(servingParams.serverAddr),
+                    servingParams.excludedAddrs,
+                    servingParams.dhcpLeaseTimeSecs*1000, log.forSubComponent(REPO_TAG), clock);
+        }
+
+        @Override
+        public DhcpPacketListener makePacketListener() {
+            return new PacketListener();
+        }
+
+        @Override
+        public Clock makeClock() {
+            return new Clock();
+        }
+
+        @Override
+        public void addArpEntry(@NonNull Inet4Address ipv4Addr, @NonNull MacAddress ethAddr,
+                @NonNull String ifname, @NonNull FileDescriptor fd) throws IOException {
+            NetworkUtils.addArpEntry(ipv4Addr, ethAddr, ifname, fd);
+        }
+    }
+
+    private static class MalformedPacketException extends Exception {
+        MalformedPacketException(String message, Throwable t) {
+            super(message, t);
+        }
+    }
+
+    public DhcpServer(@NonNull Looper looper, @NonNull InterfaceParams iface,
+            @NonNull DhcpServingParams params, @NonNull SharedLog log) {
+        this(looper, iface, params, log, null);
+    }
+
+    @VisibleForTesting
+    DhcpServer(@NonNull Looper looper, @NonNull InterfaceParams iface,
+            @NonNull DhcpServingParams params, @NonNull SharedLog log,
+            @Nullable Dependencies deps) {
+        if (deps == null) {
+            deps = new DependenciesImpl();
+        }
+        mHandler = new ServerHandler(looper);
+        mIface = iface;
+        mServingParams = params;
+        mLog = log;
+        mDeps = deps;
+        mClock = deps.makeClock();
+        mPacketListener = deps.makePacketListener();
+        mLeaseRepo = deps.makeLeaseRepository(mServingParams, mLog, mClock);
+    }
+
+    /**
+     * Start listening for and responding to packets.
+     */
+    public void start() {
+        mHandler.sendEmptyMessage(CMD_START_DHCP_SERVER);
+    }
+
+    /**
+     * Update serving parameters. All subsequently received requests will be handled with the new
+     * parameters, and current leases that are incompatible with the new parameters are dropped.
+     */
+    public void updateParams(@NonNull DhcpServingParams params) {
+        sendMessage(CMD_UPDATE_PARAMS, params);
+    }
+
+    /**
+     * Stop listening for packets.
+     *
+     * <p>As the server is stopped asynchronously, some packets may still be processed shortly after
+     * calling this method.
+     */
+    public void stop() {
+        mHandler.sendEmptyMessage(CMD_STOP_DHCP_SERVER);
+    }
+
+    private void sendMessage(int what, @Nullable Object obj) {
+        mHandler.sendMessage(mHandler.obtainMessage(what, obj));
+    }
+
+    private class ServerHandler extends Handler {
+        public ServerHandler(@NonNull Looper looper) {
+            super(looper);
+        }
+
+        @Override
+        public void handleMessage(@NonNull Message msg) {
+            switch (msg.what) {
+                case CMD_UPDATE_PARAMS:
+                    final DhcpServingParams params = (DhcpServingParams) msg.obj;
+                    mServingParams = params;
+                    mLeaseRepo.updateParams(
+                            DhcpServingParams.makeIpPrefix(mServingParams.serverAddr),
+                            params.excludedAddrs,
+                            params.dhcpLeaseTimeSecs);
+                    break;
+                case CMD_START_DHCP_SERVER:
+                    // This is a no-op if the listener is already started
+                    mPacketListener.start();
+                    break;
+                case CMD_STOP_DHCP_SERVER:
+                    // This is a no-op if the listener was not started
+                    mPacketListener.stop();
+                    break;
+            }
+        }
+    }
+
+    @VisibleForTesting
+    void processPacket(@NonNull DhcpPacket packet, int srcPort) {
+        final String packetType = packet.getClass().getSimpleName();
+        if (srcPort != DHCP_CLIENT) {
+            mLog.logf("Ignored packet of type %s sent from client port %d", packetType, srcPort);
+            return;
+        }
+
+        mLog.log("Received packet of type " + packetType);
+        final Inet4Address sid = packet.mServerIdentifier;
+        if (sid != null && !sid.equals(mServingParams.serverAddr.getAddress())) {
+            mLog.log("Packet ignored due to wrong server identifier: " + sid);
+            return;
+        }
+
+        try {
+            if (packet instanceof DhcpDiscoverPacket) {
+                processDiscover((DhcpDiscoverPacket) packet);
+            } else if (packet instanceof DhcpRequestPacket) {
+                processRequest((DhcpRequestPacket) packet);
+            } else if (packet instanceof DhcpReleasePacket) {
+                processRelease((DhcpReleasePacket) packet);
+            } else {
+                mLog.e("Unknown packet type: " + packet.getClass().getSimpleName());
+            }
+        } catch (MalformedPacketException e) {
+            // Not an internal error: only logging exception message, not stacktrace
+            mLog.e("Ignored malformed packet: " + e.getMessage());
+        }
+    }
+
+    private void processDiscover(@NonNull DhcpDiscoverPacket packet)
+            throws MalformedPacketException {
+        final DhcpLease lease;
+        final MacAddress clientMac = getMacAddr(packet);
+        try {
+            lease = mLeaseRepo.getOffer(packet.getExplicitClientIdOrNull(), clientMac,
+                    packet.mRelayIp, packet.mRequestedIp, packet.mHostName);
+        } catch (DhcpLeaseRepository.OutOfAddressesException e) {
+            transmitNak(packet, "Out of addresses to offer");
+            return;
+        } catch (DhcpLeaseRepository.InvalidAddressException e) {
+            transmitNak(packet, "Lease requested from an invalid subnet");
+            return;
+        }
+
+        transmitOffer(packet, lease, clientMac);
+    }
+
+    private void processRequest(@NonNull DhcpRequestPacket packet) throws MalformedPacketException {
+        // If set, packet SID matches with this server's ID as checked in processPacket().
+        final boolean sidSet = packet.mServerIdentifier != null;
+        final DhcpLease lease;
+        final MacAddress clientMac = getMacAddr(packet);
+        try {
+            lease = mLeaseRepo.requestLease(packet.getExplicitClientIdOrNull(), clientMac,
+                    packet.mClientIp, packet.mRequestedIp, sidSet, packet.mHostName);
+        } catch (DhcpLeaseRepository.InvalidAddressException e) {
+            transmitNak(packet, "Invalid requested address");
+            return;
+        }
+
+        transmitAck(packet, lease, clientMac);
+    }
+
+    private void processRelease(@Nullable DhcpReleasePacket packet)
+            throws MalformedPacketException {
+        final byte[] clientId = packet.getExplicitClientIdOrNull();
+        final MacAddress macAddr = getMacAddr(packet);
+        // Don't care about success (there is no ACK/NAK); logging is already done in the repository
+        mLeaseRepo.releaseLease(clientId, macAddr, packet.mClientIp);
+    }
+
+    private Inet4Address getAckOrOfferDst(@NonNull DhcpPacket request, @NonNull DhcpLease lease,
+            boolean broadcastFlag) {
+        // Unless relayed or broadcast, send to client IP if already configured on the client, or to
+        // the lease address if the client has no configured address
+        if (!isEmpty(request.mRelayIp)) {
+            return request.mRelayIp;
+        } else if (broadcastFlag) {
+            return (Inet4Address) Inet4Address.ALL;
+        } else if (!isEmpty(request.mClientIp)) {
+            return request.mClientIp;
+        } else {
+            return lease.getNetAddr();
+        }
+    }
+
+    /**
+     * Determine whether the broadcast flag should be set in the BOOTP packet flags. This does not
+     * apply to NAK responses, which should always have it set.
+     */
+    private static boolean getBroadcastFlag(@NonNull DhcpPacket request, @NonNull DhcpLease lease) {
+        // No broadcast flag if the client already has a configured IP to unicast to. RFC2131 #4.1
+        // has some contradictions regarding broadcast behavior if a client already has an IP
+        // configured and sends a request with both ciaddr (renew/rebind) and the broadcast flag
+        // set. Sending a unicast response to ciaddr matches previous behavior and is more
+        // efficient.
+        // If the client has no configured IP, broadcast if requested by the client or if the lease
+        // address cannot be used to send a unicast reply either.
+        return isEmpty(request.mClientIp) && (request.mBroadcast || isEmpty(lease.getNetAddr()));
+    }
+
+    private boolean transmitOffer(@NonNull DhcpPacket request, @NonNull DhcpLease lease,
+            @NonNull MacAddress clientMac) {
+        final boolean broadcastFlag = getBroadcastFlag(request, lease);
+        final int timeout = getLeaseTimeout(lease);
+        final Inet4Address prefixMask =
+                getPrefixMaskAsInet4Address(mServingParams.serverAddr.getPrefixLength());
+        final Inet4Address broadcastAddr = getBroadcastAddress(
+                mServingParams.getServerInet4Addr(), mServingParams.serverAddr.getPrefixLength());
+        final ByteBuffer offerPacket = DhcpPacket.buildOfferPacket(
+                ENCAP_BOOTP, request.mTransId, broadcastFlag, mServingParams.getServerInet4Addr(),
+                lease.getNetAddr(), request.mClientMac, timeout,
+                prefixMask,
+                broadcastAddr,
+                new ArrayList<>(mServingParams.defaultRouters),
+                new ArrayList<>(mServingParams.dnsServers),
+                mServingParams.getServerInet4Addr(), null /* domainName */);
+
+        return transmitOfferOrAckPacket(offerPacket, request, lease, clientMac, broadcastFlag);
+    }
+
+    private boolean transmitAck(@NonNull DhcpPacket request, @NonNull DhcpLease lease,
+            @NonNull MacAddress clientMac) {
+        // TODO: replace DhcpPacket's build methods with real builders and use common code with
+        // transmitOffer above
+        final boolean broadcastFlag = getBroadcastFlag(request, lease);
+        final int timeout = getLeaseTimeout(lease);
+        final ByteBuffer ackPacket = DhcpPacket.buildAckPacket(ENCAP_BOOTP, request.mTransId,
+                broadcastFlag, mServingParams.getServerInet4Addr(), lease.getNetAddr(),
+                request.mClientMac, timeout, mServingParams.getPrefixMaskAsAddress(),
+                mServingParams.getBroadcastAddress(),
+                new ArrayList<>(mServingParams.defaultRouters),
+                new ArrayList<>(mServingParams.dnsServers),
+                mServingParams.getServerInet4Addr(), null /* domainName */);
+
+        return transmitOfferOrAckPacket(ackPacket, request, lease, clientMac, broadcastFlag);
+    }
+
+    private boolean transmitNak(DhcpPacket request, String message) {
+        mLog.w("Transmitting NAK: " + message);
+        // Always set broadcast flag for NAK: client may not have a correct IP
+        final ByteBuffer nakPacket = DhcpPacket.buildNakPacket(
+                ENCAP_BOOTP, request.mTransId, mServingParams.getServerInet4Addr(),
+                request.mClientMac, true /* broadcast */, message);
+
+        final Inet4Address dst = isEmpty(request.mRelayIp)
+                ? (Inet4Address) Inet4Address.ALL
+                : request.mRelayIp;
+        return transmitPacket(nakPacket, DhcpNakPacket.class.getSimpleName(), dst);
+    }
+
+    private boolean transmitOfferOrAckPacket(@NonNull ByteBuffer buf, @NonNull DhcpPacket request,
+            @NonNull DhcpLease lease, @NonNull MacAddress clientMac, boolean broadcastFlag) {
+        mLog.logf("Transmitting %s with lease %s", request.getClass().getSimpleName(), lease);
+        // Client may not yet respond to ARP for the lease address, which may be the destination
+        // address. Add an entry to the ARP cache to save future ARP probes and make sure the
+        // packet reaches its destination.
+        if (!addArpEntry(clientMac, lease.getNetAddr())) {
+            // Logging for error already done
+            return false;
+        }
+        final Inet4Address dst = getAckOrOfferDst(request, lease, broadcastFlag);
+        return transmitPacket(buf, request.getClass().getSimpleName(), dst);
+    }
+
+    private boolean transmitPacket(@NonNull ByteBuffer buf, @NonNull String packetTypeTag,
+            @NonNull Inet4Address dst) {
+        try {
+            mDeps.sendPacket(mSocket, buf, dst);
+        } catch (ErrnoException | IOException e) {
+            mLog.e("Can't send packet " + packetTypeTag, e);
+            return false;
+        }
+        return true;
+    }
+
+    private boolean addArpEntry(@NonNull MacAddress macAddr, @NonNull Inet4Address inetAddr) {
+        try {
+            mDeps.addArpEntry(inetAddr, macAddr, mIface.name, mSocket);
+            return true;
+        } catch (IOException e) {
+            mLog.e("Error adding client to ARP table", e);
+            return false;
+        }
+    }
+
+    /**
+     * Get the remaining lease time in seconds, starting from {@link Clock#elapsedRealtime()}.
+     *
+     * <p>This is an unsigned 32-bit integer, so it cannot be read as a standard (signed) Java int.
+     * The return value is only intended to be used to populate the lease time field in a DHCP
+     * response, considering that lease time is an unsigned 32-bit integer field in DHCP packets.
+     *
+     * <p>Lease expiration times are tracked internally with millisecond precision: this method
+     * returns a rounded down value.
+     */
+    private int getLeaseTimeout(@NonNull DhcpLease lease) {
+        final long remainingTimeSecs = (lease.getExpTime() - mClock.elapsedRealtime()) / 1000;
+        if (remainingTimeSecs < 0) {
+            mLog.e("Processing expired lease " + lease);
+            return EXPIRED_FALLBACK_LEASE_TIME_SECS;
+        }
+
+        if (remainingTimeSecs >= toUnsignedLong(INFINITE_LEASE)) {
+            return INFINITE_LEASE;
+        }
+
+        return (int) remainingTimeSecs;
+    }
+
+    /**
+     * Get the client MAC address from a packet.
+     *
+     * @throws MalformedPacketException The address in the packet uses an unsupported format.
+     */
+    @NonNull
+    private MacAddress getMacAddr(@NonNull DhcpPacket packet) throws MalformedPacketException {
+        try {
+            return MacAddress.fromBytes(packet.getClientMac());
+        } catch (IllegalArgumentException e) {
+            final String message = "Invalid MAC address in packet: "
+                    + HexDump.dumpHexString(packet.getClientMac());
+            throw new MalformedPacketException(message, e);
+        }
+    }
+
+    private static boolean isEmpty(@NonNull Inet4Address address) {
+        return address == null || Inet4Address.ANY.equals(address);
+    }
+
+    private class PacketListener extends DhcpPacketListener {
+        public PacketListener() {
+            super(mHandler);
+        }
+
+        @Override
+        protected void onReceive(DhcpPacket packet, Inet4Address srcAddr, int srcPort) {
+            processPacket(packet, srcPort);
+        }
+
+        @Override
+        protected void logError(String msg, Exception e) {
+            mLog.e("Error receiving packet: " + msg, e);
+        }
+
+        @Override
+        protected void logParseError(byte[] packet, int length, DhcpPacket.ParseException e) {
+            mLog.e("Error parsing packet", e);
+        }
+
+        @Override
+        protected FileDescriptor createFd() {
+            // TODO: have and use an API to set a socket tag without going through the thread tag
+            final int oldTag = TrafficStats.getAndSetThreadStatsTag(TAG_SYSTEM_DHCP_SERVER);
+            try {
+                mSocket = Os.socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
+                Os.setsockoptInt(mSocket, SOL_SOCKET, SO_REUSEADDR, 1);
+                // SO_BINDTODEVICE actually takes a string. This works because the first member
+                // of struct ifreq is a NULL-terminated interface name.
+                // TODO: add a setsockoptString()
+                Os.setsockoptIfreq(mSocket, SOL_SOCKET, SO_BINDTODEVICE, mIface.name);
+                Os.setsockoptInt(mSocket, SOL_SOCKET, SO_BROADCAST, 1);
+                Os.bind(mSocket, Inet4Address.ANY, DHCP_SERVER);
+                NetworkUtils.protectFromVpn(mSocket);
+
+                return mSocket;
+            } catch (IOException | ErrnoException e) {
+                mLog.e("Error creating UDP socket", e);
+                DhcpServer.this.stop();
+                return null;
+            } finally {
+                TrafficStats.setThreadStatsTag(oldTag);
+            }
+        }
+    }
+}
diff --git a/services/net/java/android/net/ip/IpClient.java b/services/net/java/android/net/ip/IpClient.java
index b77da28..3cdef1e 100644
--- a/services/net/java/android/net/ip/IpClient.java
+++ b/services/net/java/android/net/ip/IpClient.java
@@ -431,6 +431,7 @@
         public ProvisioningConfiguration(ProvisioningConfiguration other) {
             mEnableIPv4 = other.mEnableIPv4;
             mEnableIPv6 = other.mEnableIPv6;
+            mUsingMultinetworkPolicyTracker = other.mUsingMultinetworkPolicyTracker;
             mUsingIpReachabilityMonitor = other.mUsingIpReachabilityMonitor;
             mRequestedPreDhcpActionMs = other.mRequestedPreDhcpActionMs;
             mInitialConfig = InitialConfiguration.copy(other.mInitialConfig);
diff --git a/services/net/java/android/net/util/FdEventsReader.java b/services/net/java/android/net/util/FdEventsReader.java
index 575444f..8bbf449 100644
--- a/services/net/java/android/net/util/FdEventsReader.java
+++ b/services/net/java/android/net/util/FdEventsReader.java
@@ -89,7 +89,7 @@
         mBuffer = buffer;
     }
 
-    public final void start() {
+    public void start() {
         if (onCorrectThread()) {
             createAndRegisterFd();
         } else {
@@ -100,7 +100,7 @@
         }
     }
 
-    public final void stop() {
+    public void stop() {
         if (onCorrectThread()) {
             unregisterAndDestroyFd();
         } else {
diff --git a/telephony/java/android/telephony/CarrierConfigManager.java b/telephony/java/android/telephony/CarrierConfigManager.java
index fdee2f4..d37ef1a 100644
--- a/telephony/java/android/telephony/CarrierConfigManager.java
+++ b/telephony/java/android/telephony/CarrierConfigManager.java
@@ -1970,18 +1970,25 @@
      * the binding intent go to.
      * @hide
      */
-    public static final String KEY_CARRIER_NETWORK_SERVICE_WLAN_PACKAGE_OVERRIDE_STRING
-             = "carrier_network_service_wlan_package_override_string";
+    public static final String KEY_CARRIER_NETWORK_SERVICE_WLAN_PACKAGE_OVERRIDE_STRING =
+            "carrier_network_service_wlan_package_override_string";
 
     /**
      * Decides when clients try to bind to wwan (cellular) network service, which package name will
      * the binding intent go to.
      * @hide
      */
-    public static final String KEY_CARRIER_NETWORK_SERVICE_WWAN_PACKAGE_OVERRIDE_STRING
-            = "carrier_network_service_wwan_package_override_string";
+    public static final String KEY_CARRIER_NETWORK_SERVICE_WWAN_PACKAGE_OVERRIDE_STRING =
+            "carrier_network_service_wwan_package_override_string";
 
     /**
+     * The package name of qualified networks service that telephony binds to.
+     *
+     * @hide
+     */
+    public static final String KEY_CARRIER_QUALIFIED_NETWORKS_SERVICE_PACKAGE_OVERRIDE_STRING =
+            "carrier_qualified_networks_service_package_override_string";
+    /**
      * A list of 4 LTE RSCP thresholds above which a signal level is considered POOR,
      * MODERATE, GOOD, or EXCELLENT, to be used in SignalStrength reporting.
      *
@@ -2061,6 +2068,7 @@
         sDefaults.putBoolean(KEY_CARRIER_USE_IMS_FIRST_FOR_EMERGENCY_BOOL, true);
         sDefaults.putString(KEY_CARRIER_NETWORK_SERVICE_WWAN_PACKAGE_OVERRIDE_STRING, "");
         sDefaults.putString(KEY_CARRIER_NETWORK_SERVICE_WLAN_PACKAGE_OVERRIDE_STRING, "");
+        sDefaults.putString(KEY_CARRIER_QUALIFIED_NETWORKS_SERVICE_PACKAGE_OVERRIDE_STRING, "");
         sDefaults.putString(KEY_CARRIER_DATA_SERVICE_WWAN_PACKAGE_OVERRIDE_STRING, "");
         sDefaults.putString(KEY_CARRIER_DATA_SERVICE_WLAN_PACKAGE_OVERRIDE_STRING, "");
         sDefaults.putString(KEY_CARRIER_INSTANT_LETTERING_INVALID_CHARS_STRING, "");
diff --git a/tests/net/java/android/net/dhcp/DhcpLeaseRepositoryTest.java b/tests/net/java/android/net/dhcp/DhcpLeaseRepositoryTest.java
index edadd6e..590bd67 100644
--- a/tests/net/java/android/net/dhcp/DhcpLeaseRepositoryTest.java
+++ b/tests/net/java/android/net/dhcp/DhcpLeaseRepositoryTest.java
@@ -34,8 +34,8 @@
 import android.annotation.NonNull;
 import android.net.IpPrefix;
 import android.net.MacAddress;
-import android.net.dhcp.DhcpLeaseRepository.Clock;
 import android.net.util.SharedLog;
+import android.net.dhcp.DhcpServer.Clock;
 import android.support.test.filters.SmallTest;
 import android.support.test.runner.AndroidJUnit4;
 
diff --git a/tests/net/java/android/net/dhcp/DhcpServerTest.java b/tests/net/java/android/net/dhcp/DhcpServerTest.java
new file mode 100644
index 0000000..66db5a8
--- /dev/null
+++ b/tests/net/java/android/net/dhcp/DhcpServerTest.java
@@ -0,0 +1,314 @@
+/*
+ * Copyright (C) 2018 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.dhcp;
+
+import static android.net.dhcp.DhcpPacket.DHCP_CLIENT;
+import static android.net.dhcp.DhcpPacket.ENCAP_BOOTP;
+import static android.net.dhcp.DhcpPacket.INADDR_ANY;
+import static android.net.dhcp.DhcpPacket.INADDR_BROADCAST;
+
+import static junit.framework.Assert.assertEquals;
+import static junit.framework.Assert.assertFalse;
+import static junit.framework.Assert.assertNotNull;
+import static junit.framework.Assert.assertNull;
+import static junit.framework.Assert.assertTrue;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyBoolean;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import static java.net.InetAddress.parseNumericAddress;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.net.LinkAddress;
+import android.net.MacAddress;
+import android.net.dhcp.DhcpLeaseRepository.InvalidAddressException;
+import android.net.dhcp.DhcpLeaseRepository.OutOfAddressesException;
+import android.net.dhcp.DhcpServer.Clock;
+import android.net.dhcp.DhcpServer.Dependencies;
+import android.net.util.InterfaceParams;
+import android.net.util.SharedLog;
+import android.os.test.TestLooper;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Captor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.net.Inet4Address;
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class DhcpServerTest {
+    private static final String PROP_DEXMAKER_SHARE_CLASSLOADER = "dexmaker.share_classloader";
+    private static final String TEST_IFACE = "testiface";
+    private static final MacAddress TEST_IFACE_MAC = MacAddress.fromString("11:22:33:44:55:66");
+    private static final InterfaceParams TEST_IFACEPARAMS =
+            new InterfaceParams(TEST_IFACE, 1, TEST_IFACE_MAC);
+
+    private static final Inet4Address TEST_SERVER_ADDR = parseAddr("192.168.0.2");
+    private static final LinkAddress TEST_SERVER_LINKADDR = new LinkAddress(TEST_SERVER_ADDR, 20);
+    private static final Set<Inet4Address> TEST_DEFAULT_ROUTERS = new HashSet<>(
+            Arrays.asList(parseAddr("192.168.0.123"), parseAddr("192.168.0.124")));
+    private static final Set<Inet4Address> TEST_DNS_SERVERS = new HashSet<>(
+            Arrays.asList(parseAddr("192.168.0.126"), parseAddr("192.168.0.127")));
+    private static final Set<Inet4Address> TEST_EXCLUDED_ADDRS = new HashSet<>(
+            Arrays.asList(parseAddr("192.168.0.200"), parseAddr("192.168.0.201")));
+    private static final long TEST_LEASE_TIME_SECS = 3600L;
+    private static final int TEST_MTU = 1500;
+
+    private static final int TEST_TRANSACTION_ID = 123;
+    private static final byte[] TEST_CLIENT_MAC_BYTES = new byte [] { 1, 2, 3, 4, 5, 6 };
+    private static final MacAddress TEST_CLIENT_MAC = MacAddress.fromBytes(TEST_CLIENT_MAC_BYTES);
+    private static final Inet4Address TEST_CLIENT_ADDR = parseAddr("192.168.0.42");
+
+    private static final long TEST_CLOCK_TIME = 1234L;
+    private static final int TEST_LEASE_EXPTIME_SECS = 3600;
+    private static final DhcpLease TEST_LEASE = new DhcpLease(null, TEST_CLIENT_MAC,
+            TEST_CLIENT_ADDR, TEST_LEASE_EXPTIME_SECS*1000L + TEST_CLOCK_TIME, null /* hostname */);
+
+    @NonNull @Mock
+    private Dependencies mDeps;
+    @NonNull @Mock
+    private DhcpLeaseRepository mRepository;
+    @NonNull @Mock
+    private Clock mClock;
+    @NonNull @Mock
+    private DhcpPacketListener mPacketListener;
+
+    @NonNull @Captor
+    private ArgumentCaptor<ByteBuffer> mSentPacketCaptor;
+    @NonNull @Captor
+    private ArgumentCaptor<Inet4Address> mResponseDstAddrCaptor;
+
+    @NonNull
+    private TestLooper mLooper;
+    @NonNull
+    private DhcpServer mServer;
+
+    @Nullable
+    private String mPrevShareClassloaderProp;
+
+    @Before
+    public void setUp() throws Exception {
+        // Allow mocking package-private classes
+        mPrevShareClassloaderProp = System.getProperty(PROP_DEXMAKER_SHARE_CLASSLOADER);
+        System.setProperty(PROP_DEXMAKER_SHARE_CLASSLOADER, "true");
+        MockitoAnnotations.initMocks(this);
+
+        when(mDeps.makeLeaseRepository(any(), any(), any())).thenReturn(mRepository);
+        when(mDeps.makeClock()).thenReturn(mClock);
+        when(mDeps.makePacketListener()).thenReturn(mPacketListener);
+        doNothing().when(mDeps)
+                .sendPacket(any(), mSentPacketCaptor.capture(), mResponseDstAddrCaptor.capture());
+        when(mClock.elapsedRealtime()).thenReturn(TEST_CLOCK_TIME);
+
+        final DhcpServingParams servingParams = new DhcpServingParams.Builder()
+                .setDefaultRouters(TEST_DEFAULT_ROUTERS)
+                .setDhcpLeaseTimeSecs(TEST_LEASE_TIME_SECS)
+                .setDnsServers(TEST_DNS_SERVERS)
+                .setServerAddr(TEST_SERVER_LINKADDR)
+                .setLinkMtu(TEST_MTU)
+                .setExcludedAddrs(TEST_EXCLUDED_ADDRS)
+                .build();
+
+        mLooper = new TestLooper();
+        mServer = new DhcpServer(mLooper.getLooper(), TEST_IFACEPARAMS, servingParams,
+                new SharedLog(DhcpServerTest.class.getSimpleName()), mDeps);
+
+        mServer.start();
+        mLooper.dispatchAll();
+    }
+
+    @After
+    public void tearDown() {
+        // Calling stop() several times is not an issue
+        mServer.stop();
+        System.setProperty(PROP_DEXMAKER_SHARE_CLASSLOADER,
+                (mPrevShareClassloaderProp == null ? "" : mPrevShareClassloaderProp));
+    }
+
+    @Test
+    public void testStart() throws Exception {
+        verify(mPacketListener, times(1)).start();
+    }
+
+    @Test
+    public void testStop() throws Exception {
+        mServer.stop();
+        mLooper.dispatchAll();
+        verify(mPacketListener, times(1)).stop();
+    }
+
+    @Test
+    public void testDiscover() throws Exception {
+        // TODO: refactor packet construction to eliminate unnecessary/confusing/duplicate fields
+        when(mRepository.getOffer(isNull() /* clientId */, eq(TEST_CLIENT_MAC),
+                eq(INADDR_ANY) /* relayAddr */, isNull() /* reqAddr */, isNull() /* hostname */))
+                .thenReturn(TEST_LEASE);
+
+        final DhcpDiscoverPacket discover = new DhcpDiscoverPacket(TEST_TRANSACTION_ID,
+                (short) 0 /* secs */, INADDR_ANY /* relayIp */, TEST_CLIENT_MAC_BYTES,
+                false /* broadcast */, INADDR_ANY /* srcIp */);
+        mServer.processPacket(discover, DHCP_CLIENT);
+
+        assertResponseSentTo(TEST_CLIENT_ADDR);
+        final DhcpOfferPacket packet = assertOffer(getPacket());
+        assertMatchesTestLease(packet);
+    }
+
+    @Test
+    public void testDiscover_OutOfAddresses() throws Exception {
+        when(mRepository.getOffer(isNull() /* clientId */, eq(TEST_CLIENT_MAC),
+                eq(INADDR_ANY) /* relayAddr */, isNull() /* reqAddr */, isNull() /* hostname */))
+                .thenThrow(new OutOfAddressesException("Test exception"));
+
+        final DhcpDiscoverPacket discover = new DhcpDiscoverPacket(TEST_TRANSACTION_ID,
+                (short) 0 /* secs */, INADDR_ANY /* relayIp */, TEST_CLIENT_MAC_BYTES,
+                false /* broadcast */, INADDR_ANY /* srcIp */);
+        mServer.processPacket(discover, DHCP_CLIENT);
+
+        assertResponseSentTo(INADDR_BROADCAST);
+        final DhcpNakPacket packet = assertNak(getPacket());
+        assertMatchesClient(packet);
+    }
+
+    private DhcpRequestPacket makeRequestSelectingPacket() {
+        final DhcpRequestPacket request = new DhcpRequestPacket(TEST_TRANSACTION_ID,
+                (short) 0 /* secs */, INADDR_ANY /* clientIp */, INADDR_ANY /* relayIp */,
+                TEST_CLIENT_MAC_BYTES, false /* broadcast */);
+        request.mServerIdentifier = TEST_SERVER_ADDR;
+        request.mRequestedIp = TEST_CLIENT_ADDR;
+        return request;
+    }
+
+    @Test
+    public void testRequest_Selecting_Ack() throws Exception {
+        when(mRepository.requestLease(isNull() /* clientId */, eq(TEST_CLIENT_MAC),
+                eq(INADDR_ANY) /* clientAddr */, eq(TEST_CLIENT_ADDR) /* reqAddr */,
+                eq(true) /* sidSet */, isNull() /* hostname */))
+                .thenReturn(TEST_LEASE);
+
+        final DhcpRequestPacket request = makeRequestSelectingPacket();
+        mServer.processPacket(request, DHCP_CLIENT);
+
+        assertResponseSentTo(TEST_CLIENT_ADDR);
+        final DhcpAckPacket packet = assertAck(getPacket());
+        assertMatchesTestLease(packet);
+    }
+
+    @Test
+    public void testRequest_Selecting_Nak() throws Exception {
+        when(mRepository.requestLease(isNull(), eq(TEST_CLIENT_MAC),
+                eq(INADDR_ANY) /* clientAddr */, eq(TEST_CLIENT_ADDR) /* reqAddr */,
+                eq(true) /* sidSet */, isNull() /* hostname */))
+                .thenThrow(new InvalidAddressException("Test error"));
+
+        final DhcpRequestPacket request = makeRequestSelectingPacket();
+        mServer.processPacket(request, DHCP_CLIENT);
+
+        assertResponseSentTo(INADDR_BROADCAST);
+        final DhcpNakPacket packet = assertNak(getPacket());
+        assertMatchesClient(packet);
+    }
+
+    @Test
+    public void testRequest_Selecting_WrongClientPort() throws Exception {
+        final DhcpRequestPacket request = makeRequestSelectingPacket();
+        mServer.processPacket(request, 50000);
+
+        verify(mRepository, never()).requestLease(any(), any(), any(), any(), anyBoolean(), any());
+        verify(mDeps, never()).sendPacket(any(), any(), any());
+    }
+
+    @Test
+    public void testRelease() throws Exception {
+        final DhcpReleasePacket release = new DhcpReleasePacket(TEST_TRANSACTION_ID,
+                TEST_SERVER_ADDR, TEST_CLIENT_ADDR,
+                INADDR_ANY /* relayIp */, TEST_CLIENT_MAC_BYTES);
+        mServer.processPacket(release, DHCP_CLIENT);
+
+        verify(mRepository, times(1))
+                .releaseLease(isNull(), eq(TEST_CLIENT_MAC), eq(TEST_CLIENT_ADDR));
+    }
+
+    /* TODO: add more tests once packet construction is refactored, including:
+     *  - usage of giaddr
+     *  - usage of broadcast bit
+     *  - other request states (init-reboot/renewing/rebinding)
+     */
+
+    private void assertMatchesTestLease(@NonNull DhcpPacket packet) {
+        assertMatchesClient(packet);
+        assertFalse(packet.hasExplicitClientId());
+        assertEquals(TEST_SERVER_ADDR, packet.mServerIdentifier);
+        assertEquals(TEST_CLIENT_ADDR, packet.mYourIp);
+        assertNotNull(packet.mLeaseTime);
+        assertEquals(TEST_LEASE_EXPTIME_SECS, (int) packet.mLeaseTime);
+        assertNull(packet.mHostName);
+    }
+
+    private void assertMatchesClient(@NonNull DhcpPacket packet) {
+        assertEquals(TEST_TRANSACTION_ID, packet.mTransId);
+        assertEquals(TEST_CLIENT_MAC, MacAddress.fromBytes(packet.mClientMac));
+    }
+
+    private void assertResponseSentTo(@NonNull Inet4Address addr) {
+        assertEquals(addr, mResponseDstAddrCaptor.getValue());
+    }
+
+    private static DhcpNakPacket assertNak(@Nullable DhcpPacket packet) {
+        assertTrue(packet instanceof DhcpNakPacket);
+        return (DhcpNakPacket) packet;
+    }
+
+    private static DhcpAckPacket assertAck(@Nullable DhcpPacket packet) {
+        assertTrue(packet instanceof DhcpAckPacket);
+        return (DhcpAckPacket) packet;
+    }
+
+    private static DhcpOfferPacket assertOffer(@Nullable DhcpPacket packet) {
+        assertTrue(packet instanceof DhcpOfferPacket);
+        return (DhcpOfferPacket) packet;
+    }
+
+    private DhcpPacket getPacket() throws Exception {
+        verify(mDeps, times(1)).sendPacket(any(), any(), any());
+        return DhcpPacket.decodeFullPacket(mSentPacketCaptor.getValue(), ENCAP_BOOTP);
+    }
+
+    private static Inet4Address parseAddr(@Nullable String inet4Addr) {
+        return (Inet4Address) parseNumericAddress(inet4Addr);
+    }
+}
diff --git a/tests/net/java/com/android/server/connectivity/NetworkMonitorTest.java b/tests/net/java/com/android/server/connectivity/NetworkMonitorTest.java
index b017130..b399b0d 100644
--- a/tests/net/java/com/android/server/connectivity/NetworkMonitorTest.java
+++ b/tests/net/java/com/android/server/connectivity/NetworkMonitorTest.java
@@ -106,6 +106,7 @@
                 anyString())).thenReturn(TEST_HTTP_URL);
         when(mDependencies.getSetting(any(), eq(Settings.Global.CAPTIVE_PORTAL_HTTPS_URL),
                 anyString())).thenReturn(TEST_HTTPS_URL);
+        when(mNetwork.getPrivateDnsBypassingCopy()).thenReturn(mNetwork);
 
         when(mContext.getSystemService(Context.TELEPHONY_SERVICE)).thenReturn(mTelephony);
         when(mContext.getSystemService(Context.WIFI_SERVICE)).thenReturn(mWifi);