Merge "Enterprise Policy for Private DNS Setting"
diff --git a/api/current.txt b/api/current.txt
index fabfc10..3b965f1 100755
--- a/api/current.txt
+++ b/api/current.txt
@@ -6522,6 +6522,8 @@
     method public java.util.List<java.lang.String> getDelegatedScopes(android.content.ComponentName, java.lang.String);
     method public java.lang.CharSequence getDeviceOwnerLockScreenInfo();
     method public java.lang.CharSequence getEndUserSessionMessage(android.content.ComponentName);
+    method public java.lang.String getGlobalPrivateDnsHost(android.content.ComponentName);
+    method public int getGlobalPrivateDnsMode(android.content.ComponentName);
     method public java.util.List<byte[]> getInstalledCaCerts(android.content.ComponentName);
     method public java.util.List<java.lang.String> getKeepUninstalledPackages(android.content.ComponentName);
     method public int getKeyguardDisabledFeatures(android.content.ComponentName);
@@ -6625,6 +6627,7 @@
     method public void setDelegatedScopes(android.content.ComponentName, java.lang.String, java.util.List<java.lang.String>);
     method public void setDeviceOwnerLockScreenInfo(android.content.ComponentName, java.lang.CharSequence);
     method public void setEndUserSessionMessage(android.content.ComponentName, java.lang.CharSequence);
+    method public void setGlobalPrivateDns(android.content.ComponentName, int, java.lang.String);
     method public void setGlobalSetting(android.content.ComponentName, java.lang.String, java.lang.String);
     method public void setKeepUninstalledPackages(android.content.ComponentName, java.util.List<java.lang.String>);
     method public boolean setKeyPairCertificate(android.content.ComponentName, java.lang.String, java.util.List<java.security.cert.Certificate>, boolean);
@@ -6801,6 +6804,10 @@
     field public static final int PERMISSION_POLICY_PROMPT = 0; // 0x0
     field public static final java.lang.String POLICY_DISABLE_CAMERA = "policy_disable_camera";
     field public static final java.lang.String POLICY_DISABLE_SCREEN_CAPTURE = "policy_disable_screen_capture";
