From 5e1893fd459618df2ef68715a59aef2cbfdcb91f Mon Sep 17 00:00:00 2001 From: inthewaves Date: Tue, 17 Feb 2026 03:49:24 -0800 Subject: [PATCH 1/8] gmscompat: notify for RCS permissions if Bugle tries to verifies number Adds a potential notification for missing RCS permissions when Bugle calls `IConstellationApiService#verifyPhoneNumber` for RCS configuration and activation. Requires GmsCompat changes. --- core/java/android/os/Binder.java | 4 ++ .../android/internal/gmscompat/GmsHooks.java | 52 +++++++++++++++++++ .../android/internal/gmscompat/IGms2Gca.aidl | 2 + .../internal/gmscompat/IGmsCompatLib.java | 20 +++++++ 4 files changed, 78 insertions(+) diff --git a/core/java/android/os/Binder.java b/core/java/android/os/Binder.java index 92e4b8f3e209a..615364d2e66ff 100644 --- a/core/java/android/os/Binder.java +++ b/core/java/android/os/Binder.java @@ -769,11 +769,13 @@ public void attachInterface(@Nullable IInterface owner, @Nullable String descrip if (GmsCompat.isGmsCore()) { mIsGmsServiceBroker = GmsHooks.GMS_SERVICE_BROKER_INTERFACE_DESCRIPTOR.equals(descriptor); + mIsGmsConstellationService = GmsHooks.GMS_CONSTELLATION_SERVICE_INTERFACE_DESCRIPTOR.equals(descriptor); } } private boolean mIsIGmsCallbacks; private boolean mIsGmsServiceBroker; + private boolean mIsGmsConstellationService; /** * Default implementation returns an empty interface name. @@ -1496,6 +1498,8 @@ private boolean execTransactInternal(int code, Parcel data, Parcel reply, int fl try { if (mIsGmsServiceBroker) { onBeginGmsServiceBrokerCallRet = GmsHooks.onBeginGmsServiceBrokerCall(code, data); + } else if (mIsGmsConstellationService) { + GmsHooks.onBeginGmsConstellationServiceCall(code, data); } // TODO(b/299356201) - this logic should not be in Java - it should be in native // code in libbinder so that it works for all binder users. diff --git a/core/java/com/android/internal/gmscompat/GmsHooks.java b/core/java/com/android/internal/gmscompat/GmsHooks.java index 98681d0299e64..3fce37b1116cb 100644 --- a/core/java/com/android/internal/gmscompat/GmsHooks.java +++ b/core/java/com/android/internal/gmscompat/GmsHooks.java @@ -37,7 +37,9 @@ import android.database.Cursor; import android.database.MatrixCursor; import android.database.sqlite.SQLiteOpenHelper; +import android.ext.PackageId; import android.net.Uri; +import android.os.Binder; import android.os.Bundle; import android.os.DeadSystemRuntimeException; import android.os.IBinder; @@ -672,6 +674,56 @@ public static void onEndGmsServiceBrokerCall() { GmcPackageManager.notifyPermissionsChangeListeners(); } + public static final String GMS_CONSTELLATION_SERVICE_INTERFACE_DESCRIPTOR = + "com.google.android.gms.constellation.internal.IConstellationApiService"; + + public static void onBeginGmsConstellationServiceCall(int transactionCode, Parcel data) { + if (transactionCode != 3) { // verifyPhoneNumber V2 method + return; + } + + try { + final var ctx = GmsCompat.appContext(); + if (ctx == null) return; + final var callingPkg = ctx.getPackageManager().getNameForUid(Binder.getCallingUid()); + // Ensure we're only sending RCS permission notifications for Bugle phone number + // verification attempts. + // + // GmsServiceBroker also does validation of allowed packages, but it doesn't seem it's + // restricted to only Bugle. For the Constellation service (155), the + // VerifyPhoneNumberApi__packages_allowed_to_call flag (proto list) also includes other + // apps like com.google.android.dialer, etc. in its default value. + if (!PackageId.BUGLE_NAME.equals(callingPkg)) { + Log.d(TAG, "onBeginGmsConstellationServiceCall code " + transactionCode + ", unexpected callingPkg " + callingPkg); + return; + } + + data.enforceInterface(GMS_CONSTELLATION_SERVICE_INTERFACE_DESCRIPTOR); + // IConstellationCallbacks binder + data.readStrongBinder(); + + if (data.readInt() == 1) { // VerifyPhoneNumberRequest is present + IGmsCompatLib lib = GmsCompatLib.get(); + final String policyId = lib.parseVerifyPhoneNumberRequestForPolicy(data); + final boolean isTs43Verification = policyId != null && + policyId.startsWith("upi-") && + policyId.contains("ts43"); + Log.d(TAG, "onBeginGmsConstellationServiceCall: policyId " + policyId); + // We could also decode the Bundle field in VerifyPhoneNumberRequest which + // stores the key-value "required_consumer_consent" -> "RCS", and check + // for this. However, this is set as an "API param", so it's not as backwards + // compatible or guaranteed as the String property in the VerifyPhoneNumberRequest + // class + GmsCompatApp.iGms2Gca() + .maybeShowRcsRequirementsNotification(isTs43Verification); + } + } catch (Exception e) { + Log.e(TAG, "onBeginGmsConstellationServiceCall: failed", e); + } finally { + data.setDataPosition(0); + } + } + public static IBinder maybeOverrideBinder(IBinder binder) { boolean proceed = GmsCompat.isEnabled() || GmsCompat.isClientOfGmsCore(); if (!proceed) { diff --git a/core/java/com/android/internal/gmscompat/IGms2Gca.aidl b/core/java/com/android/internal/gmscompat/IGms2Gca.aidl index a6aa9f35aea3b..c4a30840c1f48 100644 --- a/core/java/com/android/internal/gmscompat/IGms2Gca.aidl +++ b/core/java/com/android/internal/gmscompat/IGms2Gca.aidl @@ -47,4 +47,6 @@ interface IGms2Gca { Notification getMediaProjectionNotification(); void raisePackageToForeground(String targetPkg, long durationMs, @nullable String reason, int reasonCode); + + oneway void maybeShowRcsRequirementsNotification(boolean isTs43Verification); } diff --git a/core/java/com/android/internal/gmscompat/IGmsCompatLib.java b/core/java/com/android/internal/gmscompat/IGmsCompatLib.java index 556eaeaefc63c..837189b72aafd 100644 --- a/core/java/com/android/internal/gmscompat/IGmsCompatLib.java +++ b/core/java/com/android/internal/gmscompat/IGmsCompatLib.java @@ -7,9 +7,11 @@ import android.content.ServiceConnection; import android.os.BinderProxy; import android.os.IInterface; +import android.os.Parcel; import android.os.UserHandle; import java.io.FileDescriptor; +import java.text.ParseException; import java.util.concurrent.Executor; public interface IGmsCompatLib { @@ -26,4 +28,22 @@ public interface IGmsCompatLib { /** @see BinderProxy#dump(FileDescriptor, String[]) * @see BinderProxy#dumpAsync(FileDescriptor, String[]) */ boolean maybeInterceptBinderProxyDump(BinderProxy binderProxy, FileDescriptor fd, String[] args, boolean async); + + /** + * Parses a {@code com.google.android.gms.constellation.VerifyPhoneNumberRequest} and returns + * its policy id. + *

+ * This is not a replacement for {@link Parcel#readTypedObject}, so a + * {@link Parcel#readInt} == 1 check should be done beforehand if {@code data} is from a + * transaction. This is more of a replacement for direct {@code + * VerifyPhoneNumberRequest.CREATOR} usage. + *

+ * If any exceptions are thrown (exceptions from {@link Parcel} reading), the data position will + * be in an undefined position. + * + * @param data The data where the {@code VerifyPhoneNumberRequest} is in. Initial data position + * should be at the header of the {@code VerifyPhoneNumberRequest}. + */ + @Nullable + String parseVerifyPhoneNumberRequestForPolicy(Parcel data) throws ParseException; } From 723fb8393763e40e4af2c332f50df32f5dc16a1c Mon Sep 17 00:00:00 2001 From: inthewaves Date: Fri, 20 Feb 2026 05:31:16 -0800 Subject: [PATCH 2/8] gmscompat: allow GmsCompat app to read/write package flags This is needed now that the GmsCompat app has the configuration UIs instead of PermissionController. --- .../java/com/android/server/pm/GosPackageStatePermissions.java | 1 + 1 file changed, 1 insertion(+) diff --git a/services/core/java/com/android/server/pm/GosPackageStatePermissions.java b/services/core/java/com/android/server/pm/GosPackageStatePermissions.java index f3fcd750cfe45..5c1348efce7f5 100644 --- a/services/core/java/com/android/server/pm/GosPackageStatePermissions.java +++ b/services/core/java/com/android/server/pm/GosPackageStatePermissions.java @@ -117,6 +117,7 @@ static void init(PackageManagerService pm) { builder() .readFlags(playIntegrityFlags) .readWriteFlag(SUPPRESS_PLAY_INTEGRITY_API_NOTIF) + .readWriteFields(FIELD_PACKAGE_FLAGS) .apply(GmsCompatApp.PKG_NAME, computer); @GosPackageStateFlag.Enum int[] settingsReadWriteFlags = { From bc499a198b0ce8e2cdb12e5bb47559a38e892318 Mon Sep 17 00:00:00 2001 From: inthewaves Date: Wed, 18 Feb 2026 21:02:52 -0800 Subject: [PATCH 3/8] [temporary] gmscompat: add TODO for USE_ICC_AUTH for RCS in Android 17 --- core/java/android/app/ContextImpl.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index 395fe57904ac1..af1dc485695a6 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -2610,6 +2610,11 @@ public int checkPermission(String permission, int pid, int uid) { throw new IllegalArgumentException("permission is null"); } + // TODO: In Android 17, READ_PRIVILEGED_PHONE_STATE will no longer be accepted for + // TelephonyManager#getIccAuthentication. However, Manifest.permission.USE_ICC_AUTH was + // introduced. getIccAuthentication can accept this new permission along with preexisting + // Manifest.permission.USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER. Need to pay attention to how + // GmsCore uses USE_ICC_AUTH or USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER. if (GmsCompat.isGmsCore() && android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE.equals(permission)) { if (Log.isLoggable(TAG_SPOOF, Log.VERBOSE)) { From 61980a6b5fa49d3d91eeae11d3f149c1532e7b7b Mon Sep 17 00:00:00 2001 From: inthewaves Date: Fri, 20 Feb 2026 03:30:55 -0800 Subject: [PATCH 4/8] add definition of new UPDATE_AND_INVALIDATE_PERMISSION_STATE permission This is needed now that GmsCompat configuration screens are now in the GmsCompat app instead of PermissionController. --- core/res/AndroidManifest.xml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 89a74baa9ed6b..0c55f4a0629d1 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -6620,6 +6620,10 @@ + + + Date: Thu, 26 Feb 2026 14:25:00 -0800 Subject: [PATCH 5/8] Partially revert "add method for updating cached package permission state" This partially reverts commit 360638e0a1d8c61ad0b3696360535ed182f6f19e --- core/api/system-current.txt | 1 - .../android/permission/IPermissionManager.aidl | 2 -- .../android/permission/PermissionManager.java | 8 -------- .../pm/permission/PermissionManagerService.java | 15 --------------- 4 files changed, 26 deletions(-) diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 1e6f13b34a0fb..5aec7caaa5ae6 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -12845,7 +12845,6 @@ package android.permission { method @RequiresPermission(android.Manifest.permission.MANAGE_ONE_TIME_PERMISSION_SESSIONS) public void startOneTimePermissionSession(@NonNull String, long, long, int, int); method @RequiresPermission(android.Manifest.permission.MANAGE_ONE_TIME_PERMISSION_SESSIONS) public void stopOneTimePermissionSession(@NonNull String); method @FlaggedApi("android.permission.flags.device_aware_permission_apis_enabled") @RequiresPermission(anyOf={android.Manifest.permission.GRANT_RUNTIME_PERMISSIONS, android.Manifest.permission.REVOKE_RUNTIME_PERMISSIONS}) public void updatePermissionFlags(@NonNull String, @NonNull String, @NonNull String, int, int); - method public void updatePermissionState(@NonNull String, int); field @RequiresPermission(android.Manifest.permission.START_REVIEW_PERMISSION_DECISIONS) public static final String ACTION_REVIEW_PERMISSION_DECISIONS = "android.permission.action.REVIEW_PERMISSION_DECISIONS"; field public static final String EXTRA_PERMISSION_USAGES = "android.permission.extra.PERMISSION_USAGES"; field public static final int PERMISSION_GRANTED = 0; // 0x0 diff --git a/core/java/android/permission/IPermissionManager.aidl b/core/java/android/permission/IPermissionManager.aidl index fd7b3522d028f..b37581260bb14 100644 --- a/core/java/android/permission/IPermissionManager.aidl +++ b/core/java/android/permission/IPermissionManager.aidl @@ -110,8 +110,6 @@ interface IPermissionManager { Map getAllPermissionStates(String packageName, String persistentDeviceId, int userId); int getPermissionRequestState(String packageName, String permissionName, int deviceId); - - void updatePermissionState(String packageName, int userId); } /** diff --git a/core/java/android/permission/PermissionManager.java b/core/java/android/permission/PermissionManager.java index f13720c699eb5..ff33ab4ed2ee2 100644 --- a/core/java/android/permission/PermissionManager.java +++ b/core/java/android/permission/PermissionManager.java @@ -2282,12 +2282,4 @@ public String toString() { + '}'; } } - - public void updatePermissionState(@NonNull String packageName, int userId) { - try { - mPermissionManager.updatePermissionState(packageName, userId); - } catch (RemoteException e) { - e.rethrowFromSystemServer(); - } - } } diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java index f70c54bd85f71..20063b7331331 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java @@ -495,21 +495,6 @@ private List getPackagesWithAutoRevokePolicy(int autoRevokePolicy, int u return result; } - @Override - public void updatePermissionState(String packageName, int userId) { - mContext.enforceCallingPermission( - android.Manifest.permission.GRANT_RUNTIME_PERMISSIONS, - "updatePermissionState"); - - PackageState packageState = mPackageManagerInt.getPackageStateInternal(packageName); - if (packageState == null) { - Slog.w(LOG_TAG, "updatePermissionState: no PackageState for " + packageName); - return; - } - - mPermissionManagerServiceImpl.updatePermissions(packageState, userId); - } - /* Start of delegate methods to PermissionManagerServiceInterface */ @Override From 631ecd6319359cee967fe4ca6f3191331824cacd Mon Sep 17 00:00:00 2001 From: inthewaves Date: Fri, 20 Feb 2026 04:50:14 -0800 Subject: [PATCH 6/8] add method for updating and invalidating package state (This commit could probably be combined with commit "add method for updating cached package permission state", 360638e0a1d8c61ad0b3696360535ed182f6f19e) Package permission state is updated automatically for all packages after events that might impact it, e.g. after package install or uninstall, after storage volume mount, after OS update etc. On GrapheneOS, per-package permission policy can be changed via GosPackageState packageFlags. This new method is needed for updating the cached permission state manually after packageFlags change. --- .../permission/IPermissionManager.aidl | 2 + .../android/permission/PermissionManager.java | 9 +++ .../permission/PermissionManagerService.java | 65 +++++++++++++++++++ 3 files changed, 76 insertions(+) diff --git a/core/java/android/permission/IPermissionManager.aidl b/core/java/android/permission/IPermissionManager.aidl index b37581260bb14..c37c4e0a739d6 100644 --- a/core/java/android/permission/IPermissionManager.aidl +++ b/core/java/android/permission/IPermissionManager.aidl @@ -110,6 +110,8 @@ interface IPermissionManager { Map getAllPermissionStates(String packageName, String persistentDeviceId, int userId); int getPermissionRequestState(String packageName, String permissionName, int deviceId); + + void updatePermissionStateAndInvalidateCache(String packageName, int userId); } /** diff --git a/core/java/android/permission/PermissionManager.java b/core/java/android/permission/PermissionManager.java index ff33ab4ed2ee2..30719c31c8aa4 100644 --- a/core/java/android/permission/PermissionManager.java +++ b/core/java/android/permission/PermissionManager.java @@ -2282,4 +2282,13 @@ public String toString() { + '}'; } } + + /** @hide */ + public void updatePermissionStateAndInvalidateCache(@NonNull String packageName, int userId) { + try { + mPermissionManager.updatePermissionStateAndInvalidateCache(packageName, userId); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } } diff --git a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java index 20063b7331331..ea067a3eb80ea 100644 --- a/services/core/java/com/android/server/pm/permission/PermissionManagerService.java +++ b/services/core/java/com/android/server/pm/permission/PermissionManagerService.java @@ -39,6 +39,7 @@ import android.annotation.SpecialUsers.CanBeALL; import android.annotation.UserIdInt; import android.app.ActivityManager; +import android.app.AppGlobals; import android.app.AppOpsManager; import android.app.AppOpsManager.AttributionFlags; import android.app.IActivityManager; @@ -495,6 +496,70 @@ private List getPackagesWithAutoRevokePolicy(int autoRevokePolicy, int u return result; } + @Override + public void updatePermissionStateAndInvalidateCache(String packageName, int userId) { + mContext.enforceCallingPermission( + android.Manifest.permission.UPDATE_AND_INVALIDATE_PERMISSION_STATE, + "updatePermissionStateAndInvalidateCache"); + + final int callingUid = Binder.getCallingUid(); + if (UserHandle.getUserId(callingUid) != userId) { + final boolean isCallerAllowed = mContext.checkCallingPermission( + Manifest.permission.INTERACT_ACROSS_USERS_FULL) + == PackageManager.PERMISSION_GRANTED; + if (!isCallerAllowed) { + throw new SecurityException( + "uid " + callingUid + " can't update state for " + userId); + } + Slog.w(LOG_TAG, "updatePermissionStateAndInvalidateCache: cross user granted for " + callingUid); + } + + final String callingPackage; + { + final AndroidPackage callingAndroidPackage = mPackageManagerInt.getPackage(callingUid); + if (callingAndroidPackage == null) { + Slog.w(LOG_TAG, "updatePermissionStateAndInvalidateCache: no callingPackage " + + "for uid " + callingUid); + return; + } + callingPackage = callingAndroidPackage.getPackageName(); + } + + PackageState packageState = mPackageManagerInt.getPackageStateInternal(packageName); + if (packageState == null) { + Slog.w(LOG_TAG, "updatePermissionStateAndInvalidateCache: no PackageState " + + "for " + packageName); + return; + } + mPermissionManagerServiceImpl.updatePermissions(packageState, userId); + + final var pm = AppGlobals.getPackageManager(); + + final long identity = Binder.clearCallingIdentity(); + try { + final var appInfo = pm.getApplicationInfo(packageName, 0, userId); + if (appInfo == null) { + Slog.w(LOG_TAG, "updatePermissionStateAndInvalidateCache: no " + + "appInfo for " + packageName); + return; + } + + if (appInfo.enabled) { + // Invalidate cached system_server state; clearing identity, since this requires + // Manifest.permission.CHANGE_COMPONENT_ENABLED_STATE + pm.setApplicationEnabledSetting(packageName, + PackageManager.COMPONENT_ENABLED_STATE_DISABLED, 0, userId, callingPackage); + pm.setApplicationEnabledSetting(packageName, + PackageManager.COMPONENT_ENABLED_STATE_ENABLED, 0, userId, callingPackage); + } + } catch (RemoteException e) { + Slog.e(LOG_TAG, "updatePermissionStateAndInvalidateCache: failed " + + "for " + packageName, e); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + /* Start of delegate methods to PermissionManagerServiceInterface */ @Override From 842fe1ca26fe76d98dc74dcbe2448a0d21e0d531 Mon Sep 17 00:00:00 2001 From: inthewaves Date: Thu, 26 Feb 2026 14:25:28 -0800 Subject: [PATCH 7/8] fixup! gosps: support for per-app GrapheneOS-specific persistent state --- .../java/com/android/server/pm/GosPackageStatePmHooks.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/core/java/com/android/server/pm/GosPackageStatePmHooks.java b/services/core/java/com/android/server/pm/GosPackageStatePmHooks.java index c5fcdea679ac8..8ef17565fe069 100644 --- a/services/core/java/com/android/server/pm/GosPackageStatePmHooks.java +++ b/services/core/java/com/android/server/pm/GosPackageStatePmHooks.java @@ -341,7 +341,8 @@ static int runShellCommand(PackageManagerShellCommand cmd) { return 1; } if (updatePermissionState) { - cmd.mPermissionManager.updatePermissionState(packageName, userId); + cmd.mPermissionManager.updatePermissionStateAndInvalidateCache( + packageName, userId); } return 0; } From ab8658d28801f5156b07b3de76862ab5e97381d1 Mon Sep 17 00:00:00 2001 From: inthewaves Date: Thu, 26 Feb 2026 15:36:36 -0800 Subject: [PATCH 8/8] grant UPDATE_AND_INVALIDATE_PERMISSION_STATE to Shell --- packages/Shell/AndroidManifest.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index b67b3a20db806..7171e43e97d55 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -1052,6 +1052,8 @@ + +