New management API for making backups mandatory.

Let the device owner make backups with a chosen backup
transport mandatory.

BUG: 64012357
Test: make RunFrameworksServicesRoboTests
Test: manually together with the corresponding GmsCore change.
Test: cts-tradefed run cts -m CtsDevicePolicyManagerTestCases --test
com.android.cts.devicepolicy.DeviceOwnerTest#testGetAndSetMandatoryBackupTransport
Test: cts-tradefed run cts -m CtsBackupHostTestCase --test
android.cts.backup.BackupDeviceOwnerHostSideTest#testMandatoryBackupTransport

Change-Id: I9bfae5799beae3459659e697813b75a9b508ae55
diff --git a/api/current.txt b/api/current.txt
index 8b0cb94..5d372fe 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -6403,6 +6403,7 @@
     method public int getLockTaskFeatures(android.content.ComponentName);
     method public java.lang.String[] getLockTaskPackages(android.content.ComponentName);
     method public java.lang.CharSequence getLongSupportMessage(android.content.ComponentName);
+    method public android.content.ComponentName getMandatoryBackupTransport();
     method public int getMaximumFailedPasswordsForWipe(android.content.ComponentName);
     method public long getMaximumTimeToLock(android.content.ComponentName);
     method public int getOrganizationColor(android.content.ComponentName);
@@ -6502,6 +6503,7 @@
     method public void setLockTaskPackages(android.content.ComponentName, java.lang.String[]) throws java.lang.SecurityException;
     method public void setLogoutEnabled(android.content.ComponentName, boolean);
     method public void setLongSupportMessage(android.content.ComponentName, java.lang.CharSequence);
+    method public void setMandatoryBackupTransport(android.content.ComponentName, android.content.ComponentName);
     method public void setMasterVolumeMuted(android.content.ComponentName, boolean);
     method public void setMaximumFailedPasswordsForWipe(android.content.ComponentName, int);
     method public void setMaximumTimeToLock(android.content.ComponentName, long);
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index e334aab..67b59f6 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -8633,6 +8633,13 @@
      *
      * <p> Backup service is off by default when device owner is present.
      *
+     * <p> If backups are made mandatory by specifying a non-null mandatory backup transport using
+     * the {@link DevicePolicyManager#setMandatoryBackupTransport} method, the backup service is
+     * automatically enabled.
+     *
+     * <p> If the backup service is disabled using this method after the mandatory backup transport
+     * has been set, the mandatory backup transport is cleared.
+     *
      * @param admin Which {@link DeviceAdminReceiver} this request is associated with.
      * @param enabled {@code true} to enable the backup service, {@code false} to disable it.
      * @throws SecurityException if {@code admin} is not a device owner.
@@ -8664,6 +8671,43 @@
     }
 
     /**
+     * Makes backups mandatory and enforces the usage of the specified backup transport.
+     *
+     * <p>When a {@code null} backup transport is specified, backups are made optional again.
+     * <p>Only device owner can call this method.
+     * <p>If backups were disabled and a non-null backup transport {@link ComponentName} is
+     * specified, backups will be enabled.
+     *
+     * @param admin admin Which {@link DeviceAdminReceiver} this request is associated with.
+     * @param backupTransportComponent The backup transport layer to be used for mandatory backups.
+     * @throws SecurityException if {@code admin} is not a device owner.
+     */
+    public void setMandatoryBackupTransport(
+            @NonNull ComponentName admin, @Nullable ComponentName backupTransportComponent) {
+        try {
+            mService.setMandatoryBackupTransport(admin, backupTransportComponent);
+        } catch (RemoteException re) {
+            throw re.rethrowFromSystemServer();
+        }
+    }
+
+    /**
+     * Returns the backup transport which has to be used for backups if backups are mandatory or
+     * {@code null} if backups are not mandatory.
+     *
+     * @return a {@link ComponentName} of the backup transport layer to be used if backups are
+     *         mandatory or {@code null} if backups are not mandatory.
+     */
+    public ComponentName getMandatoryBackupTransport() {
+        try {
+            return mService.getMandatoryBackupTransport();
+        } catch (RemoteException re) {
+            throw re.rethrowFromSystemServer();
+        }
+    }
+
+
+    /**
      * Called by a device owner to control the network logging feature.
      *
      * <p> Network logs contain DNS lookup and connect() library call events. The following library
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index 7154053..9cdd1f8 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -359,6 +359,8 @@
 
     void setBackupServiceEnabled(in ComponentName admin, boolean enabled);
     boolean isBackupServiceEnabled(in ComponentName admin);
+    void setMandatoryBackupTransport(in ComponentName admin, in ComponentName backupTransportComponent);
+    ComponentName getMandatoryBackupTransport();
 
     void setNetworkLoggingEnabled(in ComponentName admin, boolean enabled);
     boolean isNetworkLoggingEnabled(in ComponentName admin);
diff --git a/core/java/android/app/backup/IBackupManager.aidl b/core/java/android/app/backup/IBackupManager.aidl
index 792cb5f..f3ca746 100644
--- a/core/java/android/app/backup/IBackupManager.aidl
+++ b/core/java/android/app/backup/IBackupManager.aidl
@@ -294,7 +294,8 @@
      *
      * @param transport ComponentName of the service hosting the transport. This is different from
      *                  the transport's name that is returned by {@link BackupTransport#name()}.
-     * @param listener A listener object to get a callback on the transport being selected.
+     * @param listener A listener object to get a callback on the transport being selected. It may
+     *                 be {@code null}.
      *
      * @hide
      */
