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);
+ }
+}