+    field public static final int PRIVATE_DNS_MODE_OFF = 1; // 0x1
+    field public static final int PRIVATE_DNS_MODE_OPPORTUNISTIC = 2; // 0x2
+    field public static final int PRIVATE_DNS_MODE_PROVIDER_HOSTNAME = 3; // 0x3
+    field public static final int PRIVATE_DNS_MODE_UNKNOWN = 0; // 0x0
     field public static final int RESET_PASSWORD_DO_NOT_ASK_CREDENTIALS_ON_BOOT = 2; // 0x2
     field public static final int RESET_PASSWORD_REQUIRE_ENTRY = 1; // 0x1
     field public static final int SKIP_SETUP_WIZARD = 1; // 0x1
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 74fb4df..b289a3e 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -1898,6 +1898,36 @@
     public static final String ACTION_PROFILE_OWNER_CHANGED =
             "android.app.action.PROFILE_OWNER_CHANGED";
 
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(flag = true, prefix = {"PRIVATE_DNS_MODE_"}, value = {
+            PRIVATE_DNS_MODE_UNKNOWN,
+            PRIVATE_DNS_MODE_OFF,
+            PRIVATE_DNS_MODE_OPPORTUNISTIC,
+            PRIVATE_DNS_MODE_PROVIDER_HOSTNAME
+    })
+    public @interface PrivateDnsMode {}
+
+    /**
+     * Specifies that the Private DNS setting is in an unknown state.
+     */
+    public static final int PRIVATE_DNS_MODE_UNKNOWN = 0;
+
+    /**
+     * Specifies that Private DNS was turned off completely.
+     */
+    public static final int PRIVATE_DNS_MODE_OFF = 1;
+
+    /**
+     * Specifies that the device owner requested opportunistic DNS over TLS
+     */
+    public static final int PRIVATE_DNS_MODE_OPPORTUNISTIC = 2;
+
+    /**
+     * Specifies that the device owner configured a specific host to use for Private DNS.
+     */
+    public static final int PRIVATE_DNS_MODE_PROVIDER_HOSTNAME = 3;
+
     /**
      * Return true if the given administrator component is currently active (enabled) in the system.
      *
@@ -9754,4 +9784,80 @@
             throw re.rethrowFromSystemServer();
         }
     }
+
+
+    /**
+     * Sets the global Private DNS mode and host to be used.
+     * May only be called by the device owner.
+     *
+     * @param admin which {@link DeviceAdminReceiver} this request is associated with.
+     * @param mode Which mode to set - either {@code PRIVATE_DNS_MODE_OPPORTUNISTIC} or
+     *             {@code PRIVATE_DNS_MODE_PROVIDER_HOSTNAME}.
+     *             Since the opportunistic mode defaults to ordinary DNS lookups, the
+     *             option to turn it completely off is not available, so this method
+     *             may not be called with {@code PRIVATE_DNS_MODE_OFF}.
+     * @param privateDnsHost The hostname of a server that implements DNS over TLS (RFC7858), if
+     *                       {@code PRIVATE_DNS_MODE_PROVIDER_HOSTNAME} was specified as the mode,
+     *                       null otherwise.
+     * @throws IllegalArgumentException in the following cases: if a {@code privateDnsHost} was
+     * provided but the mode was not {@code PRIVATE_DNS_MODE_PROVIDER_HOSTNAME}, if the mode
+     * specified was {@code PRIVATE_DNS_MODE_PROVIDER_HOSTNAME} but {@code privateDnsHost} does
+     * not look like a valid hostname, or if the mode specified is not one of the two valid modes.
+     *
+     * @throws SecurityException if the caller is not the device owner.
+     */
+    public void setGlobalPrivateDns(@NonNull ComponentName admin,
+            @PrivateDnsMode int mode, @Nullable String privateDnsHost) {
+        throwIfParentInstance("setGlobalPrivateDns");
+        if (mService == null) {
+            return;
+        }
+
+        try {
+            mService.setGlobalPrivateDns(admin, mode, privateDnsHost);
+        } catch (RemoteException re) {
+            throw re.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns the system-wide Private DNS mode.
+     *
+     * @param admin which {@link DeviceAdminReceiver} this request is associated with.
+     * @return one of {@code PRIVATE_DNS_MODE_OFF}, {@code PRIVATE_DNS_MODE_OPPORTUNISTIC},
+     * {@code PRIVATE_DNS_MODE_PROVIDER_HOSTNAME} or {@code PRIVATE_DNS_MODE_UNKNOWN}.
+     * @throws SecurityException if the caller is not the device owner.
+     */
+    public int getGlobalPrivateDnsMode(@NonNull ComponentName admin) {
+        throwIfParentInstance("setGlobalPrivateDns");
+        if (mService == null) {
+            return PRIVATE_DNS_MODE_UNKNOWN;
+        }
+
+        try {
+            return mService.getGlobalPrivateDnsMode(admin);
+        } catch (RemoteException re) {
+            throw re.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns the system-wide Private DNS host.
+     *
+     * @param admin which {@link DeviceAdminReceiver} this request is associated with.
+     * @return The hostname used for Private DNS queries.
+     * @throws SecurityException if the caller is not the device owner.
+     */
+    public String getGlobalPrivateDnsHost(@NonNull ComponentName admin) {
+        throwIfParentInstance("setGlobalPrivateDns");
+        if (mService == null) {
+            return null;
+        }
+
+        try {
+            return mService.getGlobalPrivateDnsHost(admin);
+        } catch (RemoteException re) {
+            throw re.rethrowFromSystemServer();
+        }
+    }
 }
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index 5e45450..cf0cad8 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -413,4 +413,8 @@
     boolean isOverrideApnEnabled(in ComponentName admin);
 
     boolean isMeteredDataDisabledPackageForUser(in ComponentName admin, String packageName, int userId);
+
+    void setGlobalPrivateDns(in ComponentName admin, int mode, in String privateDnsHost);
+    int getGlobalPrivateDnsMode(in ComponentName admin);
+    String getGlobalPrivateDnsHost(in ComponentName admin);
 }
diff --git a/core/java/android/net/NetworkUtils.java b/core/java/android/net/NetworkUtils.java
index 34e9476..c431e40e 100644
--- a/core/java/android/net/NetworkUtils.java
+++ b/core/java/android/net/NetworkUtils.java
@@ -16,8 +16,13 @@
 
 package android.net;
 
+import static android.system.OsConstants.AF_INET;
+import static android.system.OsConstants.AF_INET6;
+
+import android.annotation.NonNull;
 import android.annotation.UnsupportedAppUsage;
 import android.os.Parcel;
+import android.system.Os;
 import android.util.Log;
 import android.util.Pair;
 
@@ -570,4 +575,30 @@
         }
         return routedIPCount;
     }