diff --git a/services/backup/java/com/android/server/backup/BackupPolicyEnforcer.java b/services/backup/java/com/android/server/backup/BackupPolicyEnforcer.java
new file mode 100644
index 0000000..158084a
--- /dev/null
+++ b/services/backup/java/com/android/server/backup/BackupPolicyEnforcer.java
@@ -0,0 +1,25 @@
+package com.android.server.backup;
+
+import android.app.admin.DevicePolicyManager;
+import android.content.ComponentName;
+import android.content.Context;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+/**
+ * A helper class to decouple this service from {@link DevicePolicyManager} in order to improve
+ * testability.
+ */
+@VisibleForTesting
+public class BackupPolicyEnforcer {
+    private DevicePolicyManager mDevicePolicyManager;
+
+    public BackupPolicyEnforcer(Context context) {
+        mDevicePolicyManager =
+                (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE);
+    }
+
+    public ComponentName getMandatoryBackupTransport() {
+        return mDevicePolicyManager.getMandatoryBackupTransport();
+    }
+}
diff --git a/services/backup/java/com/android/server/backup/RefactoredBackupManagerService.java b/services/backup/java/com/android/server/backup/RefactoredBackupManagerService.java
index 350d7af..f33ec55 100644
--- a/services/backup/java/com/android/server/backup/RefactoredBackupManagerService.java
+++ b/services/backup/java/com/android/server/backup/RefactoredBackupManagerService.java
@@ -38,6 +38,7 @@
 import android.app.IActivityManager;
 import android.app.IBackupAgent;
 import android.app.PendingIntent;
+import android.app.admin.DevicePolicyManager;
 import android.app.backup.BackupManager;
 import android.app.backup.BackupManagerMonitor;
 import android.app.backup.FullBackup;
@@ -681,6 +682,8 @@
     @GuardedBy("mQueueLock")
     private ArrayList<FullBackupEntry> mFullBackupQueue;
 
+    private BackupPolicyEnforcer mBackupPolicyEnforcer;
+
     // Utility: build a new random integer token. The low bits are the ordinal of the
     // operation for near-time uniqueness, and the upper bits are random for app-
     // side unpredictability.
@@ -872,6 +875,8 @@
 
         // Power management
         mWakelock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "*backup*");
