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;
+ }
+}