Add DPM methods to allow org owned PO to suspend personal apps

* When personal apps are suspended, only dialer, IMEs, a11y, launcher
  are some other critical apps are exempted.
* User is presented with notification, clicking on which invokes an
  activity in the DPC.

Bug: 147414651
Test: manual via TestDPC
Change-Id: I09f8dad08e54b0ce8201cd5c76b3f34342e0da8f
diff --git a/api/current.txt b/api/current.txt
index 49ee24a..bf5d24d 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -6853,6 +6853,7 @@
     method @Nullable public java.util.List<java.lang.String> getPermittedAccessibilityServices(@NonNull android.content.ComponentName);
     method @Nullable public java.util.List<java.lang.String> getPermittedCrossProfileNotificationListeners(@NonNull android.content.ComponentName);
     method @Nullable public java.util.List<java.lang.String> getPermittedInputMethods(@NonNull android.content.ComponentName);
+    method public int getPersonalAppsSuspendedReasons(@NonNull android.content.ComponentName);
     method @NonNull public java.util.List<java.lang.String> getProtectedPackages(@NonNull android.content.ComponentName);
     method public long getRequiredStrongAuthTimeout(@Nullable android.content.ComponentName);
     method public boolean getScreenCaptureDisabled(@Nullable android.content.ComponentName);
@@ -6979,6 +6980,7 @@
     method public boolean setPermittedAccessibilityServices(@NonNull android.content.ComponentName, java.util.List<java.lang.String>);
     method public boolean setPermittedCrossProfileNotificationListeners(@NonNull android.content.ComponentName, @Nullable java.util.List<java.lang.String>);
     method public boolean setPermittedInputMethods(@NonNull android.content.ComponentName, java.util.List<java.lang.String>);
+    method public void setPersonalAppsSuspended(@NonNull android.content.ComponentName, boolean);
     method public void setProfileEnabled(@NonNull android.content.ComponentName);
     method public void setProfileName(@NonNull android.content.ComponentName, String);
     method public void setProtectedPackages(@NonNull android.content.ComponentName, @NonNull java.util.List<java.lang.String>);
@@ -7014,6 +7016,7 @@
     field public static final String ACTION_ADMIN_POLICY_COMPLIANCE = "android.app.action.ADMIN_POLICY_COMPLIANCE";
     field public static final String ACTION_APPLICATION_DELEGATION_SCOPES_CHANGED = "android.app.action.APPLICATION_DELEGATION_SCOPES_CHANGED";
     field public static final String ACTION_BIND_SECONDARY_LOCKSCREEN_SERVICE = "android.app.action.BIND_SECONDARY_LOCKSCREEN_SERVICE";
+    field public static final String ACTION_CHECK_POLICY_COMPLIANCE = "android.app.action.CHECK_POLICY_COMPLIANCE";
     field public static final String ACTION_DEVICE_ADMIN_SERVICE = "android.app.action.DEVICE_ADMIN_SERVICE";
     field public static final String ACTION_DEVICE_OWNER_CHANGED = "android.app.action.DEVICE_OWNER_CHANGED";
     field public static final String ACTION_GET_PROVISIONING_MODE = "android.app.action.GET_PROVISIONING_MODE";
@@ -7137,6 +7140,8 @@
     field public static final int PERMISSION_POLICY_AUTO_DENY = 2; // 0x2
     field public static final int PERMISSION_POLICY_AUTO_GRANT = 1; // 0x1
     field public static final int PERMISSION_POLICY_PROMPT = 0; // 0x0
