From 7361e3f72b4fb0a7ad59087be75366f9642b27b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kr=C3=BCger?= Date: Tue, 13 Aug 2024 13:24:20 +0200 Subject: [PATCH 1/4] GmsCoreHooks: add REQUEST_INSTALL_PACKAGES to GmsCore When setting up a google-managed work profile, GmsCore tries to install the Android Device Policy app For this to work REQUEST_INSTALL_PACKAGES permission needs to be in it's manifest --- services/core/java/com/android/server/pm/ext/GmsCoreHooks.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/core/java/com/android/server/pm/ext/GmsCoreHooks.java b/services/core/java/com/android/server/pm/ext/GmsCoreHooks.java index db24a73d417f8..1a9eb232b355f 100644 --- a/services/core/java/com/android/server/pm/ext/GmsCoreHooks.java +++ b/services/core/java/com/android/server/pm/ext/GmsCoreHooks.java @@ -75,7 +75,7 @@ static boolean shouldSkipPermissionDefinition(String name) { @Override public List addUsesPermissions() { var res = super.addUsesPermissions(); - var l = createUsesPerms(Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, Manifest.permission.READ_PHONE_NUMBERS); + var l = createUsesPerms(Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, Manifest.permission.READ_PHONE_NUMBERS, Manifest.permission.REQUEST_INSTALL_PACKAGES); res.addAll(l); return res; } From 55d959f827aea0d559bd5d998f1370511dad9829 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kr=C3=BCger?= Date: Fri, 21 Mar 2025 15:43:29 +0100 Subject: [PATCH 2/4] DevicePolicyManagerService: add automatic installation of play apps into work profile Currently when creating a work profile with a DPC app that requires play services, the DPC app expects play services to also exist on the work profile Since play services aren't global on GOS, this patch automatically installs them into the work profile Whether an app requires play services is automatically detected Because this needs to happen before the DPC app is triggered in any way in the work profile, this needs to be part of the profile creation itself. --- .../devicepolicy/DevicePolicyGmsHooks.java | 137 ++++++++++++++++++ .../DevicePolicyManagerService.java | 11 +- 2 files changed, 147 insertions(+), 1 deletion(-) create mode 100644 services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyGmsHooks.java diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyGmsHooks.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyGmsHooks.java new file mode 100644 index 0000000000000..818e2490e862f --- /dev/null +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyGmsHooks.java @@ -0,0 +1,137 @@ +package com.android.server.devicepolicy; + +import static android.app.AppOpsManager.MODE_ALLOWED; + + +import android.app.AppOpsManager; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.IPackageManager; +import android.content.pm.PackageManager; +import android.ext.PackageId; +import android.os.RemoteException; +import android.provider.Settings; + +import com.android.server.utils.Slogf; + +import java.util.Arrays; +import java.util.List; + +public class DevicePolicyGmsHooks { + protected static final String LOG_TAG = "DevicePolicyGmsHooks"; + final IPackageManager mIPackageManager; + final AppOpsManager mIAppOpsManager; + final Context mContext; + + public DevicePolicyGmsHooks(IPackageManager _mIPackageManager, Context _mContext, AppOpsManager _mIAppOpsManager) { + mIPackageManager = _mIPackageManager; + mContext = _mContext; + mIAppOpsManager = _mIAppOpsManager; + } + + /** + * Check if app requires play services + */ + private boolean requiresPlay(String pkg, int callerUserId) throws RemoteException { + ApplicationInfo ai = mIPackageManager.getApplicationInfo(pkg, PackageManager.GET_META_DATA, callerUserId); + if (ai.metaData != null) { + int playVersion = ai.metaData.getInt("com.google.android.gms.version", -1); + return playVersion != -1; + } + + return false; + } + + /** + * GrapheneOS Handler to check if any app such as role owner in a profile + * requires play services and install them + */ + public void maybeInstallPlay(int targetUserId, int callerUserId, String[] pkgNames) { + boolean shouldInstall = false; + + for (String pkgName : pkgNames) { + try { + if (requiresPlay(pkgName, callerUserId)) { + Slogf.i(LOG_TAG, "Detected " + pkgName + " needs play services"); + shouldInstall = true; + } + } catch (RemoteException e) { + // Does not happen, same process + } + } + + if (shouldInstall) { + installPlay(targetUserId, callerUserId); + } + } + /** + * GrapheneOS Handler to install sandboxed play into managed user profile + * in order to allow DPC apps that require play services to work normally + */ + private void installPlay(int targetUserId, int callerUserId) { + // TODO: possibly copy permissions from existing install in managing user? + Slogf.i(LOG_TAG, "Installing play for user " + targetUserId); + + // NOTE: we never need to copy GSF over, since it counts as a fresh install, so existence of GSF in owner is irrelevant. + List playPkgList = Arrays.asList(PackageId.GMS_CORE_NAME, PackageId.PLAY_STORE_NAME); + + boolean playAllAvailableOnSystem = true; + + try { + for (final String playPkg : playPkgList) { + ApplicationInfo ai = mIPackageManager.getApplicationInfo(playPkg, 0, callerUserId); + if (ai == null) { + playAllAvailableOnSystem = false; + Slogf.w(LOG_TAG, "Play package missing: " + playPkg); + continue; + } + + // Check signature + if (ai.ext().getPackageId() != PackageId.PLAY_STORE && ai.ext().getPackageId() != PackageId.GMS_CORE) { + playAllAvailableOnSystem = false; + Slogf.w(LOG_TAG, "Play package is not passing signature checks: " + playPkg); + } + } + if (playAllAvailableOnSystem) { + for (final String playPkg : playPkgList) { + if (mIPackageManager.isPackageAvailable(playPkg, targetUserId)) { + Slogf.d(LOG_TAG, "The play package " + + playPkg + " is already installed in " + + "user " + targetUserId); + continue; + } + Slogf.d(LOG_TAG, "Installing play package " + + playPkg + " in user " + targetUserId); + mIPackageManager.installExistingPackageAsUser( + playPkg, + targetUserId, + /* installFlags= */ 0, + PackageManager.INSTALL_REASON_POLICY, + /* whiteListedPermissions= */ null); + } + } else { + // TODO: intent to app store to install play packages? + Slogf.w(LOG_TAG, "Play Services not installed, yet requested for profile!"); + return; + } + + /* Signature check. If play store was already installed into profile earlier, + but with untrusted signature (should not happen) then this will throw */ + ApplicationInfo aiStore = mIPackageManager.getApplicationInfo(PackageId.PLAY_STORE_NAME, 0, targetUserId); + assert aiStore.ext().getPackageId() == PackageId.PLAY_STORE; + + Slogf.d(LOG_TAG, "Granting REQUEST_INSTALL_PACKAGES to Play Store"); + + // We need to grant Play Store "Allow from source" / REQUEST_INSTALL_PACKAGES, + // as this is not possible later if changing that setting is blocked by device policy + // The setting will appear as "set by admin" + + final int storeUid = mIPackageManager.getPackageUid( + PackageId.PLAY_STORE_NAME, /* flags= */ 0, targetUserId); + mIAppOpsManager.setMode(AppOpsManager.OP_REQUEST_INSTALL_PACKAGES, storeUid, + PackageId.PLAY_STORE_NAME, MODE_ALLOWED); + } catch (RemoteException e) { + // Does not happen, same process + } + } +} diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index 949935754b71d..1d46d79e82a0a 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -354,6 +354,7 @@ import android.app.admin.flags.Flags; import android.app.backup.IBackupManager; import android.app.compat.CompatChanges; +import android.app.compat.gms.GmsCompat; import android.app.role.OnRoleHoldersChangedListener; import android.app.role.RoleManager; import android.app.supervision.SupervisionManagerInternal; @@ -397,6 +398,7 @@ import android.content.res.Resources; import android.database.ContentObserver; import android.database.Cursor; +import android.ext.PackageId; import android.graphics.Bitmap; import android.hardware.usb.UsbManager; import android.health.connect.HealthConnectManager; @@ -589,7 +591,7 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { private static final String ATTRIBUTION_TAG = "DevicePolicyManagerService"; - static final boolean VERBOSE_LOG = false; // DO NOT SUBMIT WITH TRUE + static final boolean VERBOSE_LOG = true; // DO NOT SUBMIT WITH TRUE static final String DEVICE_POLICIES_XML = "device_policies.xml"; @@ -22115,6 +22117,13 @@ public UserHandle createAndProvisionManagedProfile( maybeInstallDevicePolicyManagementRoleHolderInUser(userInfo.id, caller); + DevicePolicyGmsHooks hooks = new DevicePolicyGmsHooks(mIPackageManager, mContext, mInjector.getAppOpsManager()); + int userId = userInfo.id; + int callingUserId = caller.getUserId(); + mInjector.binderWithCleanCallingIdentity(() -> { + hooks.maybeInstallPlay(userId, callingUserId, new String[]{admin.getPackageName()}); + }); + installExistingAdminPackage(userInfo.id, admin.getPackageName()); if (!enableAdminAndSetProfileOwner(userInfo.id, caller.getUserId(), admin)) { throw new ServiceSpecificException( From e0983623c72ce9cf5dc8ec02e5a04577a4f6ebe7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kr=C3=BCger?= Date: Fri, 21 Mar 2025 15:44:06 +0100 Subject: [PATCH 3/4] InstallStart: trust play store in work profile When a work profile with play services gets created Play Store can't install any apps as the work profile may have a policy active to forbid unknown sources as it is not aware that play store is not a system app and gets blocked by that policy. Here we detect if the play store is genuine and is trying to install an app in a work profile. If that is the case we allow it to proceed, despite not being a --- .../packageinstaller/InstallStart.java | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java index d28b15fae4548..58c3b23e8b9ea 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/InstallStart.java @@ -30,7 +30,9 @@ import android.content.pm.PackageInstaller; import android.content.pm.PackageInstaller.SessionInfo; import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ProviderInfo; +import android.ext.PackageId; import android.net.Uri; import android.os.Build; import android.os.Bundle; @@ -178,7 +180,22 @@ && checkPermission(Manifest.permission.INSTALL_PACKAGES, /* pid= */ -1, mAbortInstall = true; } - checkDevicePolicyRestrictions(isTrustedSource); + // When a work profile with play services gets created Play Store can't install any apps + // as the work profile may have a policy active to forbid unknown sources as it is not + // aware that play store is not a system app and gets blocked by that policy. + // Here we detect if the play store is genuine and is trying to install an app + // in a work profile. If that is the case we allow it to proceed, despite not being a + // trusted source. + boolean isTrustedPlayStore = false; + try { + isTrustedPlayStore = callingPackage != null && + mPackageManager.getApplicationInfo(callingPackage, 0).ext().getPackageId() == PackageId.PLAY_STORE; + } catch(NameNotFoundException e) { + // Should never happen. + } + boolean isManagedProfile = mUserManager.isManagedProfile(mPackageManager.getUserId()); + + checkDevicePolicyRestrictions(isTrustedSource || (isTrustedPlayStore && isManagedProfile)); final String installerPackageNameFromIntent = getIntent().getStringExtra( Intent.EXTRA_INSTALLER_PACKAGE_NAME); From 4279d22bda7f2385b40eb1e338ea3b6b7f2dd5b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Kr=C3=BCger?= Date: Fri, 21 Mar 2025 20:52:18 +0100 Subject: [PATCH 4/4] DevicePolicyGmsHooks: show toast if install fails --- .../devicepolicy/DevicePolicyGmsHooks.java | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyGmsHooks.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyGmsHooks.java index 818e2490e862f..0ee9b9a04ead9 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyGmsHooks.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyGmsHooks.java @@ -3,6 +3,7 @@ import static android.app.AppOpsManager.MODE_ALLOWED; +import android.annotation.SuppressLint; import android.app.AppOpsManager; import android.content.Context; import android.content.pm.ApplicationInfo; @@ -11,6 +12,7 @@ import android.ext.PackageId; import android.os.RemoteException; import android.provider.Settings; +import android.widget.Toast; import com.android.server.utils.Slogf; @@ -61,14 +63,20 @@ public void maybeInstallPlay(int targetUserId, int callerUserId, String[] pkgNam } if (shouldInstall) { - installPlay(targetUserId, callerUserId); + if (!installPlay(targetUserId, callerUserId)) { + Toast.makeText(mContext.getApplicationContext(), + "Failed to install Google Play in work profile despite requested by work profile App!", + Toast.LENGTH_LONG + ).show(); + } } } /** * GrapheneOS Handler to install sandboxed play into managed user profile * in order to allow DPC apps that require play services to work normally */ - private void installPlay(int targetUserId, int callerUserId) { + @SuppressLint("MissingPermission") + private boolean installPlay(int targetUserId, int callerUserId) { // TODO: possibly copy permissions from existing install in managing user? Slogf.i(LOG_TAG, "Installing play for user " + targetUserId); @@ -112,7 +120,7 @@ private void installPlay(int targetUserId, int callerUserId) { } else { // TODO: intent to app store to install play packages? Slogf.w(LOG_TAG, "Play Services not installed, yet requested for profile!"); - return; + return false; } /* Signature check. If play store was already installed into profile earlier, @@ -130,8 +138,11 @@ but with untrusted signature (should not happen) then this will throw */ PackageId.PLAY_STORE_NAME, /* flags= */ 0, targetUserId); mIAppOpsManager.setMode(AppOpsManager.OP_REQUEST_INSTALL_PACKAGES, storeUid, PackageId.PLAY_STORE_NAME, MODE_ALLOWED); + + return true; } catch (RemoteException e) { // Does not happen, same process + return false; } } }