From a85e246ce5698928b30996020d2169e5a2339fcd Mon Sep 17 00:00:00 2001 From: Pavel Grafov Date: Thu, 26 Mar 2020 16:06:24 +0000 Subject: [PATCH] Warn the user about impending personal app suspension. * updatePersonalAppsSuspension is invoked for all events relevant to profile maximum time off: user stopped, user unlocked, system boot, deadline alarm goes off, setManagedProfileMaximumTimeOff called. * It takes all relecant bits of state into account: policy, current deadline, user state. It calculates the new state of the deadline, notification and alarm and makes appropriate changes (e.g. schedules the alarm, posts notification, suspens apps). * Updated package manager query flags so that even when personal apps are being suspended while the user is locked, it includes non direct boot aware apps as well. Test: manual, with TestDPC Test: atest OrgOwnedProfileOwnerTest#testWorkProfileMaximumTimeOff Test: atest com.android.server.devicepolicy.DevicePolicyManagerTest Test: atest OrgOwnedProfileOwnerTest#testPersonalAppsSuspensionNormalApp Bug: 149075510 Change-Id: I94d2582c7af91a5d97e67d2baf2e15f0a6d5ffa9 --- .../DevicePolicyManagerService.java | 192 ++++++++------ .../PersonalAppsSuspensionHelper.java | 15 +- .../DevicePolicyManagerServiceTestable.java | 17 ++ .../devicepolicy/DevicePolicyManagerTest.java | 244 ++++++++++++++++-- 4 files changed, 368 insertions(+), 100 deletions(-) diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 22b0b62a819d1..13ae07a22625d 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -399,11 +399,14 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { private static final long EXPIRATION_GRACE_PERIOD_MS = 5 * MS_PER_DAY; // 5 days, in ms private static final long MANAGED_PROFILE_MAXIMUM_TIME_OFF_THRESHOLD = 3 * MS_PER_DAY; + /** When to warn the user about the approaching work profile off deadline: 1 day before */ + private static final long MANAGED_PROFILE_OFF_WARNING_PERIOD = 1 * MS_PER_DAY; private static final String ACTION_EXPIRED_PASSWORD_NOTIFICATION = "com.android.server.ACTION_EXPIRED_PASSWORD_NOTIFICATION"; - private static final String ACTION_PROFILE_OFF_DEADLINE = + @VisibleForTesting + static final String ACTION_PROFILE_OFF_DEADLINE = "com.android.server.ACTION_PROFILE_OFF_DEADLINE"; private static final String ATTR_PERMISSION_PROVIDER = "permission-provider"; @@ -645,6 +648,13 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { private static final boolean ENABLE_LOCK_GUARD = true; + /** Profile off deadline is not set or more than MANAGED_PROFILE_OFF_WARNING_PERIOD away. */ + private static final int PROFILE_OFF_DEADLINE_DEFAULT = 0; + /** Profile off deadline is closer than MANAGED_PROFILE_OFF_WARNING_PERIOD. */ + private static final int PROFILE_OFF_DEADLINE_WARNING = 1; + /** Profile off deadline reached, notify the user that personal apps blocked. */ + private static final int PROFILE_OFF_DEADLINE_REACHED = 2; + interface Stats { int LOCK_GUARD_GUARD = 0; @@ -922,11 +932,12 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { mUserData.remove(userHandle); } handlePackagesChanged(null /* check all admins */, userHandle); + updatePersonalAppsSuspensionOnUserStart(userHandle); } else if (Intent.ACTION_USER_STOPPED.equals(action)) { sendDeviceOwnerUserCommand(DeviceAdminReceiver.ACTION_USER_STOPPED, userHandle); if (isManagedProfile(userHandle)) { Slog.d(LOG_TAG, "Managed profile was stopped"); - updatePersonalAppSuspension(userHandle, false /* profileIsOn */); + updatePersonalAppsSuspension(userHandle, false /* unlocked */); } } else if (Intent.ACTION_USER_SWITCHED.equals(action)) { sendDeviceOwnerUserCommand(DeviceAdminReceiver.ACTION_USER_SWITCHED, userHandle); @@ -936,7 +947,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } if (isManagedProfile(userHandle)) { Slog.d(LOG_TAG, "Managed profile became unlocked"); - updatePersonalAppSuspension(userHandle, true /* profileIsOn */); + updatePersonalAppsSuspension(userHandle, true /* unlocked */); } } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_UNAVAILABLE.equals(action)) { handlePackagesChanged(null /* check all admins */, userHandle); @@ -963,7 +974,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { Slog.i(LOG_TAG, "Profile off deadline alarm was triggered"); final int userId = getManagedUserId(UserHandle.USER_SYSTEM); if (userId >= 0) { - updatePersonalAppSuspension(userId, mUserManager.isUserUnlocked(userId)); + updatePersonalAppsSuspension(userId, mUserManager.isUserUnlocked(userId)); } else { Slog.wtf(LOG_TAG, "Got deadline alarm for nonexistent profile"); } @@ -2482,6 +2493,16 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { public void runCryptoSelfTest() { CryptoTestHelper.runAndLogSelfTest(); } + + public String[] getPersonalAppsForSuspension(int userId) { + return new PersonalAppsSuspensionHelper( + mContext.createContextAsUser(UserHandle.of(userId), 0 /* flags */)) + .getPersonalAppsForSuspension(); + } + + public long systemCurrentTimeMillis() { + return System.currentTimeMillis(); + } } /** @@ -3998,10 +4019,6 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { applyManagedProfileRestrictionIfDeviceOwnerLocked(); } maybeStartSecurityLogMonitorOnActivityManagerReady(); - final int userId = getManagedUserId(UserHandle.USER_SYSTEM); - if (userId >= 0) { - updatePersonalAppSuspension(userId, false /* running */); - } break; case SystemService.PHASE_BOOT_COMPLETED: ensureDeviceOwnerUserStarted(); // TODO Consider better place to do this. @@ -4009,6 +4026,16 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } } + private void updatePersonalAppsSuspensionOnUserStart(int userHandle) { + final int profileUserHandle = getManagedUserId(userHandle); + if (profileUserHandle >= 0) { + // Given that the parent user has just started, profile should be locked. + updatePersonalAppsSuspension(profileUserHandle, false /* unlocked */); + } else { + suspendPersonalAppsInternal(userHandle, false); + } + } + private void onLockSettingsReady() { getUserData(UserHandle.USER_SYSTEM); loadOwners(); @@ -15846,11 +15873,8 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } } - final int suspendedState = suspended - ? PERSONAL_APPS_SUSPENDED_EXPLICITLY - : PERSONAL_APPS_NOT_SUSPENDED; - mInjector.binderWithCleanCallingIdentity( - () -> applyPersonalAppsSuspension(callingUserId, suspendedState)); + mInjector.binderWithCleanCallingIdentity(() -> updatePersonalAppsSuspension( + callingUserId, mUserManager.isUserUnlocked(callingUserId))); DevicePolicyEventLogger .createEvent(DevicePolicyEnums.SET_PERSONAL_APPS_SUSPENDED) @@ -15860,44 +15884,54 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } /** - * Checks whether there is a policy that requires personal apps to be suspended and if so, - * applies it. - * @param running whether the profile is currently considered running. + * Checks whether personal apps should be suspended according to the policy and applies the + * change if needed. + * + * @param unlocked whether the profile is currently running unlocked. */ - private void updatePersonalAppSuspension(int profileUserId, boolean running) { - final int suspensionState; + private void updatePersonalAppsSuspension(int profileUserId, boolean unlocked) { + final boolean suspended; synchronized (getLockObject()) { final ActiveAdmin profileOwner = getProfileOwnerAdminLocked(profileUserId); if (profileOwner != null) { - final boolean deadlineReached = - updateProfileOffDeadlineLocked(profileUserId, profileOwner, running); - suspensionState = makeSuspensionReasons( - profileOwner.mSuspendPersonalApps, deadlineReached); - Slog.d(LOG_TAG, - String.format("New personal apps suspension state: %d", suspensionState)); + final int deadlineState = + updateProfileOffDeadlineLocked(profileUserId, profileOwner, unlocked); + suspended = profileOwner.mSuspendPersonalApps + || deadlineState == PROFILE_OFF_DEADLINE_REACHED; + Slog.d(LOG_TAG, String.format("Personal apps suspended: %b, deadline state: %d", + suspended, deadlineState)); + updateProfileOffDeadlineNotificationLocked(profileUserId, profileOwner, + unlocked ? PROFILE_OFF_DEADLINE_DEFAULT : deadlineState); } else { - suspensionState = PERSONAL_APPS_NOT_SUSPENDED; + suspended = false; } } - applyPersonalAppsSuspension(profileUserId, suspensionState); + final int parentUserId = getProfileParentId(profileUserId); + suspendPersonalAppsInternal(parentUserId, suspended); } /** * Checks work profile time off policy, scheduling personal apps suspension via alarm if * necessary. - * @return whether the apps should be suspended based on maximum time off policy. + * @return profile deadline state */ - private boolean updateProfileOffDeadlineLocked( + private int updateProfileOffDeadlineLocked( int profileUserId, ActiveAdmin profileOwner, boolean unlocked) { - final long now = System.currentTimeMillis(); + final long now = mInjector.systemCurrentTimeMillis(); if (profileOwner.mProfileOffDeadline != 0 && now > profileOwner.mProfileOffDeadline) { // Profile off deadline is already reached. Slog.i(LOG_TAG, "Profile off deadline has been reached."); - return true; + return PROFILE_OFF_DEADLINE_REACHED; } boolean shouldSaveSettings = false; - if (profileOwner.mProfileOffDeadline != 0 + if (profileOwner.mSuspendPersonalApps) { + // When explicit suspension is active, deadline shouldn't be set. + if (profileOwner.mProfileOffDeadline != 0) { + profileOwner.mProfileOffDeadline = 0; + shouldSaveSettings = true; + } + } else if (profileOwner.mProfileOffDeadline != 0 && (profileOwner.mProfileMaximumTimeOffMillis == 0 || unlocked)) { // There is a deadline but either there is no policy or the profile is unlocked -> clear // the deadline. @@ -15913,52 +15947,51 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { shouldSaveSettings = true; } - updateProfileOffAlarm(profileOwner.mProfileOffDeadline); - if (shouldSaveSettings) { saveSettingsLocked(profileUserId); } - return false; - } - private void updateProfileOffAlarm(long profileOffDeadline) { + final long alarmTime; + final int deadlineState; + if (profileOwner.mProfileOffDeadline == 0) { + alarmTime = 0; + deadlineState = PROFILE_OFF_DEADLINE_DEFAULT; + } else if (profileOwner.mProfileOffDeadline - now < MANAGED_PROFILE_OFF_WARNING_PERIOD) { + // The deadline is close, upon the alarm personal apps should be suspended. + alarmTime = profileOwner.mProfileOffDeadline; + deadlineState = PROFILE_OFF_DEADLINE_WARNING; + } else { + // The deadline is quite far, upon the alarm we should warn the user first, so the + // alarm is scheduled earlier than the actual deadline. + alarmTime = profileOwner.mProfileOffDeadline - MANAGED_PROFILE_OFF_WARNING_PERIOD; + deadlineState = PROFILE_OFF_DEADLINE_DEFAULT; + } + final AlarmManager am = mInjector.getAlarmManager(); final PendingIntent pi = mInjector.pendingIntentGetBroadcast( mContext, REQUEST_PROFILE_OFF_DEADLINE, new Intent(ACTION_PROFILE_OFF_DEADLINE), PendingIntent.FLAG_ONE_SHOT | PendingIntent.FLAG_UPDATE_CURRENT); - am.cancel(pi); - if (profileOffDeadline != 0) { - Slog.i(LOG_TAG, "Profile off deadline alarm is set."); - am.set(AlarmManager.RTC, profileOffDeadline, pi); - } else { + + if (alarmTime == 0) { Slog.i(LOG_TAG, "Profile off deadline alarm is removed."); - } - } - - private void applyPersonalAppsSuspension( - int profileUserId, @PersonalAppsSuspensionReason int suspensionState) { - final boolean suspended = getUserData(UserHandle.USER_SYSTEM).mAppsSuspended; - final boolean shouldSuspend = suspensionState != PERSONAL_APPS_NOT_SUSPENDED; - if (suspended != shouldSuspend) { - suspendPersonalAppsInternal(shouldSuspend, UserHandle.USER_SYSTEM); - } - - if (suspensionState == PERSONAL_APPS_SUSPENDED_PROFILE_TIMEOUT) { - sendPersonalAppsSuspendedNotification(profileUserId); + am.cancel(pi); } else { - clearPersonalAppsSuspendedNotification(); + Slog.i(LOG_TAG, "Profile off deadline alarm is set."); + am.set(AlarmManager.RTC, alarmTime, pi); } + + return deadlineState; } - private void suspendPersonalAppsInternal(boolean suspended, int userId) { + private void suspendPersonalAppsInternal(int userId, boolean suspended) { + if (getUserData(userId).mAppsSuspended == suspended) { + return; + } Slog.i(LOG_TAG, String.format("%s personal apps for user %d", suspended ? "Suspending" : "Unsuspending", userId)); mInjector.binderWithCleanCallingIdentity(() -> { try { - final String[] appsToSuspend = - new PersonalAppsSuspensionHelper( - mContext.createContextAsUser(UserHandle.of(userId), 0 /* flags */)) - .getPersonalAppsForSuspension(); + final String[] appsToSuspend = mInjector.getPersonalAppsForSuspension(userId); final String[] failedPackages = mIPackageManager.setPackagesSuspendedAsUser( appsToSuspend, suspended, null, null, null, PLATFORM_PACKAGE_NAME, userId); if (!ArrayUtils.isEmpty(failedPackages)) { @@ -15977,37 +16010,38 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } } - private void clearPersonalAppsSuspendedNotification() { - mInjector.binderWithCleanCallingIdentity(() -> - mInjector.getNotificationManager().cancel( - SystemMessage.NOTE_PERSONAL_APPS_SUSPENDED)); - } + private void updateProfileOffDeadlineNotificationLocked(int profileUserId, + @Nullable ActiveAdmin profileOwner, int notificationState) { - private void sendPersonalAppsSuspendedNotification(int userId) { - final String profileOwnerPackageName; - final long maxTimeOffDays; - synchronized (getLockObject()) { - profileOwnerPackageName = mOwners.getProfileOwnerComponent(userId).getPackageName(); - final ActiveAdmin poAdmin = getProfileOwnerAdminLocked(userId); - maxTimeOffDays = TimeUnit.MILLISECONDS.toDays(poAdmin.mProfileMaximumTimeOffMillis); + if (notificationState == PROFILE_OFF_DEADLINE_DEFAULT) { + mInjector.getNotificationManager().cancel(SystemMessage.NOTE_PERSONAL_APPS_SUSPENDED); + return; } + final String profileOwnerPackageName = profileOwner.info.getPackageName(); + final long maxTimeOffDays = + TimeUnit.MILLISECONDS.toDays(profileOwner.mProfileMaximumTimeOffMillis); + final Intent intent = new Intent(DevicePolicyManager.ACTION_CHECK_POLICY_COMPLIANCE); intent.setPackage(profileOwnerPackageName); final PendingIntent pendingIntent = mInjector.pendingIntentGetActivityAsUser(mContext, - 0 /* requestCode */, intent, PendingIntent.FLAG_UPDATE_CURRENT, null /* options */, - UserHandle.of(userId)); + 0 /* requestCode */, intent, PendingIntent.FLAG_UPDATE_CURRENT, + null /* options */, UserHandle.of(profileUserId)); + + // TODO(b/149075510): Only the first of the notifications should be dismissible. + final String title = mContext.getString( + notificationState == PROFILE_OFF_DEADLINE_WARNING + ? R.string.personal_apps_suspended_tomorrow_title + : R.string.personal_apps_suspended_title); final Notification notification = new Notification.Builder(mContext, SystemNotificationChannels.DEVICE_ADMIN) .setSmallIcon(android.R.drawable.stat_sys_warning) .setOngoing(true) - .setContentTitle( - mContext.getString( - R.string.personal_apps_suspended_title)) + .setContentTitle(title) .setContentText(mContext.getString( - R.string.personal_apps_suspended_text, maxTimeOffDays)) + R.string.personal_apps_suspended_text, maxTimeOffDays)) .setColor(mContext.getColor(R.color.system_notification_accent_color)) .setContentIntent(pendingIntent) .build(); @@ -16039,7 +16073,7 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } mInjector.binderWithCleanCallingIdentity( - () -> updatePersonalAppSuspension(userId, mUserManager.isUserUnlocked())); + () -> updatePersonalAppsSuspension(userId, mUserManager.isUserUnlocked())); DevicePolicyEventLogger .createEvent(DevicePolicyEnums.SET_MANAGED_PROFILE_MAXIMUM_TIME_OFF) diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java b/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java index d9db17eba8870..da716eaed82bf 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java @@ -51,6 +51,10 @@ import java.util.Set; public class PersonalAppsSuspensionHelper { private static final String LOG_TAG = DevicePolicyManagerService.LOG_TAG; + // Flags to get all packages even if the user is still locked. + private static final int PACKAGE_QUERY_FLAGS = + PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DIRECT_BOOT_UNAWARE; + private final Context mContext; private final PackageManager mPackageManager; @@ -67,7 +71,7 @@ public class PersonalAppsSuspensionHelper { */ String[] getPersonalAppsForSuspension() { final List installedPackageInfos = - mPackageManager.getInstalledPackages(0 /* flags */); + mPackageManager.getInstalledPackages(PACKAGE_QUERY_FLAGS); final Set result = new ArraySet<>(); for (final PackageInfo packageInfo : installedPackageInfos) { final ApplicationInfo info = packageInfo.applicationInfo; @@ -97,7 +101,7 @@ public class PersonalAppsSuspensionHelper { final Intent intent = new Intent(Intent.ACTION_MAIN); intent.addCategory(Intent.CATEGORY_HOME); final List matchingActivities = - mPackageManager.queryIntentActivities(intent, 0); + mPackageManager.queryIntentActivities(intent, PACKAGE_QUERY_FLAGS); for (final ResolveInfo resolveInfo : matchingActivities) { if (resolveInfo.activityInfo == null || TextUtils.isEmpty(resolveInfo.activityInfo.packageName)) { @@ -107,7 +111,7 @@ public class PersonalAppsSuspensionHelper { final String packageName = resolveInfo.activityInfo.packageName; try { final ApplicationInfo applicationInfo = - mPackageManager.getApplicationInfo(packageName, 0); + mPackageManager.getApplicationInfo(packageName, PACKAGE_QUERY_FLAGS); if (applicationInfo.isSystemApp() || applicationInfo.isUpdatedSystemApp()) { result.add(packageName); } @@ -147,7 +151,8 @@ public class PersonalAppsSuspensionHelper { private String getSettingsPackageName() { final Intent intent = new Intent(Settings.ACTION_SETTINGS); intent.addCategory(Intent.CATEGORY_DEFAULT); - final ResolveInfo resolveInfo = mPackageManager.resolveActivity(intent, /* flags= */ 0); + final ResolveInfo resolveInfo = + mPackageManager.resolveActivity(intent, PACKAGE_QUERY_FLAGS); if (resolveInfo != null) { return resolveInfo.activityInfo.packageName; } @@ -164,7 +169,7 @@ public class PersonalAppsSuspensionHelper { intentToResolve.addCategory(Intent.CATEGORY_LAUNCHER); intentToResolve.setPackage(packageName); final List resolveInfos = - mPackageManager.queryIntentActivities(intentToResolve, /* flags= */ 0); + mPackageManager.queryIntentActivities(intentToResolve, PACKAGE_QUERY_FLAGS); return resolveInfos != null && !resolveInfos.isEmpty(); } diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java index 3e5c21c67bb31..cbe49eb12f955 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java @@ -124,6 +124,9 @@ public class DevicePolicyManagerServiceTestable extends DevicePolicyManagerServi // Key is a pair of uri and userId private final Map, ContentObserver> mContentObservers = new ArrayMap<>(); + // Used as an override when set to nonzero. + private long mCurrentTimeMillis = 0; + public MockInjector(MockSystemServices services, DpmMockContext context) { super(context); this.services = services; @@ -470,5 +473,19 @@ public class DevicePolicyManagerServiceTestable extends DevicePolicyManagerServi @Override public void runCryptoSelfTest() {} + + @Override + public String[] getPersonalAppsForSuspension(int userId) { + return new String[]{}; + } + + public void setSystemCurrentTimeMillis(long value) { + mCurrentTimeMillis = value; + } + + @Override + public long systemCurrentTimeMillis() { + return mCurrentTimeMillis != 0 ? mCurrentTimeMillis : System.currentTimeMillis(); + } } } diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java index 57039e53429ed..b042e77946669 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerTest.java @@ -44,6 +44,7 @@ import static org.mockito.Matchers.eq; import static org.mockito.Matchers.isNull; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.atMost; +import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.never; @@ -62,6 +63,7 @@ import android.Manifest.permission; import android.app.Activity; import android.app.AppOpsManager; import android.app.Notification; +import android.app.PendingIntent; import android.app.admin.DeviceAdminReceiver; import android.app.admin.DevicePolicyManager; import android.app.admin.DevicePolicyManagerInternal; @@ -96,6 +98,7 @@ import android.util.Pair; import androidx.test.filters.SmallTest; import com.android.internal.R; +import com.android.internal.messages.nano.SystemMessageProto; import com.android.internal.widget.LockscreenCredential; import com.android.server.LocalServices; import com.android.server.SystemService; @@ -103,6 +106,7 @@ import com.android.server.devicepolicy.DevicePolicyManagerService.RestrictionsLi import org.hamcrest.BaseMatcher; import org.hamcrest.Description; +import org.hamcrest.Matcher; import org.mockito.Mockito; import org.mockito.internal.util.collections.Sets; import org.mockito.stubbing.Answer; @@ -181,6 +185,20 @@ public class DevicePolicyManagerTest extends DpmTestBase { "wQ==\n" + "-----END CERTIFICATE-----\n"; + // Constants for testing setManagedProfileMaximumTimeOff: + // Profile maximum time off value + private static final long PROFILE_OFF_TIMEOUT = TimeUnit.DAYS.toMillis(5); + // Synthetic time at the beginning of test. + private static final long PROFILE_OFF_START = 1; + // Time when warning notification should be posted, + private static final long PROFILE_OFF_WARNING_TIME = + PROFILE_OFF_START + PROFILE_OFF_TIMEOUT - TimeUnit.DAYS.toMillis(1); + // Time when the apps should be suspended + private static final long PROFILE_OFF_DEADLINE = PROFILE_OFF_START + PROFILE_OFF_TIMEOUT; + // Notification titles for setManagedProfileMaximumTimeOff tests: + private static final String PROFILE_OFF_WARNING_TITLE = "suspended_tomorrow"; + private static final String PROFILE_OFF_SUSPENDED_TITLE = "suspended"; + @Override protected void setUp() throws Exception { super.setUp(); @@ -1558,20 +1576,7 @@ public class DevicePolicyManagerTest extends DpmTestBase { dpms.approveCaCert(fourCerts.getList().get(1), userId, true); // a notification should be shown saying that there are two certificates left to approve. verify(getServices().notificationManager, timeout(1000)) - .notifyAsUser(anyString(), anyInt(), argThat( - new BaseMatcher() { - @Override - public boolean matches(Object item) { - final Notification noti = (Notification) item; - return TEST_STRING.equals( - noti.extras.getString(Notification.EXTRA_TITLE)); - } - @Override - public void describeTo(Description description) { - description.appendText( - "Notification{title=\"" + TEST_STRING + "\"}"); - } - }), eq(user)); + .notifyAsUser(anyString(), anyInt(), argThat(hasTitle(TEST_STRING)), eq(user)); } /** @@ -6272,7 +6277,214 @@ public class DevicePolicyManagerTest extends DpmTestBase { assertThat(dpm.getAccountTypesWithManagementDisabled()).isEmpty(); } - // admin1 is the outgoing DPC, adminAnotherPakcage is the incoming one. + /** + * Tests the case when the user doesn't turn the profile on in time, verifies that the user is + * warned with a notification and then the apps get suspended. + */ + public void testMaximumProfileTimeOff_profileOffTimeExceeded() throws Exception { + prepareMocksForSetMaximumProfileTimeOff(); + + mContext.binder.callingUid = DpmMockContext.CALLER_UID; + dpm.setManagedProfileMaximumTimeOff(admin1, PROFILE_OFF_TIMEOUT); + + mContext.binder.callingUid = DpmMockContext.SYSTEM_UID; + // The profile is running, neither alarm nor notification should be posted. + verify(getServices().alarmManager, never()) + .set(anyInt(), anyLong(), any(PendingIntent.class)); + verify(getServices().notificationManager, never()) + .notify(anyInt(), any(Notification.class)); + // Apps shouldn't be suspended. + verifyZeroInteractions(getServices().ipackageManager); + clearInvocations(getServices().alarmManager); + + sendUserStoppedBroadcastForProfile(); + + // Verify the alarm was scheduled for time when the warning should be shown. + verify(getServices().alarmManager, times(1)) + .set(anyInt(), eq(PROFILE_OFF_WARNING_TIME), any()); + // But still no notification should be posted at this point. + verify(getServices().notificationManager, never()) + .notify(anyInt(), any(Notification.class)); + // Apps shouldn't be suspended. + verifyZeroInteractions(getServices().ipackageManager); + clearInvocations(getServices().alarmManager); + + // Pretend the alarm went off. + dpms.mMockInjector.setSystemCurrentTimeMillis(PROFILE_OFF_WARNING_TIME + 10); + sendProfileOffDeadlineAlarmBroadcast(); + + // Verify the alarm was scheduled for the actual deadline this time. + verify(getServices().alarmManager, times(1)).set(anyInt(), eq(PROFILE_OFF_DEADLINE), any()); + // Now the user should see a warning notification. + verify(getServices().notificationManager, times(1)) + .notify(anyInt(), argThat(hasTitle(PROFILE_OFF_WARNING_TITLE))); + // Apps shouldn't be suspended yet. + verifyZeroInteractions(getServices().ipackageManager); + clearInvocations(getServices().alarmManager); + clearInvocations(getServices().notificationManager); + + // Pretend the alarm went off. + dpms.mMockInjector.setSystemCurrentTimeMillis(PROFILE_OFF_DEADLINE + 10); + sendProfileOffDeadlineAlarmBroadcast(); + + // Verify the alarm was not set. + verifyZeroInteractions(getServices().alarmManager); + // Now the user should see a notification about suspended apps. + verify(getServices().notificationManager, times(1)) + .notify(anyInt(), argThat(hasTitle(PROFILE_OFF_SUSPENDED_TITLE))); + // Verify that the apps are suspended. + verify(getServices().ipackageManager, times(1)).setPackagesSuspendedAsUser( + any(), eq(true), any(), any(), any(), any(), anyInt()); + } + + /** + * Tests the case when the user turns the profile back on long before the deadline (> 1 day). + */ + public void testMaximumProfileTimeOff_turnOnBeforeWarning() throws Exception { + prepareMocksForSetMaximumProfileTimeOff(); + + mContext.binder.callingUid = DpmMockContext.CALLER_UID; + dpm.setManagedProfileMaximumTimeOff(admin1, PROFILE_OFF_TIMEOUT); + + mContext.binder.callingUid = DpmMockContext.SYSTEM_UID; + sendUserStoppedBroadcastForProfile(); + clearInvocations(getServices().alarmManager); + sendUserUnlockedBroadcastForProfile(); + + // Verify that the alarm got discharged. + verify(getServices().alarmManager, times(1)).cancel((PendingIntent) null); + } + + /** + * Tests the case when the user turns the profile back on after the warning notification. + */ + public void testMaximumProfileTimeOff_turnOnAfterWarning() throws Exception { + prepareMocksForSetMaximumProfileTimeOff(); + + mContext.binder.callingUid = DpmMockContext.CALLER_UID; + dpm.setManagedProfileMaximumTimeOff(admin1, PROFILE_OFF_TIMEOUT); + + mContext.binder.callingUid = DpmMockContext.SYSTEM_UID; + sendUserStoppedBroadcastForProfile(); + + // Pretend the alarm went off. + dpms.mMockInjector.setSystemCurrentTimeMillis(PROFILE_OFF_WARNING_TIME + 10); + sendProfileOffDeadlineAlarmBroadcast(); + + clearInvocations(getServices().alarmManager); + clearInvocations(getServices().notificationManager); + sendUserUnlockedBroadcastForProfile(); + + // Verify that the alarm got discharged. + verify(getServices().alarmManager, times(1)).cancel((PendingIntent) null); + // Verify that the notification is removed. + verify(getServices().notificationManager, times(1)) + .cancel(eq(SystemMessageProto.SystemMessage.NOTE_PERSONAL_APPS_SUSPENDED)); + } + + /** + * Tests the case when the user turns the profile back on when the apps are already suspended. + */ + public void testMaximumProfileTimeOff_turnOnAfterDeadline() throws Exception { + prepareMocksForSetMaximumProfileTimeOff(); + + mContext.binder.callingUid = DpmMockContext.CALLER_UID; + dpm.setManagedProfileMaximumTimeOff(admin1, PROFILE_OFF_TIMEOUT); + + mContext.binder.callingUid = DpmMockContext.SYSTEM_UID; + sendUserStoppedBroadcastForProfile(); + + // Pretend the alarm went off after the deadline. + dpms.mMockInjector.setSystemCurrentTimeMillis(PROFILE_OFF_DEADLINE + 10); + sendProfileOffDeadlineAlarmBroadcast(); + + clearInvocations(getServices().alarmManager); + clearInvocations(getServices().notificationManager); + clearInvocations(getServices().ipackageManager); + + sendUserUnlockedBroadcastForProfile(); + + // Verify that the notification is removed (at this point DPC should show it). + verify(getServices().notificationManager, times(1)) + .cancel(eq(SystemMessageProto.SystemMessage.NOTE_PERSONAL_APPS_SUSPENDED)); + // Verify that the apps are NOT unsuspeded. + verify(getServices().ipackageManager, never()).setPackagesSuspendedAsUser( + any(), eq(false), any(), any(), any(), any(), anyInt()); + } + + private void sendUserUnlockedBroadcastForProfile() throws Exception { + when(getServices().userManager.isUserUnlocked(eq(DpmMockContext.CALLER_USER_HANDLE))) + .thenReturn(true); + final Intent unlockedIntent = new Intent(Intent.ACTION_USER_UNLOCKED) + .putExtra(Intent.EXTRA_USER_HANDLE, DpmMockContext.CALLER_USER_HANDLE); + getServices().injectBroadcast( + mServiceContext, unlockedIntent, DpmMockContext.CALLER_USER_HANDLE); + flushTasks(); + } + + + private void sendProfileOffDeadlineAlarmBroadcast() throws Exception { + final Intent deadlineAlarmIntent = + new Intent(DevicePolicyManagerService.ACTION_PROFILE_OFF_DEADLINE); + getServices().injectBroadcast( + mServiceContext, deadlineAlarmIntent, DpmMockContext.CALLER_USER_HANDLE); + flushTasks(); + } + + private void sendUserStoppedBroadcastForProfile() throws Exception { + when(getServices().userManager.isUserUnlocked(eq(DpmMockContext.CALLER_USER_HANDLE))) + .thenReturn(false); + final Intent stoppedIntent = new Intent(Intent.ACTION_USER_STOPPED) + .putExtra(Intent.EXTRA_USER_HANDLE, DpmMockContext.CALLER_USER_HANDLE); + getServices().injectBroadcast(mServiceContext, stoppedIntent, + DpmMockContext.CALLER_USER_HANDLE); + flushTasks(); + } + + private void prepareMocksForSetMaximumProfileTimeOff() throws Exception { + addManagedProfile(admin1, DpmMockContext.CALLER_UID, admin1); + configureProfileOwnerOfOrgOwnedDevice(admin1, DpmMockContext.CALLER_USER_HANDLE); + + when(getServices().userManager.isUserUnlocked()).thenReturn(true); + + // Pretend our admin handles CHECK_POLICY_COMPLIANCE intent. + final Intent intent = new Intent(DevicePolicyManager.ACTION_CHECK_POLICY_COMPLIANCE); + intent.setPackage(admin1.getPackageName()); + + doReturn(Collections.singletonList(new ResolveInfo())) + .when(getServices().packageManager).queryIntentActivitiesAsUser( + any(Intent.class), anyInt(), eq(DpmMockContext.CALLER_USER_HANDLE)); + + dpms.mMockInjector.setSystemCurrentTimeMillis(PROFILE_OFF_START); + // To allow creation of Notification via Notification.Builder + mContext.applicationInfo = mRealTestContext.getApplicationInfo(); + + // Setup notification titles. + when(mServiceContext.resources + .getString(R.string.personal_apps_suspended_tomorrow_title)) + .thenReturn(PROFILE_OFF_WARNING_TITLE); + when(mServiceContext.resources + .getString(R.string.personal_apps_suspended_title)) + .thenReturn(PROFILE_OFF_SUSPENDED_TITLE); + + clearInvocations(getServices().ipackageManager); + } + + private static Matcher hasTitle(String expected) { + return new BaseMatcher() { + @Override + public boolean matches(Object item) { + final Notification notification = (Notification) item; + return expected.equals(notification.extras.getString(Notification.EXTRA_TITLE)); + } + @Override + public void describeTo(Description description) { + description.appendText("Notification{title=\"" + expected + "\"}"); + } + }; + } + + // admin1 is the outgoing DPC, adminAnotherPackage is the incoming one. private void assertDeviceOwnershipRevertedWithFakeTransferMetadata() throws Exception { writeFakeTransferMetadataFile(UserHandle.USER_SYSTEM, TransferOwnershipMetadataManager.ADMIN_TYPE_DEVICE_OWNER); @@ -6299,7 +6511,7 @@ public class DevicePolicyManagerTest extends DpmTestBase { mServiceContext.binder.restoreCallingIdentity(ident); } - // admin1 is the outgoing DPC, adminAnotherPakcage is the incoming one. + // admin1 is the outgoing DPC, adminAnotherPackage is the incoming one. private void assertProfileOwnershipRevertedWithFakeTransferMetadata() throws Exception { writeFakeTransferMetadataFile(DpmMockContext.CALLER_USER_HANDLE, TransferOwnershipMetadataManager.ADMIN_TYPE_PROFILE_OWNER);