diff options
author | Pavel Grafov <pgrafov@google.com> | 2020-01-14 12:51:19 +0000 |
---|---|---|
committer | Pavel Grafov <pgrafov@google.com> | 2020-01-23 13:33:39 +0000 |
commit | 499f005e3d95b6be80726c2c95ab7b9e018a44b4 (patch) | |
tree | 5b7a9338759b697c4fb4cc5b266896b42190714a | |
parent | b3191a3396252fe5dcfbcf0f763b00976f3857e6 (diff) |
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
10 files changed, 495 insertions, 3 deletions
diff --git a/api/current.txt b/api/current.txt index 49ee24ae5435..bf5d24d12615 100644 --- a/api/current.txt +++ b/api/current.txt @@ -6853,6 +6853,7 @@ package android.app.admin { 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 @@ package android.app.admin { 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 @@ package android.app.admin { 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 @@ package android.app.admin { 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 9a63c528f488..ffa00472006f 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -2391,6 +2391,28 @@ public class DevicePolicyManager { "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 @@ public class DevicePolicyManager { = "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 @@ public class DevicePolicyManager { } 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 e7667c0a1b4a..b9ffc4e214a6 100644 --- a/core/java/android/app/admin/IDevicePolicyManager.aidl +++ b/core/java/android/app/admin/IDevicePolicyManager.aidl @@ -470,4 +470,7 @@ interface IDevicePolicyManager { 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 7fd444a1a416..fa70139def2d 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 6a00ecbe91bc..1995a04f67ef 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 7e6eb5de25a2..3aa3053fb7ed 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 ad802ff879f2..54b420191deb 100644 --- a/proto/src/system_messages.proto +++ b/proto/src/system_messages.proto @@ -312,5 +312,9 @@ message SystemMessage { // 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 8641059aebd5..43ee97dfa17b 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 @@ abstract class BaseIDevicePolicyManager extends IDevicePolicyManager.Stub { 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 0c79a6f611a5..f22b8db3c982 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 @@ package com.android.server.devicepolicy; 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.DevicePolicyCache; 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.notification.SystemNotificationChannels; 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 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { 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 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { 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 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { 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 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { // 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 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { 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 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { 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 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } 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 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { 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 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { 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 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { synchronized (getLockObject()) { maybeMigrateToProfileOnOrganizationOwnedDeviceLocked(); } + checkPackageSuspensionOnBoot(); break; case SystemService.PHASE_BOOT_COMPLETED: ensureDeviceOwnerUserStarted(); // TODO Consider better place to do this. @@ -3771,6 +3800,34 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } } + 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 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { @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 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } AccessibilityManager accessibilityManager = getAccessibilityManagerForUser(userId); enabledServices = accessibilityManager.getEnabledAccessibilityServiceList( - AccessibilityServiceInfo.FEEDBACK_ALL_MASK); + FEEDBACK_ALL_MASK); } finally { mInjector.binderRestoreCallingIdentity(id); } @@ -15157,4 +15216,121 @@ public class DevicePolicyManagerService extends BaseIDevicePolicyManager { } 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 000000000000..180acc85e5f6 --- /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); + } +} |