+
+    private static final int[] ADDRESS_FAMILIES = new int[] {AF_INET, AF_INET6};
+
+    /**
+     * Returns true if the hostname is weakly validated.
+     * @param hostname Name of host to validate.
+     * @return True if it's a valid-ish hostname.
+     *
+     * @hide
+     */
+    public static boolean isWeaklyValidatedHostname(@NonNull String hostname) {
+        // TODO(b/34953048): Use a validation method that permits more accurate,
+        // but still inexpensive, checking of likely valid DNS hostnames.
+        final String weakHostnameRegex = "^[a-zA-Z0-9_.-]+$";
+        if (!hostname.matches(weakHostnameRegex)) {
+            return false;
+        }
+
+        for (int address_family : ADDRESS_FAMILIES) {
+            if (Os.inet_pton(address_family, hostname) != null) {
+                return false;
+            }
+        }
+
+        return true;
+    }
 }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/BaseIDevicePolicyManager.java b/services/devicepolicy/java/com/android/server/devicepolicy/BaseIDevicePolicyManager.java
index 4350596..b06be1a 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/BaseIDevicePolicyManager.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/BaseIDevicePolicyManager.java
@@ -15,7 +15,9 @@
  */
 package com.android.server.devicepolicy;
 
+import android.app.admin.DevicePolicyManager;
 import android.app.admin.IDevicePolicyManager;
+import android.content.ComponentName;
 
 import com.android.server.SystemService;
 
@@ -71,4 +73,18 @@
     public boolean checkDeviceIdentifierAccess(String packageName, int userHandle) {
         return false;
     }
+
+    @Override
+    public void setGlobalPrivateDns(ComponentName who, int mode, String privateDnsHost) {
+    }
+
+    @Override
+    public int getGlobalPrivateDnsMode(ComponentName who) {
+        return DevicePolicyManager.PRIVATE_DNS_MODE_UNKNOWN;
+    }
+
+    @Override
+    public String getGlobalPrivateDnsHost(ComponentName who) {
+        return null;
+    }
 }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 913b844..70cdba2 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -55,12 +55,18 @@
 import static android.app.admin.DevicePolicyManager.LOCK_TASK_FEATURE_OVERVIEW;
 import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_COMPLEX;
 import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED;
+import static android.app.admin.DevicePolicyManager.PRIVATE_DNS_MODE_UNKNOWN;
+import static android.app.admin.DevicePolicyManager.PRIVATE_DNS_MODE_OFF;
+import static android.app.admin.DevicePolicyManager.PRIVATE_DNS_MODE_OPPORTUNISTIC;
+import static android.app.admin.DevicePolicyManager.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME;
 import static android.app.admin.DevicePolicyManager.PROFILE_KEYGUARD_FEATURES_AFFECT_OWNER;
 import static android.app.admin.DevicePolicyManager.WIPE_EUICC;
 import static android.app.admin.DevicePolicyManager.WIPE_EXTERNAL_STORAGE;
 import static android.app.admin.DevicePolicyManager.WIPE_RESET_PROTECTION_DATA;
 import static android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES;
 
+import static android.provider.Settings.Global.PRIVATE_DNS_MODE;
+import static android.provider.Settings.Global.PRIVATE_DNS_SPECIFIER;
 import static android.provider.Telephony.Carriers.DPC_URI;
 import static android.provider.Telephony.Carriers.ENFORCE_KEY;
 import static android.provider.Telephony.Carriers.ENFORCE_MANAGED_URI;
@@ -145,6 +151,7 @@
 import android.media.IAudioService;
 import android.net.ConnectivityManager;
 import android.net.IIpConnectivityMetrics;
+import android.net.NetworkUtils;
 import android.net.ProxyInfo;
 import android.net.Uri;
 import android.net.metrics.IpConnectivityLog;