+
+        mBackupPolicyEnforcer = new BackupPolicyEnforcer(context);
     }
 
     private void initPackageTracking() {
@@ -2774,6 +2779,10 @@
     public void setBackupEnabled(boolean enable) {
         mContext.enforceCallingOrSelfPermission(android.Manifest.permission.BACKUP,
                 "setBackupEnabled");
+        if (!enable && mBackupPolicyEnforcer.getMandatoryBackupTransport() != null) {
+            Slog.w(TAG, "Cannot disable backups when the mandatory backups policy is active.");
+            return;
+        }
 
         Slog.i(TAG, "Backup enabled => " + enable);
 
@@ -3009,6 +3018,12 @@
         mContext.enforceCallingOrSelfPermission(
                 android.Manifest.permission.BACKUP, "selectBackupTransport");
 
+        if (!isAllowedByMandatoryBackupTransportPolicy(transportName)) {
+            // Don't change the transport if it is not allowed.
+            Slog.w(TAG, "Failed to select transport - disallowed by device owner policy.");
+            return mTransportManager.getCurrentTransportName();
+        }
+
         final long oldId = Binder.clearCallingIdentity();
         try {
             String previousTransportName = mTransportManager.selectTransport(transportName);
@@ -3023,10 +3038,20 @@
 
     @Override
     public void selectBackupTransportAsync(
-            ComponentName transportComponent, ISelectBackupTransportCallback listener) {
+            ComponentName transportComponent, @Nullable ISelectBackupTransportCallback listener) {
         mContext.enforceCallingOrSelfPermission(
                 android.Manifest.permission.BACKUP, "selectBackupTransportAsync");
-
+        if (!isAllowedByMandatoryBackupTransportPolicy(transportComponent)) {
+            try {
+                if (listener != null) {
+                    Slog.w(TAG, "Failed to select transport - disallowed by device owner policy.");
+                    listener.onFailure(BackupManager.ERROR_BACKUP_NOT_ALLOWED);
+                }
+            } catch (RemoteException e) {
+                Slog.e(TAG, "ISelectBackupTransportCallback listener not available");
+            }
+            return;
+        }
         final long oldId = Binder.clearCallingIdentity();
         try {
             String transportString = transportComponent.flattenToShortString();
@@ -3048,10 +3073,12 @@
                         }
 
                         try {
-                            if (transportName != null) {
-                                listener.onSuccess(transportName);
-                            } else {
-                                listener.onFailure(result);
+                            if (listener != null) {
+                                if (transportName != null) {
+                                    listener.onSuccess(transportName);
+                                } else {
+                                    listener.onFailure(result);
+                                }
                             }
                         } catch (RemoteException e) {
                             Slog.e(TAG, "ISelectBackupTransportCallback listener not available");
@@ -3062,6 +3089,38 @@
         }
     }
 
+    /**
+     * Returns if the specified transport can be set as the current transport without violating the
+     * mandatory backup transport policy.
+     */
+    private boolean isAllowedByMandatoryBackupTransportPolicy(String transportName) {
+        ComponentName mandatoryBackupTransport = mBackupPolicyEnforcer.getMandatoryBackupTransport();
+        if (mandatoryBackupTransport == null) {
+            return true;
+        }
+        final String mandatoryBackupTransportName;
+        try {
+            mandatoryBackupTransportName =
+                    mTransportManager.getTransportName(mandatoryBackupTransport);
+        } catch (TransportNotRegisteredException e) {
+            Slog.e(TAG, "mandatory backup transport not registered!");
+            return false;
+        }
+        return TextUtils.equals(mandatoryBackupTransportName, transportName);
+    }
+
+    /**
+     * Returns if the specified transport can be set as the current transport without violating the
+     * mandatory backup transport policy.
+     */
+    private boolean isAllowedByMandatoryBackupTransportPolicy(ComponentName transport) {
+        ComponentName mandatoryBackupTransport = mBackupPolicyEnforcer.getMandatoryBackupTransport();
+        if (mandatoryBackupTransport == null) {
+            return true;
+        }
+        return mandatoryBackupTransport.equals(transport);
+    }
+
     private void updateStateForTransport(String newTransportName) {
         // Publish the name change
         Settings.Secure.putString(mContext.getContentResolver(),
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index bf2b137..38d423d 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -226,6 +226,7 @@
 import java.util.List;
 import java.util.Map.Entry;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 import java.util.concurrent.atomic.AtomicBoolean;
@@ -790,6 +791,7 @@
         private static final String ATTR_LAST_NETWORK_LOGGING_NOTIFICATION = "last-notification";
         private static final String ATTR_NUM_NETWORK_LOGGING_NOTIFICATIONS = "num-notifications";
         private static final String TAG_IS_LOGOUT_ENABLED = "is_logout_enabled";
+        private static final String TAG_MANDATORY_BACKUP_TRANSPORT = "mandatory_backup_transport";
 
         DeviceAdminInfo info;
 
@@ -906,6 +908,10 @@
         // The blacklist data is stored in a file whose name is stored in the XML
         String passwordBlacklistFile = null;
 
+        // The component name of the backup transport which has to be used if backups are mandatory
+        // or null if backups are not mandatory.
+        ComponentName mandatoryBackupTransport = null;
+
         ActiveAdmin(DeviceAdminInfo _info, boolean parent) {
             info = _info;
             isParent = parent;
@@ -1169,6 +1175,11 @@
                 out.attribute(null, ATTR_VALUE, Boolean.toString(isLogoutEnabled));
                 out.endTag(null, TAG_IS_LOGOUT_ENABLED);
             }
+            if (mandatoryBackupTransport != null) {
+                out.startTag(null, TAG_MANDATORY_BACKUP_TRANSPORT);
+                out.attribute(null, ATTR_VALUE, mandatoryBackupTransport.flattenToString());
+                out.endTag(null, TAG_MANDATORY_BACKUP_TRANSPORT);
+            }
         }
 
         void writePackageListToXml(XmlSerializer out, String outerTag,
@@ -1347,6 +1358,9 @@
                 } else if (TAG_IS_LOGOUT_ENABLED.equals(tag)) {
                     isLogoutEnabled = Boolean.parseBoolean(
                             parser.getAttributeValue(null, ATTR_VALUE));
+                } else if (TAG_MANDATORY_BACKUP_TRANSPORT.equals(tag)) {
+                    mandatoryBackupTransport = ComponentName.unflattenFromString(
+                            parser.getAttributeValue(null, ATTR_VALUE));
                 } else {
                     Slog.w(LOG_TAG, "Unknown admin tag: " + tag);
                     XmlUtils.skipCurrentTag(parser);
@@ -11337,7 +11351,12 @@
         }
         Preconditions.checkNotNull(admin);
         synchronized (this) {
-            getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
+            ActiveAdmin activeAdmin = getActiveAdminForCallerLocked(
+                    admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
+            if (!enabled) {
+                activeAdmin.mandatoryBackupTransport = null;
+                saveSettingsLocked(UserHandle.USER_SYSTEM);
+            }
         }
 
         final long ident = mInjector.binderClearCallingIdentity();
@@ -11372,6 +11391,50 @@
     }
 
     @Override
+    public void setMandatoryBackupTransport(
+            ComponentName admin, ComponentName backupTransportComponent) {
+        if (!mHasFeature) {
+            return;
+        }
+        Preconditions.checkNotNull(admin);
+        synchronized (this) {
+            ActiveAdmin activeAdmin =
+                    getActiveAdminForCallerLocked(admin, DeviceAdminInfo.USES_POLICY_DEVICE_OWNER);
+            if (!Objects.equals(backupTransportComponent, activeAdmin.mandatoryBackupTransport)) {
+                activeAdmin.mandatoryBackupTransport = backupTransportComponent;
+                saveSettingsLocked(UserHandle.USER_SYSTEM);
+            }
+        }
+        final long identity = mInjector.binderClearCallingIdentity();
+        try {
+            IBackupManager ibm = mInjector.getIBackupManager();
+            if (ibm != null && backupTransportComponent != null) {
+                if (!ibm.isBackupServiceActive(UserHandle.USER_SYSTEM)) {
+                    ibm.setBackupServiceActive(UserHandle.USER_SYSTEM, true);
+                }
+                ibm.selectBackupTransportAsync(backupTransportComponent, null);
+                ibm.setBackupEnabled(true);
+            }
+        } catch (RemoteException e) {
+            throw new IllegalStateException("Failed to set mandatory backup transport.", e);
+        } finally {
+            mInjector.binderRestoreCallingIdentity(identity);
+        }
+    }
+
+    @Override
+    public ComponentName getMandatoryBackupTransport() {
+        if (!mHasFeature) {
+            return null;
+        }
+        synchronized (this) {
+            ActiveAdmin activeAdmin = getDeviceOwnerAdminLocked();
+            return activeAdmin == null ? null : activeAdmin.mandatoryBackupTransport;
+        }
+    }
+
+
+    @Override
     public boolean bindDeviceAdminServiceAsUser(
             @NonNull ComponentName admin, @NonNull IApplicationThread caller,
             @Nullable IBinder activtiyToken, @NonNull Intent serviceIntent,
diff --git a/services/robotests/src/com/android/server/backup/BackupManagerServiceRoboTest.java b/services/robotests/src/com/android/server/backup/BackupManagerServiceRoboTest.java
index bf58224..dc0a4e3 100644
--- a/services/robotests/src/com/android/server/backup/BackupManagerServiceRoboTest.java
+++ b/services/robotests/src/com/android/server/backup/BackupManagerServiceRoboTest.java
@@ -44,7 +44,9 @@
 import android.provider.Settings;
 
 import com.android.server.backup.testing.ShadowAppBackupUtils;
+import com.android.server.backup.testing.ShadowBackupPolicyEnforcer;
 import com.android.server.backup.testing.TransportData;
+import com.android.server.backup.testing.TransportTestUtils;
 import com.android.server.backup.testing.TransportTestUtils.TransportMock;
 import com.android.server.backup.transport.TransportNotRegisteredException;
 import com.android.server.testing.FrameworkRobolectricTestRunner;
@@ -72,7 +74,7 @@
 @Config(
     manifest = Config.NONE,
     sdk = 26,
-    shadows = {ShadowAppBackupUtils.class}
+    shadows = {ShadowAppBackupUtils.class, ShadowBackupPolicyEnforcer.class}
 )
 @SystemLoaderClasses({RefactoredBackupManagerService.class, TransportManager.class})
 @Presubmit
@@ -109,12 +111,15 @@
         File cacheDir = mContext.getCacheDir();
         mBaseStateDir = new File(cacheDir, "base_state_dir");
         mDataDir = new File(cacheDir, "data_dir");
+
+        ShadowBackupPolicyEnforcer.setMandatoryBackupTransport(null);
     }
 
     @After
     public void tearDown() throws Exception {
         mBackupThread.quit();
         ShadowAppBackupUtils.reset();
+        ShadowBackupPolicyEnforcer.setMandatoryBackupTransport(null);
     }
 
     /* Tests for destination string */
@@ -253,11 +258,13 @@
     private TransportData mNewTransport;
     private TransportData mOldTransport;
     private ComponentName mNewTransportComponent;
+    private ComponentName mOldTransportComponent;
 
     private void setUpForSelectTransport() throws Exception {
         mNewTransport = backupTransport();
         mNewTransportComponent = mNewTransport.getTransportComponent();
         mOldTransport = d2dTransport();
+        mOldTransportComponent = mOldTransport.getTransportComponent();
         setUpTransports(mTransportManager, mNewTransport, mOldTransport, localTransport());
         when(mTransportManager.selectTransport(eq(mNewTransport.transportName)))
                 .thenReturn(mOldTransport.transportName);
@@ -307,6 +314,43 @@
     }
 
     @Test
+    public void testSelectBackupTransportAsync_whenMandatoryTransport() throws Exception {
+        setUpForSelectTransport();
+        ShadowBackupPolicyEnforcer.setMandatoryBackupTransport(mNewTransportComponent);
+        mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+        when(mTransportManager.registerAndSelectTransport(eq(mNewTransportComponent)))
+                .thenReturn(BackupManager.SUCCESS);
+        ISelectBackupTransportCallback callback = mock(ISelectBackupTransportCallback.class);
+        RefactoredBackupManagerService backupManagerService =
+                createInitializedBackupManagerService();
+
+        backupManagerService.selectBackupTransportAsync(mNewTransportComponent, callback);
+
+        mShadowBackupLooper.runToEndOfTasks();
+        assertThat(getSettingsTransport()).isEqualTo(mNewTransport.transportName);
+        verify(callback).onSuccess(eq(mNewTransport.transportName));
+    }
+
+    @Test
+    public void testSelectBackupTransportAsync_whenOtherThanMandatoryTransport()
+            throws Exception {
+        setUpForSelectTransport();
+        ShadowBackupPolicyEnforcer.setMandatoryBackupTransport(mOldTransportComponent);
+        mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
+        when(mTransportManager.registerAndSelectTransport(eq(mNewTransportComponent)))
+                .thenReturn(BackupManager.SUCCESS);
+        ISelectBackupTransportCallback callback = mock(ISelectBackupTransportCallback.class);
+        RefactoredBackupManagerService backupManagerService =
+                createInitializedBackupManagerService();
+
+        backupManagerService.selectBackupTransportAsync(mNewTransportComponent, callback);
+
+        mShadowBackupLooper.runToEndOfTasks();
+        assertThat(getSettingsTransport()).isNotEqualTo(mNewTransport.transportName);
+        verify(callback).onFailure(eq(BackupManager.ERROR_BACKUP_NOT_ALLOWED));
+    }
+
+    @Test
     public void testSelectBackupTransportAsync_whenRegistrationFails() throws Exception {
         setUpForSelectTransport();
         mShadowContext.grantPermissions(android.Manifest.permission.BACKUP);
diff --git a/services/robotests/src/com/android/server/backup/testing/ShadowBackupPolicyEnforcer.java b/services/robotests/src/com/android/server/backup/testing/ShadowBackupPolicyEnforcer.java
new file mode 100644
index 0000000..88b30da
--- /dev/null
+++ b/services/robotests/src/com/android/server/backup/testing/ShadowBackupPolicyEnforcer.java
@@ -0,0 +1,24 @@
+package com.android.server.backup.testing;
+
+import android.content.ComponentName;
+
+import com.android.server.backup.BackupPolicyEnforcer;
+import com.android.server.backup.RefactoredBackupManagerService;
+
+import org.robolectric.annotation.Implementation;
+import org.robolectric.annotation.Implements;
+
+@Implements(BackupPolicyEnforcer.class)
+public class ShadowBackupPolicyEnforcer {
+
+    private static ComponentName sMandatoryBackupTransport;
+
+    public static void setMandatoryBackupTransport(ComponentName backupTransportComponent) {
+        sMandatoryBackupTransport = backupTransportComponent;
+    }
+
+    @Implementation
+    public ComponentName getMandatoryBackupTransport() {
+        return sMandatoryBackupTransport;
+    }
+}