Add stable AIDL parcelables for IIpClient API

Test: atest FrameworksNetTests NetworkStackTests
Bug: b/112869080
Change-Id: Ia4ac1eb482850a06c4dc1b6c1fe55d33d4087388
diff --git a/Android.bp b/Android.bp
index 580df85..81a9850 100644
--- a/Android.bp
+++ b/Android.bp
@@ -824,17 +824,23 @@
     name: "networkstack-aidl-interfaces",
     local_include_dir: "core/java",
     srcs: [
+        "core/java/android/net/ApfCapabilitiesParcelable.aidl",
+        "core/java/android/net/DhcpResultsParcelable.aidl",
         "core/java/android/net/INetworkMonitor.aidl",
         "core/java/android/net/INetworkMonitorCallbacks.aidl",
         "core/java/android/net/IIpMemoryStore.aidl",
         "core/java/android/net/INetworkStackConnector.aidl",
         "core/java/android/net/INetworkStackStatusCallback.aidl",
+        "core/java/android/net/InitialConfigurationParcelable.aidl",
         "core/java/android/net/IpPrefixParcelable.aidl",
         "core/java/android/net/LinkAddressParcelable.aidl",
         "core/java/android/net/LinkPropertiesParcelable.aidl",
+        "core/java/android/net/NetworkParcelable.aidl",
         "core/java/android/net/PrivateDnsConfigParcel.aidl",
+        "core/java/android/net/ProvisioningConfigurationParcelable.aidl",
         "core/java/android/net/ProxyInfoParcelable.aidl",
         "core/java/android/net/RouteInfoParcelable.aidl",
+        "core/java/android/net/StaticIpConfigurationParcelable.aidl",
         "core/java/android/net/dhcp/DhcpServingParamsParcel.aidl",
         "core/java/android/net/dhcp/IDhcpServer.aidl",
         "core/java/android/net/dhcp/IDhcpServerCallbacks.aidl",
diff --git a/core/java/android/net/ApfCapabilitiesParcelable.aidl b/core/java/android/net/ApfCapabilitiesParcelable.aidl
new file mode 100644
index 0000000..f0645d2
--- /dev/null
+++ b/core/java/android/net/ApfCapabilitiesParcelable.aidl
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2019 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;
+
+parcelable ApfCapabilitiesParcelable {
+    int apfVersionSupported;
+    int maximumApfProgramSize;
+    int apfPacketFormat;
+}
\ No newline at end of file
diff --git a/core/java/android/net/DhcpResultsParcelable.aidl b/core/java/android/net/DhcpResultsParcelable.aidl
new file mode 100644
index 0000000..cf5629b
--- /dev/null
+++ b/core/java/android/net/DhcpResultsParcelable.aidl
@@ -0,0 +1,27 @@
+/**
+ * Copyright (c) 2019, 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 perNmissions and
+ * limitations under the License.
+ */
+
+package android.net;
+
+import android.net.StaticIpConfigurationParcelable;
+
+parcelable DhcpResultsParcelable {
+    StaticIpConfigurationParcelable baseConfiguration;
+    int leaseDuration;
+    int mtu;
+    String serverAddress;
+    String vendorInfo;
+}
\ No newline at end of file
diff --git a/core/java/android/net/InitialConfigurationParcelable.aidl b/core/java/android/net/InitialConfigurationParcelable.aidl
new file mode 100644
index 0000000..bdda3559
--- /dev/null
+++ b/core/java/android/net/InitialConfigurationParcelable.aidl
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2019 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;
+
+import android.net.IpPrefixParcelable;
+import android.net.LinkAddressParcelable;
+
+parcelable InitialConfigurationParcelable {
+    LinkAddressParcelable[] ipAddresses;
+    IpPrefixParcelable[] directlyConnectedRoutes;
+    String[] dnsServers;
+    String gateway;
+}
\ No newline at end of file
diff --git a/core/java/android/net/NetworkParcelable.aidl b/core/java/android/net/NetworkParcelable.aidl
new file mode 100644
index 0000000..c26352e
--- /dev/null
+++ b/core/java/android/net/NetworkParcelable.aidl
@@ -0,0 +1,22 @@
+/*
+**
+** Copyright (C) 2019 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;
+
+parcelable NetworkParcelable {
+    long networkHandle;
+}
diff --git a/core/java/android/net/ProvisioningConfigurationParcelable.aidl b/core/java/android/net/ProvisioningConfigurationParcelable.aidl
new file mode 100644
index 0000000..2a144f2
--- /dev/null
+++ b/core/java/android/net/ProvisioningConfigurationParcelable.aidl
@@ -0,0 +1,38 @@
+/*
+**
+** Copyright (C) 2019 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;
+
+import android.net.ApfCapabilitiesParcelable;
+import android.net.InitialConfigurationParcelable;
+import android.net.NetworkParcelable;
+import android.net.StaticIpConfigurationParcelable;
+
+parcelable ProvisioningConfigurationParcelable {
+    boolean enableIPv4;
+    boolean enableIPv6;
+    boolean usingMultinetworkPolicyTracker;
+    boolean usingIpReachabilityMonitor;
+    int requestedPreDhcpActionMs;
+    InitialConfigurationParcelable initialConfig;
+    StaticIpConfigurationParcelable staticIpConfig;
+    ApfCapabilitiesParcelable apfCapabilities;
+    int provisioningTimeoutMs;
+    int ipv6AddrGenMode;
+    NetworkParcelable network;
+    String displayName;
+}
diff --git a/core/java/android/net/StaticIpConfigurationParcelable.aidl b/core/java/android/net/StaticIpConfigurationParcelable.aidl
new file mode 100644
index 0000000..45dc021
--- /dev/null
+++ b/core/java/android/net/StaticIpConfigurationParcelable.aidl
@@ -0,0 +1,27 @@
+/*
+**
+** Copyright (C) 2019 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;
+
+import android.net.LinkAddressParcelable;
+
+parcelable StaticIpConfigurationParcelable {
+    LinkAddressParcelable ipAddress;
+    String gateway;
+    String[] dnsServers;
+    String domains;
+}
diff --git a/services/net/java/android/net/apf/ApfCapabilities.java b/core/java/android/net/apf/ApfCapabilities.java
similarity index 78%
rename from services/net/java/android/net/apf/ApfCapabilities.java
rename to core/java/android/net/apf/ApfCapabilities.java
index dec8ca2..f28cdc9 100644
--- a/services/net/java/android/net/apf/ApfCapabilities.java
+++ b/core/java/android/net/apf/ApfCapabilities.java
@@ -38,18 +38,28 @@
      */
     public final int apfPacketFormat;
 
-    public ApfCapabilities(int apfVersionSupported, int maximumApfProgramSize, int apfPacketFormat)
-    {
+    public ApfCapabilities(
+            int apfVersionSupported, int maximumApfProgramSize, int apfPacketFormat) {
         this.apfVersionSupported = apfVersionSupported;
         this.maximumApfProgramSize = maximumApfProgramSize;
         this.apfPacketFormat = apfPacketFormat;
     }
 
+    @Override
     public String toString() {
         return String.format("%s{version: %d, maxSize: %d, format: %d}", getClass().getSimpleName(),
                 apfVersionSupported, maximumApfProgramSize, apfPacketFormat);
     }
 
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof  ApfCapabilities)) return false;
+        final ApfCapabilities other = (ApfCapabilities) obj;
+        return apfVersionSupported == other.apfVersionSupported
+                && maximumApfProgramSize == other.maximumApfProgramSize
+                && apfPacketFormat == other.apfPacketFormat;
+    }
+
     /**
      * Returns true if the APF interpreter advertises support for the data buffer access opcodes
      * LDDW and STDW.
diff --git a/services/net/java/android/net/shared/InitialConfiguration.java b/services/net/java/android/net/shared/InitialConfiguration.java
new file mode 100644
index 0000000..bc2373f
--- /dev/null
+++ b/services/net/java/android/net/shared/InitialConfiguration.java
@@ -0,0 +1,234 @@
+/*
+ * Copyright (C) 2019 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.shared;
+
+import static android.net.shared.ParcelableUtil.fromParcelableArray;
+import static android.net.shared.ParcelableUtil.toParcelableArray;
+import static android.text.TextUtils.join;
+
+import android.net.InitialConfigurationParcelable;
+import android.net.IpPrefix;
+import android.net.IpPrefixParcelable;
+import android.net.LinkAddress;
+import android.net.LinkAddressParcelable;
+import android.net.RouteInfo;
+
+import java.net.Inet6Address;
+import java.net.InetAddress;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Predicate;
+
+/** @hide */
+public class InitialConfiguration {
+    public final Set<LinkAddress> ipAddresses = new HashSet<>();
+    public final Set<IpPrefix> directlyConnectedRoutes = new HashSet<>();
+    public final Set<InetAddress> dnsServers = new HashSet<>();
+
+    private static final int RFC6177_MIN_PREFIX_LENGTH = 48;
+    private static final int RFC7421_PREFIX_LENGTH = 64;
+
+    /**
+     * Create a InitialConfiguration that is a copy of the specified configuration.
+     */
+    public static InitialConfiguration copy(InitialConfiguration config) {
+        if (config == null) {
+            return null;
+        }
+        InitialConfiguration configCopy = new InitialConfiguration();
+        configCopy.ipAddresses.addAll(config.ipAddresses);
+        configCopy.directlyConnectedRoutes.addAll(config.directlyConnectedRoutes);
+        configCopy.dnsServers.addAll(config.dnsServers);
+        return configCopy;
+    }
+
+    @Override
+    public String toString() {
+        return String.format(
+                "InitialConfiguration(IPs: {%s}, prefixes: {%s}, DNS: {%s})",
+                join(", ", ipAddresses), join(", ", directlyConnectedRoutes),
+                join(", ", dnsServers));
+    }
+
+    /**
+     * Tests whether the contents of this IpConfiguration represent a valid configuration.
+     */
+    public boolean isValid() {
+        if (ipAddresses.isEmpty()) {
+            return false;
+        }
+
+        // For every IP address, there must be at least one prefix containing that address.
+        for (LinkAddress addr : ipAddresses) {
+            if (!any(directlyConnectedRoutes, (p) -> p.contains(addr.getAddress()))) {
+                return false;
+            }
+        }
+        // For every dns server, there must be at least one prefix containing that address.
+        for (InetAddress addr : dnsServers) {
+            if (!any(directlyConnectedRoutes, (p) -> p.contains(addr))) {
+                return false;
+            }
+        }
+        // All IPv6 LinkAddresses have an RFC7421-suitable prefix length
+        // (read: compliant with RFC4291#section2.5.4).
+        if (any(ipAddresses, not(InitialConfiguration::isPrefixLengthCompliant))) {
+            return false;
+        }
+        // If directlyConnectedRoutes contains an IPv6 default route
+        // then ipAddresses MUST contain at least one non-ULA GUA.
+        if (any(directlyConnectedRoutes, InitialConfiguration::isIPv6DefaultRoute)
+                && all(ipAddresses, not(InitialConfiguration::isIPv6GUA))) {
+            return false;
+        }
+        // The prefix length of routes in directlyConnectedRoutes be within reasonable
+        // bounds for IPv6: /48-/64 just as we’d accept in RIOs.
+        if (any(directlyConnectedRoutes, not(InitialConfiguration::isPrefixLengthCompliant))) {
+            return false;
+        }
+        // There no more than one IPv4 address
+        if (ipAddresses.stream().filter(LinkAddress::isIPv4).count() > 1) {
+            return false;
+        }
+
+        return true;
+    }
+
+    /**
+     * @return true if the given list of addressess and routes satisfies provisioning for this
+     * InitialConfiguration. LinkAddresses and RouteInfo objects are not compared with equality
+     * because addresses and routes seen by Netlink will contain additional fields like flags,
+     * interfaces, and so on. If this InitialConfiguration has no IP address specified, the
+     * provisioning check always fails.
+     *
+     * If the given list of routes is null, only addresses are taken into considerations.
+     */
+    public boolean isProvisionedBy(List<LinkAddress> addresses, List<RouteInfo> routes) {
+        if (ipAddresses.isEmpty()) {
+            return false;
+        }
+
+        for (LinkAddress addr : ipAddresses) {
+            if (!any(addresses, (addrSeen) -> addr.isSameAddressAs(addrSeen))) {
+                return false;
+            }
+        }
+
+        if (routes != null) {
+            for (IpPrefix prefix : directlyConnectedRoutes) {
+                if (!any(routes, (routeSeen) -> isDirectlyConnectedRoute(routeSeen, prefix))) {
+                    return false;
+                }
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Convert this configuration to a {@link InitialConfigurationParcelable}.
+     */
+    public InitialConfigurationParcelable toStableParcelable() {
+        final InitialConfigurationParcelable p = new InitialConfigurationParcelable();
+        p.ipAddresses = toParcelableArray(ipAddresses,
+                LinkPropertiesParcelableUtil::toStableParcelable, LinkAddressParcelable.class);
+        p.directlyConnectedRoutes = toParcelableArray(directlyConnectedRoutes,
+                LinkPropertiesParcelableUtil::toStableParcelable, IpPrefixParcelable.class);
+        p.dnsServers = toParcelableArray(
+                dnsServers, IpConfigurationParcelableUtil::parcelAddress, String.class);
+        return p;
+    }
+
+    /**
+     * Create an instance of {@link InitialConfiguration} based on the contents of the specified
+     * {@link InitialConfigurationParcelable}.
+     */
+    public static InitialConfiguration fromStableParcelable(InitialConfigurationParcelable p) {
+        if (p == null) return null;
+        final InitialConfiguration config = new InitialConfiguration();
+        config.ipAddresses.addAll(fromParcelableArray(
+                p.ipAddresses, LinkPropertiesParcelableUtil::fromStableParcelable));
+        config.directlyConnectedRoutes.addAll(fromParcelableArray(
+                p.directlyConnectedRoutes, LinkPropertiesParcelableUtil::fromStableParcelable));
+        config.dnsServers.addAll(
+                fromParcelableArray(p.dnsServers, IpConfigurationParcelableUtil::unparcelAddress));
+        return config;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof InitialConfiguration)) return false;
+        final InitialConfiguration other = (InitialConfiguration) obj;
+        return ipAddresses.equals(other.ipAddresses)
+                && directlyConnectedRoutes.equals(other.directlyConnectedRoutes)
+                && dnsServers.equals(other.dnsServers);
+    }
+
+    private static boolean isDirectlyConnectedRoute(RouteInfo route, IpPrefix prefix) {
+        return !route.hasGateway() && prefix.equals(route.getDestination());
+    }
+
+    private static boolean isPrefixLengthCompliant(LinkAddress addr) {
+        return addr.isIPv4() || isCompliantIPv6PrefixLength(addr.getPrefixLength());
+    }
+
+    private static boolean isPrefixLengthCompliant(IpPrefix prefix) {
+        return prefix.isIPv4() || isCompliantIPv6PrefixLength(prefix.getPrefixLength());
+    }
+
+    private static boolean isCompliantIPv6PrefixLength(int prefixLength) {
+        return (RFC6177_MIN_PREFIX_LENGTH <= prefixLength)
+                && (prefixLength <= RFC7421_PREFIX_LENGTH);
+    }
+
+    private static boolean isIPv6DefaultRoute(IpPrefix prefix) {
+        return prefix.getAddress().equals(Inet6Address.ANY);
+    }
+
+    private static boolean isIPv6GUA(LinkAddress addr) {
+        return addr.isIPv6() && addr.isGlobalPreferred();
+    }
+
+    // TODO: extract out into CollectionUtils.
+
+    /**
+     * Indicate whether any element of the specified iterable verifies the specified predicate.
+     */
+    public static <T> boolean any(Iterable<T> coll, Predicate<T> fn) {
+        for (T t : coll) {
+            if (fn.test(t)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Indicate whether all elements of the specified iterable verifies the specified predicate.
+     */
+    public static <T> boolean all(Iterable<T> coll, Predicate<T> fn) {
+        return !any(coll, not(fn));
+    }
+
+    /**
+     * Create a predicate that returns the opposite value of the specified predicate.
+     */
+    public static <T> Predicate<T> not(Predicate<T> fn) {
+        return (t) -> !fn.test(t);
+    }
+}
diff --git a/services/net/java/android/net/shared/IpConfigurationParcelableUtil.java b/services/net/java/android/net/shared/IpConfigurationParcelableUtil.java
new file mode 100644
index 0000000..2c368c8
--- /dev/null
+++ b/services/net/java/android/net/shared/IpConfigurationParcelableUtil.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2019 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.shared;
+
+import static android.net.shared.ParcelableUtil.fromParcelableArray;
+import static android.net.shared.ParcelableUtil.toParcelableArray;
+
+import android.annotation.Nullable;
+import android.net.ApfCapabilitiesParcelable;
+import android.net.DhcpResults;
+import android.net.DhcpResultsParcelable;
+import android.net.InetAddresses;
+import android.net.StaticIpConfiguration;
+import android.net.StaticIpConfigurationParcelable;
+import android.net.apf.ApfCapabilities;
+
+import java.net.Inet4Address;
+import java.net.InetAddress;
+
+/**
+ * Collection of utility methods to convert to and from stable AIDL parcelables for IpClient
+ * configuration classes.
+ * @hide
+ */
+public final class IpConfigurationParcelableUtil {
+    /**
+     * Convert a StaticIpConfiguration to a StaticIpConfigurationParcelable.
+     */
+    public static StaticIpConfigurationParcelable toStableParcelable(
+            @Nullable StaticIpConfiguration config) {
+        if (config == null) return null;
+        final StaticIpConfigurationParcelable p = new StaticIpConfigurationParcelable();
+        p.ipAddress = LinkPropertiesParcelableUtil.toStableParcelable(config.ipAddress);
+        p.gateway = parcelAddress(config.gateway);
+        p.dnsServers = toParcelableArray(
+                config.dnsServers, IpConfigurationParcelableUtil::parcelAddress, String.class);
+        p.domains = config.domains;
+        return p;
+    }
+
+    /**
+     * Convert a StaticIpConfigurationParcelable to a StaticIpConfiguration.
+     */
+    public static StaticIpConfiguration fromStableParcelable(
+            @Nullable StaticIpConfigurationParcelable p) {
+        if (p == null) return null;
+        final StaticIpConfiguration config = new StaticIpConfiguration();
+        config.ipAddress = LinkPropertiesParcelableUtil.fromStableParcelable(p.ipAddress);
+        config.gateway = unparcelAddress(p.gateway);
+        config.dnsServers.addAll(fromParcelableArray(
+                p.dnsServers, IpConfigurationParcelableUtil::unparcelAddress));
+        config.domains = p.domains;
+        return config;
+    }
+
+    /**
+     * Convert DhcpResults to a DhcpResultsParcelable.
+     */
+    public static DhcpResultsParcelable toStableParcelable(@Nullable DhcpResults results) {
+        if (results == null) return null;
+        final DhcpResultsParcelable p = new DhcpResultsParcelable();
+        p.baseConfiguration = toStableParcelable((StaticIpConfiguration) results);
+        p.leaseDuration = results.leaseDuration;
+        p.mtu = results.mtu;
+        p.serverAddress = parcelAddress(results.serverAddress);
+        p.vendorInfo = results.vendorInfo;
+        return p;
+    }
+
+    /**
+     * Convert a DhcpResultsParcelable to DhcpResults.
+     */
+    public static DhcpResults fromStableParcelable(@Nullable DhcpResultsParcelable p) {
+        if (p == null) return null;
+        final DhcpResults results = new DhcpResults(fromStableParcelable(p.baseConfiguration));
+        results.leaseDuration = p.leaseDuration;
+        results.mtu = p.mtu;
+        results.serverAddress = (Inet4Address) unparcelAddress(p.serverAddress);
+        results.vendorInfo = p.vendorInfo;
+        return results;
+    }
+
+    /**
+     * Convert ApfCapabilities to ApfCapabilitiesParcelable.
+     */
+    public static ApfCapabilitiesParcelable toStableParcelable(@Nullable ApfCapabilities caps) {
+        if (caps == null) return null;
+        final ApfCapabilitiesParcelable p = new ApfCapabilitiesParcelable();
+        p.apfVersionSupported = caps.apfVersionSupported;
+        p.maximumApfProgramSize = caps.maximumApfProgramSize;
+        p.apfPacketFormat = caps.apfPacketFormat;
+        return p;
+    }
+
+    /**
+     * Convert ApfCapabilitiesParcelable toApfCapabilities.
+     */
+    public static ApfCapabilities fromStableParcelable(@Nullable ApfCapabilitiesParcelable p) {
+        if (p == null) return null;
+        return new ApfCapabilities(
+                p.apfVersionSupported, p.maximumApfProgramSize, p.apfPacketFormat);
+    }
+
+    /**
+     * Convert InetAddress to String.
+     * TODO: have an InetAddressParcelable
+     */
+    public static String parcelAddress(@Nullable InetAddress addr) {
+        if (addr == null) return null;
+        return addr.getHostAddress();
+    }
+
+    /**
+     * Convert String to InetAddress.
+     * TODO: have an InetAddressParcelable
+     */
+    public static InetAddress unparcelAddress(@Nullable String addr) {
+        if (addr == null) return null;
+        return InetAddresses.parseNumericAddress(addr);
+    }
+}
diff --git a/services/net/java/android/net/shared/LinkPropertiesParcelableUtil.java b/services/net/java/android/net/shared/LinkPropertiesParcelableUtil.java
index 5b77f54..d5213df 100644
--- a/services/net/java/android/net/shared/LinkPropertiesParcelableUtil.java
+++ b/services/net/java/android/net/shared/LinkPropertiesParcelableUtil.java
@@ -16,11 +16,12 @@
 
 package android.net.shared;
 
+import static android.net.shared.IpConfigurationParcelableUtil.parcelAddress;
+import static android.net.shared.IpConfigurationParcelableUtil.unparcelAddress;
 import static android.net.shared.ParcelableUtil.fromParcelableArray;
 import static android.net.shared.ParcelableUtil.toParcelableArray;
 
 import android.annotation.Nullable;
-import android.net.InetAddresses;
 import android.net.IpPrefix;
 import android.net.IpPrefixParcelable;
 import android.net.LinkAddress;
@@ -33,7 +34,6 @@
 import android.net.RouteInfoParcelable;
 import android.net.Uri;
 
-import java.net.InetAddress;
 import java.util.Arrays;
 
 /**
@@ -81,7 +81,7 @@
             return null;
         }
         final IpPrefixParcelable parcel = new IpPrefixParcelable();
-        parcel.address = ipPrefix.getAddress().getHostAddress();
+        parcel.address = parcelAddress(ipPrefix.getAddress());
         parcel.prefixLength = ipPrefix.getPrefixLength();
         return parcel;
     }
@@ -93,7 +93,7 @@
         if (parcel == null) {
             return null;
         }
-        return new IpPrefix(InetAddresses.parseNumericAddress(parcel.address), parcel.prefixLength);
+        return new IpPrefix(unparcelAddress(parcel.address), parcel.prefixLength);
     }
 
     /**
@@ -105,7 +105,7 @@
         }
         final RouteInfoParcelable parcel = new RouteInfoParcelable();
         parcel.destination = toStableParcelable(routeInfo.getDestination());
-        parcel.gatewayAddr = routeInfo.getGateway().getHostAddress();
+        parcel.gatewayAddr = parcelAddress(routeInfo.getGateway());
         parcel.ifaceName = routeInfo.getInterface();
         parcel.type = routeInfo.getType();
         return parcel;
@@ -120,7 +120,7 @@
         }
         final IpPrefix destination = fromStableParcelable(parcel.destination);
         return new RouteInfo(
-                destination, InetAddresses.parseNumericAddress(parcel.gatewayAddr),
+                destination, unparcelAddress(parcel.gatewayAddr),
                 parcel.ifaceName, parcel.type);
     }
 
@@ -132,7 +132,7 @@
             return null;
         }
         final LinkAddressParcelable parcel = new LinkAddressParcelable();
-        parcel.address = la.getAddress().getHostAddress();
+        parcel.address = parcelAddress(la.getAddress());
         parcel.prefixLength = la.getPrefixLength();
         parcel.flags = la.getFlags();
         parcel.scope = la.getScope();
@@ -147,7 +147,7 @@
             return null;
         }
         return new LinkAddress(
-                InetAddresses.parseNumericAddress(parcel.address),
+                unparcelAddress(parcel.address),
                 parcel.prefixLength,
                 parcel.flags,
                 parcel.scope);
@@ -167,11 +167,11 @@
                 LinkPropertiesParcelableUtil::toStableParcelable,
                 LinkAddressParcelable.class);
         parcel.dnses = toParcelableArray(
-                lp.getDnsServers(), InetAddress::getHostAddress, String.class);
+                lp.getDnsServers(), IpConfigurationParcelableUtil::parcelAddress, String.class);
         parcel.pcscfs = toParcelableArray(
-                lp.getPcscfServers(), InetAddress::getHostAddress, String.class);
-        parcel.validatedPrivateDnses = toParcelableArray(
-                lp.getValidatedPrivateDnsServers(), InetAddress::getHostAddress, String.class);
+                lp.getPcscfServers(), IpConfigurationParcelableUtil::parcelAddress, String.class);
+        parcel.validatedPrivateDnses = toParcelableArray(lp.getValidatedPrivateDnsServers(),
+                IpConfigurationParcelableUtil::parcelAddress, String.class);
         parcel.usePrivateDns = lp.isPrivateDnsActive();
         parcel.privateDnsServerName = lp.getPrivateDnsServerName();
         parcel.domains = lp.getDomains();
@@ -199,11 +199,13 @@
         lp.setInterfaceName(parcel.ifaceName);
         lp.setLinkAddresses(fromParcelableArray(parcel.linkAddresses,
                 LinkPropertiesParcelableUtil::fromStableParcelable));
-        lp.setDnsServers(fromParcelableArray(parcel.dnses, InetAddresses::parseNumericAddress));
-        lp.setPcscfServers(fromParcelableArray(parcel.pcscfs, InetAddresses::parseNumericAddress));
+        lp.setDnsServers(fromParcelableArray(
+                parcel.dnses, IpConfigurationParcelableUtil::unparcelAddress));
+        lp.setPcscfServers(fromParcelableArray(
+                parcel.pcscfs, IpConfigurationParcelableUtil::unparcelAddress));
         lp.setValidatedPrivateDnsServers(
                 fromParcelableArray(parcel.validatedPrivateDnses,
-                InetAddresses::parseNumericAddress));
+                IpConfigurationParcelableUtil::unparcelAddress));
         lp.setUsePrivateDns(parcel.usePrivateDns);
         lp.setPrivateDnsServerName(parcel.privateDnsServerName);
         lp.setDomains(parcel.domains);
diff --git a/services/net/java/android/net/shared/NetworkParcelableUtil.java b/services/net/java/android/net/shared/NetworkParcelableUtil.java
new file mode 100644
index 0000000..d0b54b8
--- /dev/null
+++ b/services/net/java/android/net/shared/NetworkParcelableUtil.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2019 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.shared;
+
+import android.annotation.Nullable;
+import android.net.Network;
+import android.net.NetworkParcelable;
+
+/**
+ * Utility methods to convert to/from stable AIDL parcelables for network attribute classes.
+ * @hide
+ */
+public final class NetworkParcelableUtil {
+    /**
+     * Convert from a Network to a NetworkParcelable.
+     */
+    public static NetworkParcelable toStableParcelable(@Nullable Network network) {
+        if (network == null) {
+            return null;
+        }
+        final NetworkParcelable p = new NetworkParcelable();
+        p.networkHandle = network.getNetworkHandle();
+
+        return p;
+    }
+
+    /**
+     * Convert from a NetworkParcelable to a Network.
+     */
+    public static Network fromStableParcelable(@Nullable NetworkParcelable p) {
+        if (p == null) {
+            return null;
+        }
+        return Network.fromNetworkHandle(p.networkHandle);
+    }
+}
diff --git a/services/net/java/android/net/shared/ParcelableUtil.java b/services/net/java/android/net/shared/ParcelableUtil.java
index a18976c..3f40300 100644
--- a/services/net/java/android/net/shared/ParcelableUtil.java
+++ b/services/net/java/android/net/shared/ParcelableUtil.java
@@ -20,7 +20,7 @@
 
 import java.lang.reflect.Array;
 import java.util.ArrayList;
-import java.util.List;
+import java.util.Collection;
 import java.util.function.Function;
 
 /**
@@ -36,7 +36,7 @@
      * converter function.
      */
     public static <ParcelableType, BaseType> ParcelableType[] toParcelableArray(
-            @NonNull List<BaseType> base,
+            @NonNull Collection<BaseType> base,
             @NonNull Function<BaseType, ParcelableType> conv,
             @NonNull Class<ParcelableType> parcelClass) {
         final ParcelableType[] out = (ParcelableType[]) Array.newInstance(parcelClass, base.size());
diff --git a/services/net/java/android/net/shared/PrivateDnsConfig.java b/services/net/java/android/net/shared/PrivateDnsConfig.java
index 41e0bad..c7dc530 100644
--- a/services/net/java/android/net/shared/PrivateDnsConfig.java
+++ b/services/net/java/android/net/shared/PrivateDnsConfig.java
@@ -16,7 +16,9 @@
 
 package android.net.shared;
 
-import android.net.InetAddresses;
+import static android.net.shared.ParcelableUtil.fromParcelableArray;
+import static android.net.shared.ParcelableUtil.toParcelableArray;
+
 import android.net.PrivateDnsConfigParcel;
 import android.text.TextUtils;
 
@@ -70,12 +72,8 @@
     public PrivateDnsConfigParcel toParcel() {
         final PrivateDnsConfigParcel parcel = new PrivateDnsConfigParcel();
         parcel.hostname = hostname;
-
-        final String[] parceledIps = new String[ips.length];
-        for (int i = 0; i < ips.length; i++) {
-            parceledIps[i] = ips[i].getHostAddress();
-        }
-        parcel.ips = parceledIps;
+        parcel.ips = toParcelableArray(
+                Arrays.asList(ips), IpConfigurationParcelableUtil::parcelAddress, String.class);
 
         return parcel;
     }
@@ -84,11 +82,9 @@
      * Build a configuration from a stable AIDL-compatible parcel.
      */
     public static PrivateDnsConfig fromParcel(PrivateDnsConfigParcel parcel) {
-        final InetAddress[] ips = new InetAddress[parcel.ips.length];
-        for (int i = 0; i < ips.length; i++) {
-            ips[i] = InetAddresses.parseNumericAddress(parcel.ips[i]);
-        }
-
+        InetAddress[] ips = new InetAddress[parcel.ips.length];
+        ips = fromParcelableArray(parcel.ips, IpConfigurationParcelableUtil::unparcelAddress)
+                .toArray(ips);
         return new PrivateDnsConfig(parcel.hostname, ips);
     }
 }
diff --git a/services/net/java/android/net/shared/ProvisioningConfiguration.java b/services/net/java/android/net/shared/ProvisioningConfiguration.java
new file mode 100644
index 0000000..d995d1b
--- /dev/null
+++ b/services/net/java/android/net/shared/ProvisioningConfiguration.java
@@ -0,0 +1,309 @@
+/*
+ * Copyright (C) 2019 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.shared;
+
+import android.annotation.Nullable;
+import android.net.INetd;
+import android.net.Network;
+import android.net.ProvisioningConfigurationParcelable;
+import android.net.StaticIpConfiguration;
+import android.net.apf.ApfCapabilities;
+
+import java.util.Objects;
+import java.util.StringJoiner;
+
+/**
+ * This class encapsulates parameters to be passed to
+ * IpClient#startProvisioning(). A defensive copy is made by IpClient
+ * and the values specified herein are in force until IpClient#stop()
+ * is called.
+ *
+ * Example use:
+ *
+ *     final ProvisioningConfiguration config =
+ *             new ProvisioningConfiguration.Builder()
+ *                     .withPreDhcpAction()
+ *                     .withProvisioningTimeoutMs(36 * 1000)
+ *                     .build();
+ *     mIpClient.startProvisioning(config.toStableParcelable());
+ *     ...
+ *     mIpClient.stop();
+ *
+ * The specified provisioning configuration will only be active until
+ * IIpClient#stop() is called. Future calls to IIpClient#startProvisioning()
+ * must specify the configuration again.
+ * @hide
+ */
+public class ProvisioningConfiguration {
+    // TODO: Delete this default timeout once those callers that care are
+    // fixed to pass in their preferred timeout.
+    //
+    // We pick 36 seconds so we can send DHCP requests at
+    //
+    //     t=0, t=2, t=6, t=14, t=30
+    //
+    // allowing for 10% jitter.
+    private static final int DEFAULT_TIMEOUT_MS = 36 * 1000;
+
+    /**
+     * Builder to create a {@link ProvisioningConfiguration}.
+     */
+    public static class Builder {
+        protected ProvisioningConfiguration mConfig = new ProvisioningConfiguration();
+
+        /**
+         * Specify that the configuration should not enable IPv4. It is enabled by default.
+         */
+        public Builder withoutIPv4() {
+            mConfig.mEnableIPv4 = false;
+            return this;
+        }
+
+        /**
+         * Specify that the configuration should not enable IPv6. It is enabled by default.
+         */
+        public Builder withoutIPv6() {
+            mConfig.mEnableIPv6 = false;
+            return this;
+        }
+
+        /**
+         * Specify that the configuration should not use a MultinetworkPolicyTracker. It is used
+         * by default.
+         */
+        public Builder withoutMultinetworkPolicyTracker() {
+            mConfig.mUsingMultinetworkPolicyTracker = false;
+            return this;
+        }
+
+        /**
+         * Specify that the configuration should not use a IpReachabilityMonitor. It is used by
+         * default.
+         */
+        public Builder withoutIpReachabilityMonitor() {
+            mConfig.mUsingIpReachabilityMonitor = false;
+            return this;
+        }
+
+        /**
+         * Identical to {@link #withPreDhcpAction(int)}, using a default timeout.
+         * @see #withPreDhcpAction(int)
+         */
+        public Builder withPreDhcpAction() {
+            mConfig.mRequestedPreDhcpActionMs = DEFAULT_TIMEOUT_MS;
+            return this;
+        }
+
+        /**
+         * Specify that {@link IpClientCallbacks#onPreDhcpAction()} should be called. Clients must
+         * call {@link IIpClient#completedPreDhcpAction()} when the callback called. This behavior
+         * is disabled by default.
+         * @param dhcpActionTimeoutMs Timeout for clients to call completedPreDhcpAction().
+         */
+        public Builder withPreDhcpAction(int dhcpActionTimeoutMs) {
+            mConfig.mRequestedPreDhcpActionMs = dhcpActionTimeoutMs;
+            return this;
+        }
+
+        /**
+         * Specify the initial provisioning configuration.
+         */
+        public Builder withInitialConfiguration(InitialConfiguration initialConfig) {
+            mConfig.mInitialConfig = initialConfig;
+            return this;
+        }
+
+        /**
+         * Specify a static configuration for provisioning.
+         */
+        public Builder withStaticConfiguration(StaticIpConfiguration staticConfig) {
+            mConfig.mStaticIpConfig = staticConfig;
+            return this;
+        }
+
+        /**
+         * Specify ApfCapabilities.
+         */
+        public Builder withApfCapabilities(ApfCapabilities apfCapabilities) {
+            mConfig.mApfCapabilities = apfCapabilities;
+            return this;
+        }
+
+        /**
+         * Specify the timeout to use for provisioning.
+         */
+        public Builder withProvisioningTimeoutMs(int timeoutMs) {
+            mConfig.mProvisioningTimeoutMs = timeoutMs;
+            return this;
+        }
+
+        /**
+         * Specify that IPv6 address generation should use a random MAC address.
+         */
+        public Builder withRandomMacAddress() {
+            mConfig.mIPv6AddrGenMode = INetd.IPV6_ADDR_GEN_MODE_EUI64;
+            return this;
+        }
+
+        /**
+         * Specify that IPv6 address generation should use a stable MAC address.
+         */
+        public Builder withStableMacAddress() {
+            mConfig.mIPv6AddrGenMode = INetd.IPV6_ADDR_GEN_MODE_STABLE_PRIVACY;
+            return this;
+        }
+
+        /**
+         * Specify the network to use for provisioning.
+         */
+        public Builder withNetwork(Network network) {
+            mConfig.mNetwork = network;
+            return this;
+        }
+
+        /**
+         * Specify the display name that the IpClient should use.
+         */
+        public Builder withDisplayName(String displayName) {
+            mConfig.mDisplayName = displayName;
+            return this;
+        }
+
+        /**
+         * Build the configuration using previously specified parameters.
+         */
+        public ProvisioningConfiguration build() {
+            return new ProvisioningConfiguration(mConfig);
+        }
+    }
+
+    public boolean mEnableIPv4 = true;
+    public boolean mEnableIPv6 = true;
+    public boolean mUsingMultinetworkPolicyTracker = true;
+    public boolean mUsingIpReachabilityMonitor = true;
+    public int mRequestedPreDhcpActionMs;
+    public InitialConfiguration mInitialConfig;
+    public StaticIpConfiguration mStaticIpConfig;
+    public ApfCapabilities mApfCapabilities;
+    public int mProvisioningTimeoutMs = DEFAULT_TIMEOUT_MS;
+    public int mIPv6AddrGenMode = INetd.IPV6_ADDR_GEN_MODE_STABLE_PRIVACY;
+    public Network mNetwork = null;
+    public String mDisplayName = null;
+
+    public ProvisioningConfiguration() {} // used by Builder
+
+    public ProvisioningConfiguration(ProvisioningConfiguration other) {
+        mEnableIPv4 = other.mEnableIPv4;
+        mEnableIPv6 = other.mEnableIPv6;
+        mUsingMultinetworkPolicyTracker = other.mUsingMultinetworkPolicyTracker;
+        mUsingIpReachabilityMonitor = other.mUsingIpReachabilityMonitor;
+        mRequestedPreDhcpActionMs = other.mRequestedPreDhcpActionMs;
+        mInitialConfig = InitialConfiguration.copy(other.mInitialConfig);
+        mStaticIpConfig = other.mStaticIpConfig == null
+                ? null
+                : new StaticIpConfiguration(other.mStaticIpConfig);
+        mApfCapabilities = other.mApfCapabilities;
+        mProvisioningTimeoutMs = other.mProvisioningTimeoutMs;
+        mIPv6AddrGenMode = other.mIPv6AddrGenMode;
+        mNetwork = other.mNetwork;
+        mDisplayName = other.mDisplayName;
+    }
+
+    /**
+     * Create a ProvisioningConfigurationParcelable from this ProvisioningConfiguration.
+     */
+    public ProvisioningConfigurationParcelable toStableParcelable() {
+        final ProvisioningConfigurationParcelable p = new ProvisioningConfigurationParcelable();
+        p.enableIPv4 = mEnableIPv4;
+        p.enableIPv6 = mEnableIPv6;
+        p.usingMultinetworkPolicyTracker = mUsingMultinetworkPolicyTracker;
+        p.usingIpReachabilityMonitor = mUsingIpReachabilityMonitor;
+        p.requestedPreDhcpActionMs = mRequestedPreDhcpActionMs;
+        p.initialConfig = mInitialConfig == null ? null : mInitialConfig.toStableParcelable();
+        p.staticIpConfig = IpConfigurationParcelableUtil.toStableParcelable(mStaticIpConfig);
+        p.apfCapabilities = IpConfigurationParcelableUtil.toStableParcelable(mApfCapabilities);
+        p.provisioningTimeoutMs = mProvisioningTimeoutMs;
+        p.ipv6AddrGenMode = mIPv6AddrGenMode;
+        p.network = NetworkParcelableUtil.toStableParcelable(mNetwork);
+        p.displayName = mDisplayName;
+        return p;
+    }
+
+    /**
+     * Create a ProvisioningConfiguration from a ProvisioningConfigurationParcelable.
+     */
+    public static ProvisioningConfiguration fromStableParcelable(
+            @Nullable ProvisioningConfigurationParcelable p) {
+        if (p == null) return null;
+        final ProvisioningConfiguration config = new ProvisioningConfiguration();
+        config.mEnableIPv4 = p.enableIPv4;
+        config.mEnableIPv6 = p.enableIPv6;
+        config.mUsingMultinetworkPolicyTracker = p.usingMultinetworkPolicyTracker;
+        config.mUsingIpReachabilityMonitor = p.usingIpReachabilityMonitor;
+        config.mRequestedPreDhcpActionMs = p.requestedPreDhcpActionMs;
+        config.mInitialConfig = InitialConfiguration.fromStableParcelable(p.initialConfig);
+        config.mStaticIpConfig = IpConfigurationParcelableUtil.fromStableParcelable(
+                p.staticIpConfig);
+        config.mApfCapabilities = IpConfigurationParcelableUtil.fromStableParcelable(
+                p.apfCapabilities);
+        config.mProvisioningTimeoutMs = p.provisioningTimeoutMs;
+        config.mIPv6AddrGenMode = p.ipv6AddrGenMode;
+        config.mNetwork = NetworkParcelableUtil.fromStableParcelable(p.network);
+        config.mDisplayName = p.displayName;
+        return config;
+    }
+
+    @Override
+    public String toString() {
+        return new StringJoiner(", ", getClass().getSimpleName() + "{", "}")
+                .add("mEnableIPv4: " + mEnableIPv4)
+                .add("mEnableIPv6: " + mEnableIPv6)
+                .add("mUsingMultinetworkPolicyTracker: " + mUsingMultinetworkPolicyTracker)
+                .add("mUsingIpReachabilityMonitor: " + mUsingIpReachabilityMonitor)
+                .add("mRequestedPreDhcpActionMs: " + mRequestedPreDhcpActionMs)
+                .add("mInitialConfig: " + mInitialConfig)
+                .add("mStaticIpConfig: " + mStaticIpConfig)
+                .add("mApfCapabilities: " + mApfCapabilities)
+                .add("mProvisioningTimeoutMs: " + mProvisioningTimeoutMs)
+                .add("mIPv6AddrGenMode: " + mIPv6AddrGenMode)
+                .add("mNetwork: " + mNetwork)
+                .add("mDisplayName: " + mDisplayName)
+                .toString();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof ProvisioningConfiguration)) return false;
+        final ProvisioningConfiguration other = (ProvisioningConfiguration) obj;
+        return mEnableIPv4 == other.mEnableIPv4
+                && mEnableIPv6 == other.mEnableIPv6
+                && mUsingMultinetworkPolicyTracker == other.mUsingMultinetworkPolicyTracker
+                && mUsingIpReachabilityMonitor == other.mUsingIpReachabilityMonitor
+                && mRequestedPreDhcpActionMs == other.mRequestedPreDhcpActionMs
+                && Objects.equals(mInitialConfig, other.mInitialConfig)
+                && Objects.equals(mStaticIpConfig, other.mStaticIpConfig)
+                && Objects.equals(mApfCapabilities, other.mApfCapabilities)
+                && mProvisioningTimeoutMs == other.mProvisioningTimeoutMs
+                && mIPv6AddrGenMode == other.mIPv6AddrGenMode
+                && Objects.equals(mNetwork, other.mNetwork)
+                && Objects.equals(mDisplayName, other.mDisplayName);
+    }
+
+    public boolean isValid() {
+        return (mInitialConfig == null) || mInitialConfig.isValid();
+    }
+}
diff --git a/tests/net/java/android/net/shared/InitialConfigurationTest.java b/tests/net/java/android/net/shared/InitialConfigurationTest.java
new file mode 100644
index 0000000..78792bd
--- /dev/null
+++ b/tests/net/java/android/net/shared/InitialConfigurationTest.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2019 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.shared;
+
+import static android.net.InetAddresses.parseNumericAddress;
+import static android.net.shared.ParcelableTestUtil.assertFieldCountEquals;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import android.net.IpPrefix;
+import android.net.LinkAddress;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.Arrays;
+import java.util.function.Consumer;
+
+/**
+ * Tests for {@link InitialConfiguration}
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class InitialConfigurationTest {
+    private InitialConfiguration mConfig;
+
+    @Before
+    public void setUp() {
+        mConfig = new InitialConfiguration();
+        mConfig.ipAddresses.addAll(Arrays.asList(
+                new LinkAddress(parseNumericAddress("192.168.45.45"), 16),
+                new LinkAddress(parseNumericAddress("2001:db8::45"), 33)));
+        mConfig.directlyConnectedRoutes.addAll(Arrays.asList(
+                new IpPrefix(parseNumericAddress("192.168.46.46"), 17),
+                new IpPrefix(parseNumericAddress("2001:db8::46"), 34)));
+        mConfig.dnsServers.addAll(Arrays.asList(
+                parseNumericAddress("192.168.47.47"),
+                parseNumericAddress("2001:db8::47")));
+        // Any added InitialConfiguration field must be included in equals() to be tested properly
+        assertFieldCountEquals(3, InitialConfiguration.class);
+    }
+
+    @Test
+    public void testParcelUnparcelInitialConfiguration() {
+        final InitialConfiguration unparceled =
+                InitialConfiguration.fromStableParcelable(mConfig.toStableParcelable());
+        assertEquals(mConfig, unparceled);
+    }
+
+    @Test
+    public void testEquals() {
+        assertEquals(mConfig, InitialConfiguration.copy(mConfig));
+
+        assertNotEqualsAfterChange(c -> c.ipAddresses.add(
+                new LinkAddress(parseNumericAddress("192.168.47.47"), 24)));
+        assertNotEqualsAfterChange(c -> c.directlyConnectedRoutes.add(
+                new IpPrefix(parseNumericAddress("192.168.46.46"), 32)));
+        assertNotEqualsAfterChange(c -> c.dnsServers.add(parseNumericAddress("2001:db8::49")));
+        assertFieldCountEquals(3, InitialConfiguration.class);
+    }
+
+    private void assertNotEqualsAfterChange(Consumer<InitialConfiguration> mutator) {
+        final InitialConfiguration newConfig = InitialConfiguration.copy(mConfig);
+        mutator.accept(newConfig);
+        assertNotEquals(mConfig, newConfig);
+    }
+}
diff --git a/tests/net/java/android/net/shared/IpConfigurationParcelableUtilTest.java b/tests/net/java/android/net/shared/IpConfigurationParcelableUtilTest.java
new file mode 100644
index 0000000..14df392
--- /dev/null
+++ b/tests/net/java/android/net/shared/IpConfigurationParcelableUtilTest.java
@@ -0,0 +1,130 @@
+/*
+ * Copyright (C) 2019 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.shared;
+
+import static android.net.InetAddresses.parseNumericAddress;
+import static android.net.shared.IpConfigurationParcelableUtil.fromStableParcelable;
+import static android.net.shared.IpConfigurationParcelableUtil.toStableParcelable;
+import static android.net.shared.ParcelableTestUtil.assertFieldCountEquals;
+
+import static org.junit.Assert.assertEquals;
+
+import android.net.DhcpResults;
+import android.net.LinkAddress;
+import android.net.StaticIpConfiguration;
+import android.net.apf.ApfCapabilities;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.net.Inet4Address;
+
+/**
+ * Tests for {@link IpConfigurationParcelableUtil}.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class IpConfigurationParcelableUtilTest {
+    private StaticIpConfiguration mStaticIpConfiguration;
+    private DhcpResults mDhcpResults;
+
+    @Before
+    public void setUp() {
+        mStaticIpConfiguration = new StaticIpConfiguration();
+        mStaticIpConfiguration.ipAddress = new LinkAddress(parseNumericAddress("2001:db8::42"), 64);
+        mStaticIpConfiguration.gateway = parseNumericAddress("192.168.42.42");
+        mStaticIpConfiguration.dnsServers.add(parseNumericAddress("2001:db8::43"));
+        mStaticIpConfiguration.dnsServers.add(parseNumericAddress("192.168.43.43"));
+        mStaticIpConfiguration.domains = "example.com";
+        // Any added StaticIpConfiguration field must be included in equals() to be tested properly
+        assertFieldCountEquals(4, StaticIpConfiguration.class);
+
+        mDhcpResults = new DhcpResults(mStaticIpConfiguration);
+        mDhcpResults.serverAddress = (Inet4Address) parseNumericAddress("192.168.44.44");
+        mDhcpResults.vendorInfo = "TEST_VENDOR_INFO";
+        mDhcpResults.leaseDuration = 3600;
+        mDhcpResults.mtu = 1450;
+        // Any added DhcpResults field must be included in equals() to be tested properly
+        assertFieldCountEquals(4, DhcpResults.class);
+    }
+
+    @Test
+    public void testParcelUnparcelStaticConfiguration() {
+        doStaticConfigurationParcelUnparcelTest();
+    }
+
+    @Test
+    public void testParcelUnparcelStaticConfiguration_NullIpAddress() {
+        mStaticIpConfiguration.ipAddress = null;
+        doStaticConfigurationParcelUnparcelTest();
+    }
+
+    @Test
+    public void testParcelUnparcelStaticConfiguration_NullGateway() {
+        mStaticIpConfiguration.gateway = null;
+        doStaticConfigurationParcelUnparcelTest();
+    }
+
+    @Test
+    public void testParcelUnparcelStaticConfiguration_NullDomains() {
+        mStaticIpConfiguration.domains = null;
+        doStaticConfigurationParcelUnparcelTest();
+    }
+
+    @Test
+    public void testParcelUnparcelStaticConfiguration_EmptyDomains() {
+        mStaticIpConfiguration.domains = "";
+        doStaticConfigurationParcelUnparcelTest();
+    }
+
+    private void doStaticConfigurationParcelUnparcelTest() {
+        final StaticIpConfiguration unparceled =
+                fromStableParcelable(toStableParcelable(mStaticIpConfiguration));
+        assertEquals(mStaticIpConfiguration, unparceled);
+    }
+
+    @Test
+    public void testParcelUnparcelDhcpResults() {
+        doDhcpResultsParcelUnparcelTest();
+    }
+
+    @Test
+    public void testParcelUnparcelDhcpResults_NullServerAddress() {
+        mDhcpResults.serverAddress = null;
+        doDhcpResultsParcelUnparcelTest();
+    }
+
+    @Test
+    public void testParcelUnparcelDhcpResults_NullVendorInfo() {
+        mDhcpResults.vendorInfo = null;
+        doDhcpResultsParcelUnparcelTest();
+    }
+
+    private void doDhcpResultsParcelUnparcelTest() {
+        final DhcpResults unparceled = fromStableParcelable(toStableParcelable(mDhcpResults));
+        assertEquals(mDhcpResults, unparceled);
+    }
+
+    @Test
+    public void testParcelUnparcelApfCapabilities() {
+        final ApfCapabilities caps = new ApfCapabilities(123, 456, 789);
+        assertEquals(caps, fromStableParcelable(toStableParcelable(caps)));
+    }
+}
diff --git a/tests/net/java/android/net/shared/LinkPropertiesParcelableUtilTest.java b/tests/net/java/android/net/shared/LinkPropertiesParcelableUtilTest.java
index 4cabfc9..6f711c0 100644
--- a/tests/net/java/android/net/shared/LinkPropertiesParcelableUtilTest.java
+++ b/tests/net/java/android/net/shared/LinkPropertiesParcelableUtilTest.java
@@ -18,6 +18,7 @@
 
 import static android.net.shared.LinkPropertiesParcelableUtil.fromStableParcelable;
 import static android.net.shared.LinkPropertiesParcelableUtil.toStableParcelable;
+import static android.net.shared.ParcelableTestUtil.assertFieldCountEquals;
 
 import static org.junit.Assert.assertEquals;
 
@@ -35,7 +36,6 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-import java.lang.reflect.Modifier;
 import java.util.Arrays;
 import java.util.Collections;
 
@@ -100,8 +100,7 @@
         // Verify that this test does not miss any new field added later.
         // If any added field is not included in LinkProperties#equals, assertLinkPropertiesEquals
         // must also be updated.
-        assertEquals(14, Arrays.stream(LinkProperties.class.getDeclaredFields())
-                .filter(f -> !Modifier.isStatic(f.getModifiers())).count());
+        assertFieldCountEquals(14, LinkProperties.class);
 
         return lp;
     }
diff --git a/tests/net/java/android/net/shared/ParcelableTestUtil.java b/tests/net/java/android/net/shared/ParcelableTestUtil.java
new file mode 100644
index 0000000..088ea3c
--- /dev/null
+++ b/tests/net/java/android/net/shared/ParcelableTestUtil.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2019 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.shared;
+
+import static org.junit.Assert.assertEquals;
+
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
+
+/**
+ * Utility classes to write tests for stable AIDL parceling/unparceling
+ */
+public final class ParcelableTestUtil {
+
+    /**
+     * Verifies that the number of nonstatic fields in a class equals a given count.
+     *
+     * <p>This assertion serves as a reminder to update test code around it if fields are added
+     * after the test is written.
+     * @param count Expected number of nonstatic fields in the class.
+     * @param clazz Class to test.
+     */
+    public static <T> void assertFieldCountEquals(int count, Class<T> clazz) {
+        assertEquals(count, Arrays.stream(clazz.getDeclaredFields())
+                .filter(f -> !Modifier.isStatic(f.getModifiers())).count());
+    }
+}
diff --git a/tests/net/java/android/net/shared/ProvisioningConfigurationTest.java b/tests/net/java/android/net/shared/ProvisioningConfigurationTest.java
new file mode 100644
index 0000000..6ea47d2
--- /dev/null
+++ b/tests/net/java/android/net/shared/ProvisioningConfigurationTest.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (C) 2019 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.shared;
+
+import static android.net.InetAddresses.parseNumericAddress;
+import static android.net.shared.ParcelableTestUtil.assertFieldCountEquals;
+import static android.net.shared.ProvisioningConfiguration.fromStableParcelable;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotEquals;
+
+import android.net.LinkAddress;
+import android.net.Network;
+import android.net.StaticIpConfiguration;
+import android.net.apf.ApfCapabilities;
+import android.support.test.filters.SmallTest;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.function.Consumer;
+
+/**
+ * Tests for {@link ProvisioningConfiguration}.
+ */
+@RunWith(AndroidJUnit4.class)
+@SmallTest
+public class ProvisioningConfigurationTest {
+    private ProvisioningConfiguration mConfig;
+
+    @Before
+    public void setUp() {
+        mConfig = new ProvisioningConfiguration();
+        mConfig.mEnableIPv4 = true;
+        mConfig.mEnableIPv6 = true;
+        mConfig.mUsingMultinetworkPolicyTracker = true;
+        mConfig.mUsingIpReachabilityMonitor = true;
+        mConfig.mRequestedPreDhcpActionMs = 42;
+        mConfig.mInitialConfig = new InitialConfiguration();
+        mConfig.mInitialConfig.ipAddresses.add(
+                new LinkAddress(parseNumericAddress("192.168.42.42"), 24));
+        mConfig.mStaticIpConfig = new StaticIpConfiguration();
+        mConfig.mStaticIpConfig.ipAddress =
+                new LinkAddress(parseNumericAddress("2001:db8::42"), 90);
+        // Not testing other InitialConfig or StaticIpConfig members: they have their own unit tests
+        mConfig.mApfCapabilities = new ApfCapabilities(1, 2, 3);
+        mConfig.mProvisioningTimeoutMs = 4200;
+        mConfig.mIPv6AddrGenMode = 123;
+        mConfig.mNetwork = new Network(321);
+        mConfig.mDisplayName = "test_config";
+        // Any added field must be included in equals() to be tested properly
+        assertFieldCountEquals(12, ProvisioningConfiguration.class);
+    }
+
+    @Test
+    public void testParcelUnparcel() {
+        doParcelUnparcelTest();
+    }
+
+    @Test
+    public void testParcelUnparcel_NullInitialConfiguration() {
+        mConfig.mInitialConfig = null;
+        doParcelUnparcelTest();
+    }
+
+    @Test
+    public void testParcelUnparcel_NullStaticConfiguration() {
+        mConfig.mStaticIpConfig = null;
+        doParcelUnparcelTest();
+    }
+
+    @Test
+    public void testParcelUnparcel_NullApfCapabilities() {
+        mConfig.mApfCapabilities = null;
+        doParcelUnparcelTest();
+    }
+
+    @Test
+    public void testParcelUnparcel_NullNetwork() {
+        mConfig.mNetwork = null;
+        doParcelUnparcelTest();
+    }
+
+    private void doParcelUnparcelTest() {
+        final ProvisioningConfiguration unparceled =
+                fromStableParcelable(mConfig.toStableParcelable());
+        assertEquals(mConfig, unparceled);
+    }
+
+    @Test
+    public void testEquals() {
+        assertEquals(mConfig, new ProvisioningConfiguration(mConfig));
+
+        assertNotEqualsAfterChange(c -> c.mEnableIPv4 = false);
+        assertNotEqualsAfterChange(c -> c.mEnableIPv6 = false);
+        assertNotEqualsAfterChange(c -> c.mUsingMultinetworkPolicyTracker = false);
+        assertNotEqualsAfterChange(c -> c.mUsingIpReachabilityMonitor = false);
+        assertNotEqualsAfterChange(c -> c.mRequestedPreDhcpActionMs++);
+        assertNotEqualsAfterChange(c -> c.mInitialConfig.ipAddresses.add(
+                new LinkAddress(parseNumericAddress("192.168.47.47"), 16)));
+        assertNotEqualsAfterChange(c -> c.mInitialConfig = null);
+        assertNotEqualsAfterChange(c -> c.mStaticIpConfig.ipAddress =
+                new LinkAddress(parseNumericAddress("2001:db8::47"), 64));
+        assertNotEqualsAfterChange(c -> c.mStaticIpConfig = null);
+        assertNotEqualsAfterChange(c -> c.mApfCapabilities = new ApfCapabilities(4, 5, 6));
+        assertNotEqualsAfterChange(c -> c.mApfCapabilities = null);
+        assertNotEqualsAfterChange(c -> c.mProvisioningTimeoutMs++);
+        assertNotEqualsAfterChange(c -> c.mIPv6AddrGenMode++);
+        assertNotEqualsAfterChange(c -> c.mNetwork = new Network(123));
+        assertNotEqualsAfterChange(c -> c.mNetwork = null);
+        assertNotEqualsAfterChange(c -> c.mDisplayName = "other_test");
+        assertNotEqualsAfterChange(c -> c.mDisplayName = null);
+        assertFieldCountEquals(12, ProvisioningConfiguration.class);
+    }
+
+    private void assertNotEqualsAfterChange(Consumer<ProvisioningConfiguration> mutator) {
+        final ProvisioningConfiguration newConfig = new ProvisioningConfiguration(mConfig);
+        mutator.accept(newConfig);
+        assertNotEquals(mConfig, newConfig);
+    }
+}