+    field public static final int PERSONAL_APPS_NOT_SUSPENDED = 0; // 0x0
+    field public static final int PERSONAL_APPS_SUSPENDED_EXPLICITLY = 1; // 0x1
     field public static final String POLICY_DISABLE_CAMERA = "policy_disable_camera";
     field public static final String POLICY_DISABLE_SCREEN_CAPTURE = "policy_disable_screen_capture";
     field public static final int PRIVATE_DNS_MODE_OFF = 1; // 0x1
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 9a63c52..ffa0047 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -2391,6 +2391,28 @@
             "android.app.action.BIND_SECONDARY_LOCKSCREEN_SERVICE";
 
     /**
+     * Return value for {@link #getPersonalAppsSuspendedReasons} when personal apps are not
+     * suspended.
+     */
+    public static final int PERSONAL_APPS_NOT_SUSPENDED = 0;
+
+    /**
+     * Flag for {@link #getPersonalAppsSuspendedReasons} return value. Set when personal
+     * apps are suspended by an admin explicitly via {@link #setPersonalAppsSuspended}.
+     */
+    public static final int PERSONAL_APPS_SUSPENDED_EXPLICITLY = 1 << 0;
+
+    /**
+     * @hide
+     */
+    @IntDef(flag = true, prefix = { "PERSONAL_APPS_" }, value = {
+            PERSONAL_APPS_NOT_SUSPENDED,
+            PERSONAL_APPS_SUSPENDED_EXPLICITLY
+    })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface PersonalAppSuspensionReason {}
+
+    /**
      * Return true if the given administrator component is currently active (enabled) in the system.
      *
      * @param admin The administrator component to check for.
@@ -4566,6 +4588,18 @@
             = "android.app.action.START_ENCRYPTION";
 
     /**
+     * Activity action: launch the DPC to check policy compliance. This intent is launched when
+     * the user taps on the notification about personal apps suspension. When handling this intent
+     * the DPC must check if personal apps should still be suspended and either unsuspend them or
+     * instruct the user on how to resolve the noncompliance causing the suspension.
+     *
+     * @see #setPersonalAppsSuspended
+     */
+    @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+    public static final String ACTION_CHECK_POLICY_COMPLIANCE =
+            "android.app.action.CHECK_POLICY_COMPLIANCE";
+
+    /**
      * Broadcast action: notify managed provisioning that new managed user is created.
      *
      * @hide
@@ -11650,4 +11684,48 @@
         }
         return false;
     }
+
+    /**
+     * Called by profile owner of an organization-owned managed profile to check whether
+     * personal apps are suspended.
+     *
+     * @return a bitmask of reasons for personal apps suspension or
+     *     {@link #PERSONAL_APPS_NOT_SUSPENDED} if apps are not suspended.
+     * @see #setPersonalAppsSuspended
+     */
+    public @PersonalAppSuspensionReason int getPersonalAppsSuspendedReasons(
+            @NonNull ComponentName admin) {
+        throwIfParentInstance("getPersonalAppsSuspendedReasons");
+        if (mService != null) {
+            try {
+                return mService.getPersonalAppsSuspendedReasons(admin);
+            } catch (RemoteException re) {
+                throw re.rethrowFromSystemServer();
+            }
+        }
+        return 0;
+    }
+
+    /**
+     * Called by a profile owner of an organization-owned managed profile to suspend personal
+     * apps on the device. When personal apps are suspended the device can only be used for calls.
+     *
+     * <p>When personal apps are suspended, an ongoing notification about that is shown to the user.
+     * When the user taps the notification, system invokes {@link #ACTION_CHECK_POLICY_COMPLIANCE}
+     * in the profile owner package. Profile owner implementation that uses personal apps suspension
+     * must handle this intent.
+     *
+     * @param admin Which {@link DeviceAdminReceiver} this request is associated with
+     * @param suspended Whether personal apps should be suspended.
+     */
+    public void setPersonalAppsSuspended(@NonNull ComponentName admin, boolean suspended) {
+        throwIfParentInstance("setPersonalAppsSuspended");
+        if (mService != null) {
+            try {
+                mService.setPersonalAppsSuspended(admin, suspended);
+            } catch (RemoteException re) {
+                throw re.rethrowFromSystemServer();
+            }
+        }
+    }
 }
diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl
index e7667c0..b9ffc4e 100644
--- a/core/java/android/app/admin/IDevicePolicyManager.aidl
+++ b/core/java/android/app/admin/IDevicePolicyManager.aidl
@@ -470,4 +470,7 @@
 
     void setCommonCriteriaModeEnabled(in ComponentName admin, boolean enabled);
     boolean isCommonCriteriaModeEnabled(in ComponentName admin);
