Always-on app VPNs

Bug: 22547950
Change-Id: I46b204170bfac58d944f39b22f815b080de71a58
diff --git a/api/current.txt b/api/current.txt
index 94bb21a..99c5b64 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -5746,6 +5746,7 @@
     method public int enableSystemApp(android.content.ComponentName, android.content.Intent);
     method public java.lang.String[] getAccountTypesWithManagementDisabled();
     method public java.util.List<android.content.ComponentName> getActiveAdmins();
+    method public java.lang.String getAlwaysOnVpnPackage(android.content.ComponentName);
     method public android.os.Bundle getApplicationRestrictions(android.content.ComponentName, java.lang.String);
     method public java.lang.String getApplicationRestrictionsManagingPackage(android.content.ComponentName);
     method public boolean getAutoTimeRequired();
@@ -5806,6 +5807,7 @@
     method public boolean removeUser(android.content.ComponentName, android.os.UserHandle);
     method public boolean resetPassword(java.lang.String, int);
     method public void setAccountManagementDisabled(android.content.ComponentName, java.lang.String, boolean);
+    method public boolean setAlwaysOnVpnPackage(android.content.ComponentName, java.lang.String);
     method public boolean setApplicationHidden(android.content.ComponentName, java.lang.String, boolean);
     method public void setApplicationRestrictions(android.content.ComponentName, java.lang.String, android.os.Bundle);
     method public void setApplicationRestrictionsManagingPackage(android.content.ComponentName, java.lang.String);
diff --git a/api/system-current.txt b/api/system-current.txt
index 79e44ea..09172a8 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -5871,6 +5871,7 @@
     method public int enableSystemApp(android.content.ComponentName, android.content.Intent);
     method public java.lang.String[] getAccountTypesWithManagementDisabled();
     method public java.util.List<android.content.ComponentName> getActiveAdmins();
+    method public java.lang.String getAlwaysOnVpnPackage(android.content.ComponentName);
     method public android.os.Bundle getApplicationRestrictions(android.content.ComponentName, java.lang.String);
     method public java.lang.String getApplicationRestrictionsManagingPackage(android.content.ComponentName);
     method public boolean getAutoTimeRequired();
@@ -5940,6 +5941,7 @@
     method public boolean resetPassword(java.lang.String, int);
     method public void setAccountManagementDisabled(android.content.ComponentName, java.lang.String, boolean);
     method public deprecated boolean setActiveProfileOwner(android.content.ComponentName, java.lang.String) throws java.lang.IllegalArgumentException;
+    method public boolean setAlwaysOnVpnPackage(android.content.ComponentName, java.lang.String);
     method public boolean setApplicationHidden(android.content.ComponentName, java.lang.String, boolean);
     method public void setApplicationRestrictions(android.content.ComponentName, java.lang.String, android.os.Bundle);
     method public void setApplicationRestrictionsManagingPackage(android.content.ComponentName, java.lang.String);
diff --git a/api/test-current.txt b/api/test-current.txt
index 090644b..6f41c84 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -5746,6 +5746,7 @@
     method public int enableSystemApp(android.content.ComponentName, android.content.Intent);
     method public java.lang.String[] getAccountTypesWithManagementDisabled();
     method public java.util.List<android.content.ComponentName> getActiveAdmins();
+    method public java.lang.String getAlwaysOnVpnPackage(android.content.ComponentName);
     method public android.os.Bundle getApplicationRestrictions(android.content.ComponentName, java.lang.String);
     method public java.lang.String getApplicationRestrictionsManagingPackage(android.content.ComponentName);
     method public boolean getAutoTimeRequired();
@@ -5806,6 +5807,7 @@
     method public boolean removeUser(android.content.ComponentName, android.os.UserHandle);
     method public boolean resetPassword(java.lang.String, int);
     method public void setAccountManagementDisabled(android.content.ComponentName, java.lang.String, boolean);