@@ -395,6 +402,8 @@
         GLOBAL_SETTINGS_WHITELIST.add(Settings.Global.WIFI_SLEEP_POLICY);
         GLOBAL_SETTINGS_WHITELIST.add(Settings.Global.STAY_ON_WHILE_PLUGGED_IN);
         GLOBAL_SETTINGS_WHITELIST.add(Settings.Global.WIFI_DEVICE_OWNER_CONFIGS_LOCKDOWN);
+        GLOBAL_SETTINGS_WHITELIST.add(Settings.Global.PRIVATE_DNS_MODE);
+        GLOBAL_SETTINGS_WHITELIST.add(Settings.Global.PRIVATE_DNS_SPECIFIER);
 
         GLOBAL_SETTINGS_DEPRECATED = new ArraySet<>();
         GLOBAL_SETTINGS_DEPRECATED.add(Settings.Global.BLUETOOTH_ON);
@@ -13114,4 +13123,78 @@
     private static String getManagedProvisioningPackage(Context context) {
         return context.getResources().getString(R.string.config_managed_provisioning_package);
     }
+
+    private void putPrivateDnsSettings(@Nullable String mode, @Nullable String host) {
+        // Set Private DNS settings using system permissions, as apps cannot write
+        // to global settings.
+        long origId = mInjector.binderClearCallingIdentity();
+        try {
+            mInjector.settingsGlobalPutString(PRIVATE_DNS_MODE, mode);
+            mInjector.settingsGlobalPutString(PRIVATE_DNS_SPECIFIER, host);
+        } finally {
+            mInjector.binderRestoreCallingIdentity(origId);
+        }
+    }
+
+    @Override
+    public void setGlobalPrivateDns(@NonNull ComponentName who, int mode, String privateDnsHost) {
+        if (!mHasFeature) {
+            return;
+        }
+
+        Preconditions.checkNotNull(who, "ComponentName is null");
+        enforceDeviceOwner(who);
+
+        switch (mode) {
+            case PRIVATE_DNS_MODE_OPPORTUNISTIC:
+                if (!TextUtils.isEmpty(privateDnsHost)) {
+                    throw new IllegalArgumentException("A DNS host should not be provided when " +
+                            "setting opportunistic mode.");
+                }
+                putPrivateDnsSettings(ConnectivityManager.PRIVATE_DNS_MODE_OPPORTUNISTIC, null);
+                break;
+            case PRIVATE_DNS_MODE_PROVIDER_HOSTNAME:
+                if (!NetworkUtils.isWeaklyValidatedHostname(privateDnsHost)) {
+                    throw new IllegalArgumentException(
+                            String.format("Provided hostname is not valid: %s", privateDnsHost));
+                }
+                putPrivateDnsSettings(ConnectivityManager.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME,
+                        privateDnsHost);
+                break;
+            default:
+                throw new IllegalArgumentException(String.format("Unsupported mode: %d", mode));
+        }
+    }
+
+    @Override
+    public int getGlobalPrivateDnsMode(@NonNull ComponentName who) {
+        if (!mHasFeature) {
+            return PRIVATE_DNS_MODE_UNKNOWN;
+        }
+
+        Preconditions.checkNotNull(who, "ComponentName is null");
+        enforceDeviceOwner(who);
+        switch (mInjector.settingsGlobalGetString(PRIVATE_DNS_MODE)) {
+            case ConnectivityManager.PRIVATE_DNS_MODE_OFF:
+                return PRIVATE_DNS_MODE_OFF;
+            case ConnectivityManager.PRIVATE_DNS_MODE_OPPORTUNISTIC:
+                return PRIVATE_DNS_MODE_OPPORTUNISTIC;
+            case ConnectivityManager.PRIVATE_DNS_MODE_PROVIDER_HOSTNAME:
+                return PRIVATE_DNS_MODE_PROVIDER_HOSTNAME;
+        }
+
+        return PRIVATE_DNS_MODE_UNKNOWN;
+    }
+
+    @Override
+    public String getGlobalPrivateDnsHost(@NonNull ComponentName who) {
+        if (!mHasFeature) {
+            return null;
+        }
+
+        Preconditions.checkNotNull(who, "ComponentName is null");
+        enforceDeviceOwner(who);
+
+        return mInjector.settingsGlobalGetString(PRIVATE_DNS_SPECIFIER);
+    }
 }