+
+    int getPersonalAppsSuspendedReasons(in ComponentName admin);
+    void setPersonalAppsSuspended(in ComponentName admin, boolean suspended);
 }
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 7fd444a..fa70139 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -4326,6 +4326,13 @@
          generation). -->
     <bool name="config_customBugreport">false</bool>
 
+    <!-- Names of packages that should not be suspended when personal use is blocked by policy. -->
+    <string-array name="config_packagesExemptFromSuspension" translatable="false">
+        <!-- Add packages here, example: -->
+        <!-- <item>com.android.settings</item> -->
+    </string-array>
+
+
     <!-- Class name of the custom country detector to be used. -->
     <string name="config_customCountryDetector" translatable="false">com.android.server.location.ComprehensiveCountryDetector</string>
 
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index 6a00ecb..1995a04 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -425,6 +425,12 @@
     <!-- A toast message displayed when printing is attempted but disabled by policy. -->
     <string name="printing_disabled_by">Printing disabled by <xliff:g id="owner_app">%s</xliff:g>.</string>
 
+    <!-- Content title for a notification that personal apps are suspended [CHAR LIMIT=NONE] -->
+    <string name="personal_apps_suspended_notification_title">Personal apps have been suspended by an admin</string>
+
+    <!-- Message for a notification about personal apps suspension when work profile is off. [CHAR LIMIT=NONE] -->
+    <string name="personal_apps_suspended_notification_text">Tap here to check policy compliance.</string>
+
     <!-- Display name for any time a piece of data refers to the owner of the phone. For example, this could be used in place of the phone's phone number. -->
     <string name="me">Me</string>
 
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index 7e6eb5d..3aa3053 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -1209,6 +1209,8 @@
   <java-symbol type="string" name="device_ownership_relinquished" />
   <java-symbol type="string" name="network_logging_notification_title" />
   <java-symbol type="string" name="network_logging_notification_text" />
+  <java-symbol type="string" name="personal_apps_suspended_notification_title" />
+  <java-symbol type="string" name="personal_apps_suspended_notification_text" />
   <java-symbol type="string" name="factory_reset_warning" />
   <java-symbol type="string" name="factory_reset_message" />
   <java-symbol type="string" name="lockscreen_transport_play_description" />
@@ -3819,6 +3821,9 @@
   <java-symbol type="dimen" name="waterfall_display_right_edge_size" />
   <java-symbol type="dimen" name="waterfall_display_bottom_edge_size" />
 
+  <!-- For device policy -->
+  <java-symbol type="array" name="config_packagesExemptFromSuspension" />
+
   <!-- Accessibility take screenshot -->
   <java-symbol type="string" name="capability_desc_canTakeScreenshot" />
   <java-symbol type="string" name="capability_title_canTakeScreenshot" />
diff --git a/proto/src/system_messages.proto b/proto/src/system_messages.proto
index ad802ff..54b4201 100644
--- a/proto/src/system_messages.proto
+++ b/proto/src/system_messages.proto
@@ -312,5 +312,9 @@
     // Notify the user that data or apps are being moved to external storage.
     // Package: com.android.systemui
     NOTE_STORAGE_MOVE = 0x534d4f56;
+
+    // Notify the user that the admin suspended personal apps on the device.
+    // Package: android
+    NOTE_PERSONAL_APPS_SUSPENDED = 1003;
   }
 }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/BaseIDevicePolicyManager.java b/services/devicepolicy/java/com/android/server/devicepolicy/BaseIDevicePolicyManager.java
index 8641059..43ee97d 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/BaseIDevicePolicyManager.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/BaseIDevicePolicyManager.java
@@ -68,4 +68,11 @@
     public boolean isOrganizationOwnedDeviceWithManagedProfile() {
         return false;
     }
+
+    public int getPersonalAppsSuspendedReasons(ComponentName admin) {
+        return 0;
+    }
+
+    public void setPersonalAppsSuspended(ComponentName admin, boolean suspended) {
+    }
 }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
index 0c79a6f..f22b8db 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java
@@ -19,6 +19,7 @@
 import static android.Manifest.permission.BIND_DEVICE_ADMIN;
 import static android.Manifest.permission.MANAGE_CA_CERTIFICATES;
 import static android.Manifest.permission.REQUEST_PASSWORD_COMPLEXITY;