+    method public boolean setAlwaysOnVpnPackage(android.content.ComponentName, java.lang.String);
     method public boolean setApplicationHidden(android.content.ComponentName, java.lang.String, boolean);
     method public void setApplicationRestrictions(android.content.ComponentName, java.lang.String, android.os.Bundle);
     method public void setApplicationRestrictionsManagingPackage(android.content.ComponentName, java.lang.String);
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index eda0982..ad4e450 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -2415,6 +2415,53 @@
     }
 
     /**
+     * Called by a device or profile owner to configure an always-on VPN connection through a
+     * specific application for the current user.
+     * This connection is automatically granted and persisted after a reboot.
+     *
+     * <p>The designated package should declare a {@link android.net.VpnService} in its
+     *    manifest guarded by {@link android.Manifest.permission#BIND_VPN_SERVICE},
+     *    otherwise the call will fail.
+     *
+     * @param vpnPackage The package name for an installed VPN app on the device, or {@code null}
+     *                   to remove an existing always-on VPN configuration.
+     *
+     * @return {@code true} if the package is set as always-on VPN controller;
+     *         {@code false} otherwise.
+     */
+    public boolean setAlwaysOnVpnPackage(@NonNull ComponentName admin,
+            @Nullable String vpnPackage) {
+        if (mService != null) {
+            try {
+                return mService.setAlwaysOnVpnPackage(admin, vpnPackage);
+            } catch (RemoteException e) {
+                Log.w(TAG, REMOTE_EXCEPTION_MESSAGE, e);
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Called by a device or profile owner to read the name of the package administering an
+     * always-on VPN connection for the current user.
+     * If there is no such package, or the always-on VPN is provided by the system instead
+     * of by an application, {@code null} will be returned.
+     *
+     * @return Package name of VPN controller responsible for always-on VPN,
+     *         or {@code null} if none is set.
+     */
+    public String getAlwaysOnVpnPackage(@NonNull ComponentName admin) {
+        if (mService != null) {
+            try {
+                return mService.getAlwaysOnVpnPackage(admin);
+            } catch (RemoteException e) {
+                Log.w(TAG, REMOTE_EXCEPTION_MESSAGE, e);
+            }
+        }
+        return null;
+    }
+
+    /**
      * Called by an application that is administering the device to disable all cameras
      * on the device, for this user. After setting this, no applications running as this user
      * will be able to access any cameras on the device.
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index 30ce682..fb01dfb 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -144,6 +144,9 @@
     void setCertInstallerPackage(in ComponentName who, String installerPackage);
     String getCertInstallerPackage(in ComponentName who);
 
+    boolean setAlwaysOnVpnPackage(in ComponentName who, String vpnPackage);
+    String getAlwaysOnVpnPackage(in ComponentName who);
+
     void addPersistentPreferredActivity(in ComponentName admin, in IntentFilter filter, in ComponentName activity);
     void clearPackagePersistentPreferredActivities(in ComponentName admin, String packageName);
 
diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java
index 515e9a2..523f4c2 100644
--- a/core/java/android/net/ConnectivityManager.java
+++ b/core/java/android/net/ConnectivityManager.java
@@ -17,6 +17,7 @@
 
 import static com.android.internal.util.Preconditions.checkNotNull;
 
+import android.annotation.Nullable;
 import android.annotation.SdkConstant;
 import android.annotation.SdkConstant.SdkConstantType;
 import android.app.PendingIntent;
@@ -686,6 +687,47 @@
     }
 
     /**
+     * Configures an always-on VPN connection through a specific application.
+     * This connection is automatically granted and persisted after a reboot.
+     *
+     * <p>The designated package should declare a {@link VpnService} in its
+     *    manifest guarded by {@link android.Manifest.permission.BIND_VPN_SERVICE},
+     *    otherwise the call will fail.
+     *
+     * @param userId The identifier of the user to set an always-on VPN for.
+     * @param vpnPackage The package name for an installed VPN app on the device, or {@code null}
+     *                   to remove an existing always-on VPN configuration.
+
+     * @return {@code true} if the package is set as always-on VPN controller;
+     *         {@code false} otherwise.
+     * @hide
+     */
+    public boolean setAlwaysOnVpnPackageForUser(int userId, @Nullable String vpnPackage) {
+        try {
+            return mService.setAlwaysOnVpnPackage(userId, vpnPackage);
+        } catch (RemoteException e) {
+            return false;
+        }
+    }
+
+    /**
+     * Returns the package name of the currently set always-on VPN application.
+     * If there is no always-on VPN set, or the VPN is provided by the system instead
+     * of by an app, {@code null} will be returned.
+     *
+     * @return Package name of VPN controller responsible for always-on VPN,
+     *         or {@code null} if none is set.
+     * @hide
+     */
+    public String getAlwaysOnVpnPackageForUser(int userId) {
+        try {
+            return mService.getAlwaysOnVpnPackage(userId);
+        } catch (RemoteException e) {
+            return null;
+        }
+    }
+
+    /**
      * Returns details about the currently active default data network
      * for a given uid.  This is for internal use only to avoid spying
      * other apps.
diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl
index d4dd669..d018731 100644
--- a/core/java/android/net/IConnectivityManager.aidl
+++ b/core/java/android/net/IConnectivityManager.aidl
@@ -117,6 +117,8 @@
     VpnInfo[] getAllVpnInfo();
 
     boolean updateLockdownVpn();
+    boolean setAlwaysOnVpnPackage(int userId, String packageName);
+    String getAlwaysOnVpnPackage(int userId);
 
     int checkMobileProvisioning(int suggestedTimeOutMs);
 
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 4eaee0b..6eacafb 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -4402,6 +4402,13 @@
         public static final String HTTP_PROXY = Global.HTTP_PROXY;
 
         /**
+         * Package designated as always-on VPN provider.
+         *
+         * @hide
+         */
+        public static final String ALWAYS_ON_VPN_APP = "always_on_vpn_app";
+
+        /**
          * Whether applications can be installed for this user via the system's
          * {@link Intent#ACTION_INSTALL_PACKAGE} mechanism.
          *
diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java
index 65a27c8..2bb9370 100644
--- a/services/core/java/com/android/server/ConnectivityService.java
+++ b/services/core/java/com/android/server/ConnectivityService.java
@@ -1568,12 +1568,11 @@
         // load the global proxy at startup
         mHandler.sendMessage(mHandler.obtainMessage(EVENT_APPLY_GLOBAL_HTTP_PROXY));
 
-        // Try bringing up tracker, but if KeyStore isn't ready yet, wait
-        // for user to unlock device.
-        if (!updateLockdownVpn()) {
-            final IntentFilter filter = new IntentFilter(Intent.ACTION_USER_PRESENT);
-            mContext.registerReceiver(mUserPresentReceiver, filter);
-        }
+        // Try bringing up tracker, but KeyStore won't be ready yet for secondary users so wait
+        // for user to unlock device too.
+        updateLockdownVpn();
+        final IntentFilter filter = new IntentFilter(Intent.ACTION_USER_PRESENT);
+        mContext.registerReceiverAsUser(mUserPresentReceiver, UserHandle.ALL, filter, null, null);
 
         // Configure whether mobile data is always on.
         mHandler.sendMessage(mHandler.obtainMessage(EVENT_CONFIGURE_MOBILE_DATA_ALWAYS_ON));
@@ -1586,10 +1585,16 @@
     private BroadcastReceiver mUserPresentReceiver = new BroadcastReceiver() {
         @Override
         public void onReceive(Context context, Intent intent) {
+            // User that sent this intent = user that was just unlocked
+            final int unlockedUser = getSendingUserId();
+
             // Try creating lockdown tracker, since user present usually means
             // unlocked keystore.
-            if (updateLockdownVpn()) {
-                mContext.unregisterReceiver(this);
+            if (mUserManager.getUserInfo(unlockedUser).isPrimary() &&
+                    LockdownVpnTracker.isEnabled()) {
+                updateLockdownVpn();
+            } else {
+                updateAlwaysOnVpn(unlockedUser);
             }
         }
     };
@@ -3258,6 +3263,76 @@
         }
     }
 
+    /**
+     * Sets up or tears down the always-on VPN for user {@param user} as appropriate.
+     *
+     * @return {@code false} in case of errors; {@code true} otherwise.
+     */
+    private boolean updateAlwaysOnVpn(int user) {
+        final String lockdownPackage = getAlwaysOnVpnPackage(user);
+        if (lockdownPackage == null) {
+            return true;
+        }
+
+        // Create an intent to start the VPN service declared in the app's manifest.
+        Intent serviceIntent = new Intent(VpnConfig.SERVICE_INTERFACE);
+        serviceIntent.setPackage(lockdownPackage);
+
+        try {
+            return mContext.startServiceAsUser(serviceIntent, UserHandle.of(user)) != null;
+        } catch (RuntimeException e) {
+            return false;
+        }
+    }
+
+    @Override
+    public boolean setAlwaysOnVpnPackage(int userId, String packageName) {
+        enforceConnectivityInternalPermission();
+        enforceCrossUserPermission(userId);
+
+        // Can't set always-on VPN if legacy VPN is already in lockdown mode.
+        if (LockdownVpnTracker.isEnabled()) {
+            return false;
+        }
+
+        // If the current VPN package is the same as the new one, this is a no-op
+        final String oldPackage = getAlwaysOnVpnPackage(userId);
+        if (TextUtils.equals(oldPackage, packageName)) {
+            return true;
+        }
+
+        synchronized (mVpns) {
+            Vpn vpn = mVpns.get(userId);
+            if (vpn == null) {
+                Slog.w(TAG, "User " + userId + " has no Vpn configuration");
+                return false;
+            }
+            if (!vpn.setAlwaysOnPackage(packageName)) {
+                return false;
+            }
+            if (!updateAlwaysOnVpn(userId)) {
+                vpn.setAlwaysOnPackage(null);
+                return false;
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public String getAlwaysOnVpnPackage(int userId) {
+        enforceConnectivityInternalPermission();
+        enforceCrossUserPermission(userId);
+
+        synchronized (mVpns) {
+            Vpn vpn = mVpns.get(userId);
+            if (vpn == null) {
+                Slog.w(TAG, "User " + userId + " has no Vpn configuration");
+                return null;
+            }
+            return vpn.getAlwaysOnPackage();
+        }
+    }
+
     @Override
     public int checkMobileProvisioning(int suggestedTimeOutMs) {
         // TODO: Remove?  Any reason to trigger a provisioning check?
diff --git a/services/core/java/com/android/server/connectivity/Vpn.java b/services/core/java/com/android/server/connectivity/Vpn.java
index 2bea278..2600d70 100644
--- a/services/core/java/com/android/server/connectivity/Vpn.java
+++ b/services/core/java/com/android/server/connectivity/Vpn.java
@@ -63,6 +63,7 @@
 import android.os.SystemService;
 import android.os.UserHandle;
 import android.os.UserManager;
+import android.provider.Settings;
 import android.security.Credentials;
 import android.security.KeyStore;
 import android.text.TextUtils;
@@ -169,6 +170,58 @@
     }
 
     /**
+     * Configures an always-on VPN connection through a specific application.
+     * This connection is automatically granted and persisted after a reboot.
+     *
+     * <p>The designated package should exist and declare a {@link VpnService} in its
+     *    manifest guarded by {@link android.Manifest.permission.BIND_VPN_SERVICE},
+     *    otherwise the call will fail.
+     *
+     * @param newPackage the package to designate as always-on VPN supplier.
+     */
+    public synchronized boolean setAlwaysOnPackage(String packageName) {
+        enforceControlPermissionOrInternalCaller();
+
+        // Disconnect current VPN.
+        prepareInternal(VpnConfig.LEGACY_VPN);
+
+        // Pre-authorize new always-on VPN package.
+        if (packageName != null) {
+            if (!setPackageAuthorization(packageName, true)) {
+                return false;
+            }
+        }
+
+        // Save the new package name in Settings.Secure.
+        final long token = Binder.clearCallingIdentity();
+        try {
+            Settings.Secure.putStringForUser(mContext.getContentResolver(),
+                    Settings.Secure.ALWAYS_ON_VPN_APP, packageName, mUserHandle);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+        return true;
+    }
+
+    /**
+     * @return the package name of the VPN controller responsible for always-on VPN,
+     *         or {@code null} if none is set or always-on VPN is controlled through
+     *         lockdown instead.
+     * @hide
+     */
+    public synchronized String getAlwaysOnPackage() {
+        enforceControlPermissionOrInternalCaller();
+
+        final long token = Binder.clearCallingIdentity();
+        try {
+            return Settings.Secure.getStringForUser(mContext.getContentResolver(),
+                    Settings.Secure.ALWAYS_ON_VPN_APP, mUserHandle);
+        } finally {
+            Binder.restoreCallingIdentity(token);
+        }
+    }
+
+    /**
      * Prepare for a VPN application. This method is designed to solve
      * race conditions. It first compares the current prepared package
      * with {@code oldPackage}. If they are the same, the prepared
@@ -270,14 +323,14 @@
     /**
      * Set whether a package has the ability to launch VPNs without user intervention.
      */
-    public void setPackageAuthorization(String packageName, boolean authorized) {
+    public boolean setPackageAuthorization(String packageName, boolean authorized) {
         // Check if the caller is authorized.
-        enforceControlPermission();
+        enforceControlPermissionOrInternalCaller();
 
         int uid = getAppUid(packageName, mUserHandle);
         if (uid == -1 || VpnConfig.LEGACY_VPN.equals(packageName)) {
             // Authorization for nonexistent packages (or fake ones) can't be updated.
-            return;
+            return false;
         }
 
         long token = Binder.clearCallingIdentity();
@@ -286,11 +339,13 @@
                     (AppOpsManager) mContext.getSystemService(Context.APP_OPS_SERVICE);
             appOps.setMode(AppOpsManager.OP_ACTIVATE_VPN, uid, packageName,
                     authorized ? AppOpsManager.MODE_ALLOWED : AppOpsManager.MODE_IGNORED);
+            return true;
         } catch (Exception e) {
             Log.wtf(TAG, "Failed to set app ops for package " + packageName + ", uid " + uid, e);
         } finally {
             Binder.restoreCallingIdentity(token);
         }
+        return false;
     }
 
     private boolean isVpnUserPreConsented(String packageName) {
@@ -743,6 +798,13 @@
         mContext.enforceCallingPermission(Manifest.permission.CONTROL_VPN, "Unauthorized Caller");
     }
 
+    private void enforceControlPermissionOrInternalCaller() {
+        // Require caller to be either an application with CONTROL_VPN permission or a process
+        // in the system server.
+        mContext.enforceCallingOrSelfPermission(Manifest.permission.CONTROL_VPN,
+                "Unauthorized Caller");
+    }
+
     private class Connection implements ServiceConnection {
         private IBinder mService;
 
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index cf661ce..bde57f0 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -3782,6 +3782,42 @@
         }
     }
 
+    @Override
+    public boolean setAlwaysOnVpnPackage(ComponentName admin, String vpnPackage)
+            throws SecurityException {
+        synchronized (this) {
+            getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
+        }
+
+        final int userId = mInjector.userHandleGetCallingUserId();
+        final long token = mInjector.binderClearCallingIdentity();
+        try{
+            ConnectivityManager connectivityManager = (ConnectivityManager)
+                    mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+            return connectivityManager.setAlwaysOnVpnPackageForUser(userId, vpnPackage);
+        } finally {
+            mInjector.binderRestoreCallingIdentity(token);
+        }
+    }
+
+    @Override
+    public String getAlwaysOnVpnPackage(ComponentName admin)
+            throws SecurityException {
+        synchronized (this) {
+            getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_PROFILE_OWNER);
+        }
+
+        final int userId = mInjector.userHandleGetCallingUserId();
+        final long token = mInjector.binderClearCallingIdentity();
+        try{
+            ConnectivityManager connectivityManager = (ConnectivityManager)
+                    mContext.getSystemService(Context.CONNECTIVITY_SERVICE);
+            return connectivityManager.getAlwaysOnVpnPackageForUser(userId);
+        } finally {
+            mInjector.binderRestoreCallingIdentity(token);
+        }
+    }
+
     private void wipeDataLocked(boolean wipeExtRequested, String reason) {
         if (wipeExtRequested) {
             StorageManager sm = (StorageManager) mContext.getSystemService(