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:
@@ -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)
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user