Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit a85e246c authored by Pavel Grafov's avatar Pavel Grafov
Browse files

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
parent 3360c54c
Loading
Loading
Loading
Loading
+113 −79
Original line number Diff line number Diff line
@@ -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;
        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;
        }
    private void updateProfileOffAlarm(long profileOffDeadline) {
        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);
        if (alarmTime == 0) {
            Slog.i(LOG_TAG, "Profile off deadline alarm is removed.");
            am.cancel(pi);
        if (profileOffDeadline != 0) {
            Slog.i(LOG_TAG, "Profile off deadline alarm is set.");
            am.set(AlarmManager.RTC, profileOffDeadline, pi);
        } else {
            Slog.i(LOG_TAG, "Profile off deadline alarm is removed.");
        }
            Slog.i(LOG_TAG, "Profile off deadline alarm is set.");
            am.set(AlarmManager.RTC, alarmTime, pi);
        }
    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);
        return deadlineState;
    }
        if (suspensionState == PERSONAL_APPS_SUSPENDED_PROFILE_TIMEOUT) {
            sendPersonalAppsSuspendedNotification(profileUserId);
        } else {
            clearPersonalAppsSuspendedNotification();
        }
    private void suspendPersonalAppsInternal(int userId, boolean suspended) {
        if (getUserData(userId).mAppsSuspended == suspended) {
            return;
        }
    private void suspendPersonalAppsInternal(boolean suspended, int userId) {
        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,35 +16010,36 @@ 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))
                        .setColor(mContext.getColor(R.color.system_notification_accent_color))
@@ -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)
+10 −5
Original line number Diff line number Diff line
@@ -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();
    }

+17 −0
Original line number Diff line number Diff line
@@ -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();
        }
    }
}
+228 −16

File changed.

Preview size limit exceeded, changes collapsed.