+import static android.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_ALL_MASK;
 import static android.app.ActivityManager.LOCK_TASK_MODE_NONE;
 import static android.app.admin.DeviceAdminReceiver.EXTRA_TRANSFER_OWNERSHIP_ADMIN_EXTRAS_BUNDLE;
 import static android.app.admin.DevicePolicyManager.ACTION_PROVISION_MANAGED_USER;
@@ -126,6 +127,7 @@
 import android.app.admin.DevicePolicyEventLogger;
 import android.app.admin.DevicePolicyManager;
 import android.app.admin.DevicePolicyManager.PasswordComplexity;
+import android.app.admin.DevicePolicyManager.PersonalAppSuspensionReason;
 import android.app.admin.DevicePolicyManagerInternal;
 import android.app.admin.DeviceStateCache;
 import android.app.admin.FactoryResetProtectionPolicy;
@@ -253,6 +255,7 @@
 import com.android.internal.os.BackgroundThread;
 import com.android.internal.statusbar.IStatusBarService;
 import com.android.internal.telephony.SmsApplication;
+import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.DumpUtils;
 import com.android.internal.util.FastXmlSerializer;
 import com.android.internal.util.FunctionalUtils.ThrowingRunnable;
@@ -372,6 +375,8 @@
 
     private static final String TAG_SECONDARY_LOCK_SCREEN = "secondary-lock-screen";
 
+    private static final String TAG_PERSONAL_APPS_SUSPENDED = "personal-apps-suspended";
+
     private static final int REQUEST_EXPIRE_PASSWORD = 5571;
 
     private static final long MS_PER_DAY = TimeUnit.DAYS.toMillis(1);
@@ -784,6 +789,10 @@
 
         long mPasswordTokenHandle = 0;
 
+        // Flag reflecting the current state of the personal apps suspension. This flag should
+        // only be written AFTER all the needed apps were suspended or unsuspended.
+        boolean mPersonalAppsSuspended = false;
+
         public DevicePolicyData(int userHandle) {
             mUserHandle = userHandle;
         }
@@ -1013,6 +1022,7 @@
         private static final String TAG_CROSS_PROFILE_PACKAGES = "cross-profile-packages";
         private static final String TAG_FACTORY_RESET_PROTECTION_POLICY =
                 "factory_reset_protection_policy";
+        private static final String TAG_SUSPEND_PERSONAL_APPS = "suspend-personal-apps";
 
         DeviceAdminInfo info;
 
@@ -1135,6 +1145,9 @@
         // represented as an empty list.
         List<String> mCrossProfilePackages = Collections.emptyList();
 
+        // Whether the admin explicitly requires personal apps to be suspended
+        boolean mSuspendPersonalApps = false;
+
         ActiveAdmin(DeviceAdminInfo _info, boolean parent) {
             info = _info;
             isParent = parent;
@@ -1344,8 +1357,7 @@
                 writeTextToXml(out, TAG_ORGANIZATION_NAME, organizationName);
             }
             if (isLogoutEnabled) {
-                writeAttributeValueToXml(
-                        out, TAG_IS_LOGOUT_ENABLED, isLogoutEnabled);
+                writeAttributeValueToXml(out, TAG_IS_LOGOUT_ENABLED, isLogoutEnabled);
             }
             if (startUserSessionMessage != null) {
                 writeTextToXml(out, TAG_START_USER_SESSION_MESSAGE, startUserSessionMessage);
@@ -1366,6 +1378,9 @@
                 mFactoryResetProtectionPolicy.writeToXml(out);
                 out.endTag(null, TAG_FACTORY_RESET_PROTECTION_POLICY);
             }
