Merge "Warn the user about impending personal app suspension." into rvc-dev am: 89d4da77ea am: 6236bfbca0 am: de893b7dae

Change-Id: I39a1787c62161c22362c37708803bb4dc9e60039
This commit is contained in:
Pavel Grafov
2020-04-22 15:00:42 +00:00
committed by Automerger Merge Worker
4 changed files with 368 additions and 100 deletions

View File

@@ -403,11 +403,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";
@@ -649,6 +652,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;
@@ -926,11 +936,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);
@@ -940,7 +951,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);
@@ -967,7 +978,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");
}
@@ -2486,6 +2497,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();
}
}
/**
@@ -4045,10 +4066,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.
@@ -4056,6 +4073,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();
@@ -15893,11 +15920,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)
@@ -15907,44 +15931,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.
@@ -15960,52 +15994,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)) {
@@ -16024,37 +16057,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();
@@ -16086,7 +16120,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)

View File

@@ -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<PackageInfo> installedPackageInfos =
mPackageManager.getInstalledPackages(0 /* flags */);
mPackageManager.getInstalledPackages(PACKAGE_QUERY_FLAGS);
final Set<String> 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<ResolveInfo> 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<ResolveInfo> resolveInfos =
mPackageManager.queryIntentActivities(intentToResolve, /* flags= */ 0);
mPackageManager.queryIntentActivities(intentToResolve, PACKAGE_QUERY_FLAGS);
return resolveInfos != null && !resolveInfos.isEmpty();
}

View File

@@ -124,6 +124,9 @@ public class DevicePolicyManagerServiceTestable extends DevicePolicyManagerServi
// Key is a pair of uri and userId
private final Map<Pair<Uri, Integer>, 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();
}
}
}

View File

@@ -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<Notification>() {
@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<Notification> hasTitle(String expected) {
return new BaseMatcher<Notification>() {
@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);