diff options
9 files changed, 273 insertions, 9 deletions
diff --git a/api/current.txt b/api/current.txt index 8b0cb9420a6b..5d372fe36391 100644 --- a/api/current.txt +++ b/api/current.txt @@ -6403,6 +6403,7 @@ package android.app.admin { 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 @@ package android.app.admin { 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 e334aab7005e..67b59f6ee97d 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -8633,6 +8633,13 @@ public class DevicePolicyManager { * * <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 @@ public class DevicePolicyManager { } /** + * 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 7154053f593c..9cdd1f836d45 100644 --- a/core/java/android/app/admin/IDevicePolicyManager.aidl +++ b/core/java/android/app/admin/IDevicePolicyManager.aidl @@ -359,6 +359,8 @@ interface IDevicePolicyManager { 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 792cb5f29f9c..f3ca74656e8c 100644 --- a/core/java/android/app/backup/IBackupManager.aidl +++ b/core/java/android/app/backup/IBackupManager.aidl @@ -294,7 +294,8 @@ interface IBackupManager { * * @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 000000000000..158084a4be21 --- /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 350d7af78a4c..f33ec55fbf04 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.AppGlobals; 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 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter @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 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter // Power management mWakelock = mPowerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "*backup*"); + + mBackupPolicyEnforcer = new BackupPolicyEnforcer(context); } private void initPackageTracking() { @@ -2774,6 +2779,10 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter 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 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter 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 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter @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 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter } 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 @@ public class RefactoredBackupManagerService implements BackupManagerServiceInter } } + /** + * 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 bf2b137f65b8..38d423da522c 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.HashMap; 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 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { 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 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { // 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 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { 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 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } 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 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } 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 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } @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 bf5822463746..dc0a4e32b4a8 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.platform.test.annotations.Presubmit; 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 @@ import java.util.Map; @Config( manifest = Config.NONE, sdk = 26, - shadows = {ShadowAppBackupUtils.class} + shadows = {ShadowAppBackupUtils.class, ShadowBackupPolicyEnforcer.class} ) @SystemLoaderClasses({RefactoredBackupManagerService.class, TransportManager.class}) @Presubmit @@ -109,12 +111,15 @@ public class BackupManagerServiceRoboTest { 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 @@ public class BackupManagerServiceRoboTest { 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 @@ public class BackupManagerServiceRoboTest { } @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 000000000000..88b30da46433 --- /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; + } +} |