+            if (mSuspendPersonalApps) {
+                writeAttributeValueToXml(out, TAG_SUSPEND_PERSONAL_APPS, mSuspendPersonalApps);
+            }
         }
 
         void writeTextToXml(XmlSerializer out, String tag, String text) throws IOException {
@@ -1602,6 +1617,9 @@
                 } else if (TAG_FACTORY_RESET_PROTECTION_POLICY.equals(tag)) {
                     mFactoryResetProtectionPolicy = FactoryResetProtectionPolicy.readFromXml(
                                 parser);
+                } else if (TAG_SUSPEND_PERSONAL_APPS.equals(tag)) {
+                    mSuspendPersonalApps = Boolean.parseBoolean(
+                            parser.getAttributeValue(null, ATTR_VALUE));
                 } else {
                     Slog.w(LOG_TAG, "Unknown admin tag: " + tag);
                     XmlUtils.skipCurrentTag(parser);
@@ -3408,6 +3426,13 @@
                 out.endTag(null, TAG_PROTECTED_PACKAGES);
             }
 
+            if (policy.mPersonalAppsSuspended) {
+                out.startTag(null, TAG_PERSONAL_APPS_SUSPENDED);
+                out.attribute(null, ATTR_VALUE,
+                        Boolean.toString(policy.mPersonalAppsSuspended));
+                out.endTag(null, TAG_PERSONAL_APPS_SUSPENDED);
+            }
+
             out.endTag(null, "policies");
 
             out.endDocument();
@@ -3624,6 +3649,9 @@
                     policy.mOwnerInstalledCaCerts.add(parser.getAttributeValue(null, ATTR_ALIAS));
                 } else if (TAG_PROTECTED_PACKAGES.equals(tag)) {
                     policy.mProtectedPackages.add(parser.getAttributeValue(null, ATTR_NAME));
+                } else if (TAG_PERSONAL_APPS_SUSPENDED.equals(tag)) {
+                    policy.mPersonalAppsSuspended =
+                            Boolean.parseBoolean(parser.getAttributeValue(null, ATTR_VALUE));
                 } else {
                     Slog.w(LOG_TAG, "Unknown tag: " + tag);
                     XmlUtils.skipCurrentTag(parser);
@@ -3764,6 +3792,7 @@
                 synchronized (getLockObject()) {
                     maybeMigrateToProfileOnOrganizationOwnedDeviceLocked();
                 }
+                checkPackageSuspensionOnBoot();
                 break;
             case SystemService.PHASE_BOOT_COMPLETED:
                 ensureDeviceOwnerUserStarted(); // TODO Consider better place to do this.
@@ -3771,6 +3800,34 @@
         }
     }
 
+    private void checkPackageSuspensionOnBoot() {
+        int profileUserId = UserHandle.USER_NULL;
+        final boolean shouldSuspend;
+        synchronized (getLockObject()) {
+            for (final int userId : mOwners.getProfileOwnerKeys()) {
+                if (mOwners.isProfileOwnerOfOrganizationOwnedDevice(userId)) {
+                    profileUserId = userId;
+                    break;
+                }
+            }
+
+            if (profileUserId == UserHandle.USER_NULL) {
+                shouldSuspend = false;
+            } else {
+                shouldSuspend = getProfileOwnerAdminLocked(profileUserId).mSuspendPersonalApps;
+            }
+        }
+
+        final boolean suspended = getUserData(UserHandle.USER_SYSTEM).mPersonalAppsSuspended;
+        if (suspended != shouldSuspend) {
+            suspendPersonalAppsInternal(shouldSuspend, UserHandle.USER_SYSTEM);
+        }
+
+        if (shouldSuspend) {
+            sendPersonalAppsSuspendedNotification(profileUserId);
+        }
+    }
+
     private void onLockSettingsReady() {
         getUserData(UserHandle.USER_SYSTEM);
         loadOwners();
@@ -3883,11 +3940,13 @@
     @Override
     void handleUnlockUser(int userId) {
         startOwnerService(userId, "unlock-user");
+        maybeUpdatePersonalAppsSuspendedNotification(userId);
     }
 
     @Override
     void handleStopUser(int userId) {
         stopOwnerService(userId, "stop-user");
+        maybeUpdatePersonalAppsSuspendedNotification(userId);
     }
 
     private void startOwnerService(int userId, String actionForLog) {
@@ -9746,7 +9805,7 @@
                 }
                 AccessibilityManager accessibilityManager = getAccessibilityManagerForUser(userId);
                 enabledServices = accessibilityManager.getEnabledAccessibilityServiceList(
-                        AccessibilityServiceInfo.FEEDBACK_ALL_MASK);
+                        FEEDBACK_ALL_MASK);
             } finally {
                 mInjector.binderRestoreCallingIdentity(id);
             }
@@ -15157,4 +15216,121 @@
         }
         return mInjector.settingsGlobalGetInt(Settings.Global.COMMON_CRITERIA_MODE, 0) != 0;
     }
+
+    @Override
+    public @PersonalAppSuspensionReason int getPersonalAppsSuspendedReasons(ComponentName who) {
+        synchronized (getLockObject()) {
+            final ActiveAdmin admin = getActiveAdminForCallerLocked(who,
+                    DeviceAdminInfo.USES_POLICY_ORGANIZATION_OWNED_PROFILE_OWNER,
+                    false /* parent */);
+            // DO shouldn't be able to use this method.
+            enforceProfileOwnerOfOrganizationOwnedDevice(admin);
+            if (admin.mSuspendPersonalApps) {
+                return DevicePolicyManager.PERSONAL_APPS_SUSPENDED_EXPLICITLY;
+            } else {
+                return DevicePolicyManager.PERSONAL_APPS_NOT_SUSPENDED;
+            }
+        }
+    }
+
+    @Override
+    public void setPersonalAppsSuspended(ComponentName who, boolean suspended) {
+        final int callingUserId = mInjector.userHandleGetCallingUserId();
+        synchronized (getLockObject()) {
+            final ActiveAdmin admin = getActiveAdminForCallerLocked(who,
+                    DeviceAdminInfo.USES_POLICY_ORGANIZATION_OWNED_PROFILE_OWNER,
+                    false /* parent */);
+            // DO shouldn't be able to use this method.
+            enforceProfileOwnerOfOrganizationOwnedDevice(admin);
+            if (admin.mSuspendPersonalApps != suspended) {
+                admin.mSuspendPersonalApps = suspended;
+                saveSettingsLocked(callingUserId);
+            }
+        }
+
+        if (getUserData(UserHandle.USER_SYSTEM).mPersonalAppsSuspended == suspended) {
+            // Admin request matches current state, nothing to do.
+            return;
+        }
+
+        suspendPersonalAppsInternal(suspended, UserHandle.USER_SYSTEM);
+
+        mInjector.binderWithCleanCallingIdentity(() -> {
+            if (suspended) {
+                sendPersonalAppsSuspendedNotification(callingUserId);
+            } else {
+                clearPersonalAppsSuspendedNotification(callingUserId);
+            }
+        });
+    }
+
+    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, mInjector.getPackageManager())
+                                .getPersonalAppsForSuspension(userId);
+                final String[] failedPackages = mIPackageManager.setPackagesSuspendedAsUser(
+                        appsToSuspend, suspended, null, null, null, PLATFORM_PACKAGE_NAME, userId);
+                if (!ArrayUtils.isEmpty(failedPackages)) {
+                    Slog.wtf(LOG_TAG, String.format("Failed to %s packages: %s",
+                            suspended ? "suspend" : "unsuspend", String.join(",", failedPackages)));
+                }
+            } catch (RemoteException re) {
+                // Shouldn't happen.
+                Slog.e(LOG_TAG, "Failed talking to the package manager", re);
+            }
+        });
+
+        synchronized (getLockObject()) {
+            getUserData(userId).mPersonalAppsSuspended = suspended;
+            saveSettingsLocked(userId);
+        }
+    }
+
+    private void maybeUpdatePersonalAppsSuspendedNotification(int profileUserId) {
+        // TODO(b/147414651): Unless updated, the notification stops working after turning the
+        //  profile off and back on, so it has to be updated more often than necessary.
+        if (getUserData(UserHandle.USER_SYSTEM).mPersonalAppsSuspended
+                && getProfileParentId(profileUserId) == UserHandle.USER_SYSTEM) {
+            sendPersonalAppsSuspendedNotification(profileUserId);
+        }
+    }
+
+    private void clearPersonalAppsSuspendedNotification(int userId) {
+        mInjector.binderWithCleanCallingIdentity(() ->
+                mInjector.getNotificationManager().cancel(
+                        SystemMessage.NOTE_PERSONAL_APPS_SUSPENDED));
+    }
+
+    private void sendPersonalAppsSuspendedNotification(int userId) {
+        final String profileOwnerPackageName;
+        synchronized (getLockObject()) {
+            profileOwnerPackageName = mOwners.getProfileOwnerComponent(userId).getPackageName();
+        }
+
+        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));
+
+        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_notification_title))
+                        .setContentText(mContext.getString(
+                                R.string.personal_apps_suspended_notification_text))
+                        .setColor(mContext.getColor(R.color.system_notification_accent_color))
+                        .setContentIntent(pendingIntent)
+                        .build();
+        mInjector.getNotificationManager().notify(
+                SystemMessage.NOTE_PERSONAL_APPS_SUSPENDED, notification);
+    }
 }
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java b/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java
new file mode 100644
index 0000000..180acc8
--- /dev/null
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright (C) 2020 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.devicepolicy;
+
+import static android.accessibilityservice.AccessibilityServiceInfo.FEEDBACK_ALL_MASK;
+
+import android.accessibilityservice.AccessibilityServiceInfo;
+import android.annotation.Nullable;
+import android.annotation.UserIdInt;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.IBinder;
+import android.os.ServiceManager;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Slog;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.IAccessibilityManager;
+import android.view.inputmethod.InputMethodInfo;
+
+import com.android.internal.R;
+import com.android.server.inputmethod.InputMethodManagerInternal;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Utility class to find what personal apps should be suspended to limit personal device use.
+ */
+public class PersonalAppsSuspensionHelper {
+    private static final String LOG_TAG = DevicePolicyManagerService.LOG_TAG;
+
+    private final Context mContext;
+    private final PackageManager mPackageManager;
+
+    public PersonalAppsSuspensionHelper(Context context, PackageManager packageManager) {
+        mContext = context;
+        mPackageManager = packageManager;
+    }
+
+    /**
+     * @return List of packages that should be suspended to limit personal use.
+     */
+    String[] getPersonalAppsForSuspension(@UserIdInt int userId) {
+        final List<PackageInfo> installedPackageInfos =
+                mPackageManager.getInstalledPackagesAsUser(0 /* flags */, userId);
+        final Set<String> result = new HashSet<>();
+        for (final PackageInfo packageInfo : installedPackageInfos) {
+            final ApplicationInfo info = packageInfo.applicationInfo;
+            if ((!info.isSystemApp() && !info.isUpdatedSystemApp())
+                    || hasLauncherIntent(packageInfo.packageName)) {
+                result.add(packageInfo.packageName);
+            }
+        }
+        result.removeAll(getCriticalPackages());
+        result.removeAll(getSystemLauncherPackages());
+        result.removeAll(getAccessibilityServices(userId));
+        result.removeAll(getInputMethodPackages(userId));
+        result.remove(getActiveLauncherPackages(userId));
+        result.remove(getDialerPackage(userId));
+        result.remove(getSettingsPackageName(userId));
+
+        Slog.i(LOG_TAG, "Packages subject to suspension: " + String.join(",", result));
+        return result.toArray(new String[0]);
+    }
+
+    private List<String> getSystemLauncherPackages() {
+        final List<String> result = new ArrayList<>();
+        final Intent intent = new Intent(Intent.ACTION_MAIN);
+        intent.addCategory(Intent.CATEGORY_HOME);
+        final List<ResolveInfo> matchingActivities =
+                mPackageManager.queryIntentActivities(intent, 0);
+        for (final ResolveInfo resolveInfo : matchingActivities) {
+            if (resolveInfo.activityInfo == null
+                    || TextUtils.isEmpty(resolveInfo.activityInfo.packageName)) {
+                Slog.wtf(LOG_TAG, "Could not find package name for launcher app" + resolveInfo);
+                continue;
+            }
+            final String packageName = resolveInfo.activityInfo.packageName;
+            try {
+                final ApplicationInfo applicationInfo =
+                        mPackageManager.getApplicationInfo(packageName, 0);
+                if (applicationInfo.isSystemApp() || applicationInfo.isUpdatedSystemApp()) {
+                    Log.d(LOG_TAG, "Not suspending system launcher package: " + packageName);
+                    result.add(packageName);
+                }
+            } catch (PackageManager.NameNotFoundException e) {
+                Slog.e(LOG_TAG, "Could not find application info for launcher app: " + packageName);
+            }
+        }
+        return result;
+    }
+
+    private List<String> getAccessibilityServices(int userId) {
+        final List<AccessibilityServiceInfo> accessibilityServiceInfos =
+                getAccessibilityManagerForUser(userId)
+                        .getEnabledAccessibilityServiceList(FEEDBACK_ALL_MASK);
+        final List<String> result = new ArrayList<>();
+        for (final AccessibilityServiceInfo serviceInfo : accessibilityServiceInfos) {
+            final ComponentName componentName =
+                    ComponentName.unflattenFromString(serviceInfo.getId());
+            if (componentName != null) {
+                final String packageName = componentName.getPackageName();
+                Slog.d(LOG_TAG, "Not suspending a11y service: " + packageName);
+                result.add(packageName);
+            }
+        }
+        return result;
+    }
+
+    private List<String> getInputMethodPackages(int userId) {
+        final List<InputMethodInfo> enabledImes =
+                InputMethodManagerInternal.get().getEnabledInputMethodListAsUser(userId);
+        final List<String> result = new ArrayList<>();
+        for (final InputMethodInfo info : enabledImes) {
+            Slog.d(LOG_TAG, "Not suspending IME: " + info.getPackageName());
+            result.add(info.getPackageName());
+        }
+        return result;
+    }
+
+    @Nullable
+    private String getActiveLauncherPackages(int userId) {
+        final Intent intent = new Intent(Intent.ACTION_MAIN);
+        intent.addCategory(Intent.CATEGORY_HOME);
+        intent.addCategory(Intent.CATEGORY_DEFAULT);
+        return getPackageNameForIntent("active launcher", intent, userId);
+    }
+
+    @Nullable
+    private String getSettingsPackageName(int userId) {
+        final Intent intent = new Intent(Settings.ACTION_SETTINGS);
+        intent.addCategory(Intent.CATEGORY_DEFAULT);
+        return getPackageNameForIntent("settings", intent, userId);
+    }
+
+    @Nullable
+    private String getDialerPackage(int userId) {
+        final Intent intent = new Intent(Intent.ACTION_DIAL);
+        intent.addCategory(Intent.CATEGORY_DEFAULT);
+        return getPackageNameForIntent("dialer", intent, userId);
+    }
+
+    @Nullable
+    private String getPackageNameForIntent(String name, Intent intent, int userId) {
+        final ResolveInfo resolveInfo =
+                mPackageManager.resolveActivityAsUser(intent, /* flags= */ 0, userId);
+        if (resolveInfo != null) {
+            final String packageName = resolveInfo.activityInfo.packageName;
+            Slog.d(LOG_TAG, "Not suspending " + name + " package: " + packageName);
+            return packageName;
+        }
+        return null;
+    }
+
+    private List<String> getCriticalPackages() {
+        final List<String> result = Arrays.asList(mContext.getResources()
+                .getStringArray(R.array.config_packagesExemptFromSuspension));
+        Slog.d(LOG_TAG, "Not suspending critical packages: " + String.join(",", result));
+        return result;
+    }
+
+    private boolean hasLauncherIntent(String packageName) {
+        final Intent intentToResolve = new Intent(Intent.ACTION_MAIN);
+        intentToResolve.addCategory(Intent.CATEGORY_LAUNCHER);
+        intentToResolve.setPackage(packageName);
+        final List<ResolveInfo> resolveInfos = mPackageManager.queryIntentActivities(
+                intentToResolve, PackageManager.GET_UNINSTALLED_PACKAGES);
+        return resolveInfos != null && !resolveInfos.isEmpty();
+    }
+
+    private AccessibilityManager getAccessibilityManagerForUser(int userId) {
+        final IBinder iBinder = ServiceManager.getService(Context.ACCESSIBILITY_SERVICE);
+        final IAccessibilityManager service =
+                iBinder == null ? null : IAccessibilityManager.Stub.asInterface(iBinder);
+        return new AccessibilityManager(mContext, service, userId);
+    }
+}