From 4b2d001583b064208b1f426b878776ec57815325 Mon Sep 17 00:00:00 2001 From: Cabbache Date: Sun, 8 Feb 2026 22:13:05 +0100 Subject: [PATCH] add microphone scopes framework support Add GosPackageState flag and persistence for microphone scopes configuration. Expose MicrophoneScopeInfo through PackageManagerNative for native services to query per-app audio file paths. Spoof RECORD_AUDIO permission checks in-process and bypass foreground service type validation for apps with microphone scopes enabled. --- core/api/system-current.txt | 4 + .../java/android/app/ActivityThreadHooks.java | 2 + .../content/pm/AppPermissionUtils.java | 16 ++++ .../android/content/pm/GosPackageState.java | 26 ++++- .../content/pm/GosPackageStateFlag.java | 2 + core/java/android/ext/DerivedPackageFlag.java | 2 + .../internal/app/MicrophoneScopes.java | 72 ++++++++++++++ .../com/android/server/am/ActiveServices.java | 37 ++++++-- .../server/pm/GosPackageStatePermission.java | 5 +- .../server/pm/GosPackageStatePermissions.java | 12 ++- .../server/pm/GosPackageStatePersistence.java | 15 ++- .../server/pm/GosPackageStatePmHooks.java | 8 +- .../server/pm/PackageManagerNative.java | 94 +++++++++++++++++++ .../pm/PackageManagerSettingsTests.java | 3 +- 14 files changed, 276 insertions(+), 22 deletions(-) create mode 100644 core/java/com/android/internal/app/MicrophoneScopes.java diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 880e7d4c90dcf..0cea2ef118be3 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -4365,6 +4365,7 @@ package android.content.pm { method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator CREATOR; field @Nullable public final byte[] contactScopes; + field @Nullable public final byte[] microphoneScopes; field @Nullable public final byte[] storageScopes; } @@ -4378,6 +4379,7 @@ package android.content.pm { method @NonNull public android.content.pm.GosPackageState.Editor setContactScopes(@Nullable byte[]); method @NonNull public android.content.pm.GosPackageState.Editor setFlagState(int, boolean); method @NonNull public android.content.pm.GosPackageState.Editor setKillUidAfterApply(boolean); + method @NonNull public android.content.pm.GosPackageState.Editor setMicrophoneScopes(@Nullable byte[]); method @NonNull public android.content.pm.GosPackageState.Editor setNotifyUidAfterApply(boolean); method @NonNull public android.content.pm.GosPackageState.Editor setPackageFlagState(int, boolean); method @NonNull public android.content.pm.GosPackageState.Editor setStorageScopes(@Nullable byte[]); @@ -4385,6 +4387,7 @@ package android.content.pm { public interface GosPackageStateFlag { field public static final int CONTACT_SCOPES_ENABLED = 5; // 0x5 + field public static final int MICROPHONE_SCOPES_ENABLED = 29; // 0x1d field public static final int STORAGE_SCOPES_ENABLED = 0; // 0x0 } @@ -5275,6 +5278,7 @@ package android.ext { field public static final int HAS_READ_MEDIA_IMAGES_DECLARATION = 1024; // 0x400 field public static final int HAS_READ_MEDIA_VIDEO_DECLARATION = 2048; // 0x800 field public static final int HAS_READ_MEDIA_VISUAL_USER_SELECTED_DECLARATION = 4096; // 0x1000 + field public static final int HAS_RECORD_AUDIO_DECLARATION = 8388608; // 0x800000 field public static final int HAS_WRITE_CONTACTS_DECLARATION = 2097152; // 0x200000 field public static final int HAS_WRITE_EXTERNAL_STORAGE_DECLARATION = 32; // 0x20 } diff --git a/core/java/android/app/ActivityThreadHooks.java b/core/java/android/app/ActivityThreadHooks.java index ba92e565b13ae..8220e1b46b7a7 100644 --- a/core/java/android/app/ActivityThreadHooks.java +++ b/core/java/android/app/ActivityThreadHooks.java @@ -12,6 +12,7 @@ import android.util.Log; import com.android.internal.app.ContactScopes; +import com.android.internal.app.MicrophoneScopes; import com.android.internal.app.StorageScopesAppHooks; import com.android.internal.gmscompat.GmsHooks; @@ -74,6 +75,7 @@ static void onBind2(Context appContext, Bundle appBindArgs) { static void onGosPackageStateChanged(Context ctx, GosPackageState state, boolean fromBind) { StorageScopesAppHooks.maybeEnable(state); ContactScopes.maybeEnable(ctx, state); + MicrophoneScopes.maybeEnable(ctx, state); } static Service instantiateService(String className) { diff --git a/core/java/android/content/pm/AppPermissionUtils.java b/core/java/android/content/pm/AppPermissionUtils.java index 6d3a18f6359eb..e2f60673d5820 100644 --- a/core/java/android/content/pm/AppPermissionUtils.java +++ b/core/java/android/content/pm/AppPermissionUtils.java @@ -21,6 +21,7 @@ import android.app.compat.gms.GmsCompat; import com.android.internal.app.ContactScopes; +import com.android.internal.app.MicrophoneScopes; import com.android.internal.app.StorageScopesAppHooks; import com.android.internal.gmscompat.GmsHooks; @@ -47,6 +48,10 @@ public static boolean shouldSpoofSelfCheck(String permName) { return true; } + if (MicrophoneScopes.shouldSpoofSelfPermissionCheck(permName)) { + return true; + } + if (GmsCompat.isEnabled()) { if (GmsHooks.config().shouldSpoofSelfPermissionCheck(permName)) { return true; @@ -70,6 +75,10 @@ public static boolean shouldSpoofSelfAppOpCheck(int op) { return true; } + if (MicrophoneScopes.shouldSpoofSelfAppOpCheck(op)) { + return true; + } + return false; } @@ -108,6 +117,13 @@ private static int getSpoofablePermissionDflag(GosPackageState ps, String perm, } } + if (ps.hasFlag(GosPackageStateFlag.MICROPHONE_SCOPES_ENABLED)) { + int permDflag = MicrophoneScopes.getSpoofablePermissionDflag(perm); + if (permDflag != 0) { + return permDflag; + } + } + return 0; } diff --git a/core/java/android/content/pm/GosPackageState.java b/core/java/android/content/pm/GosPackageState.java index 91daf0b273226..e1fe6f42d9d35 100644 --- a/core/java/android/content/pm/GosPackageState.java +++ b/core/java/android/content/pm/GosPackageState.java @@ -37,6 +37,8 @@ public final class GosPackageState implements Parcelable { public final byte[] storageScopes; @Nullable public final byte[] contactScopes; + @Nullable + public final byte[] microphoneScopes; /** * These flags are lazily derived from persistent state. They are intentionally skipped from * equals() and hashCode(). derivedFlags are stored here for performance reasons, to avoid @@ -63,15 +65,17 @@ public final class GosPackageState implements Parcelable { /** @hide */ public GosPackageState(long flagStorage1, long packageFlagStorage, - @Nullable byte[] storageScopes, @Nullable byte[] contactScopes) { + @Nullable byte[] storageScopes, @Nullable byte[] contactScopes, + @Nullable byte[] microphoneScopes) { this.flagStorage1 = flagStorage1; this.packageFlagStorage = packageFlagStorage; this.storageScopes = storageScopes; this.contactScopes = contactScopes; + this.microphoneScopes = microphoneScopes; } private static GosPackageState createEmpty() { - return new GosPackageState(0L, 0L, null, null); + return new GosPackageState(0L, 0L, null, null, null); } private static final int TYPE_NONE = 0; @@ -94,6 +98,7 @@ public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeLong(this.packageFlagStorage); dest.writeByteArray(storageScopes); dest.writeByteArray(contactScopes); + dest.writeByteArray(microphoneScopes); dest.writeInt(derivedFlags); } @@ -106,7 +111,7 @@ public GosPackageState createFromParcel(Parcel in) { case TYPE_NONE: return NONE; }; var res = new GosPackageState(in.readLong(), in.readLong(), - in.createByteArray(), in.createByteArray()); + in.createByteArray(), in.createByteArray(), in.createByteArray()); res.derivedFlags = in.readInt(); return res; } @@ -119,7 +124,7 @@ public GosPackageState[] newArray(int size) { @Override public int hashCode() { - return Long.hashCode(flagStorage1) + Arrays.hashCode(storageScopes) + Arrays.hashCode(contactScopes) + Long.hashCode(packageFlagStorage); + return Long.hashCode(flagStorage1) + Arrays.hashCode(storageScopes) + Arrays.hashCode(contactScopes) + Arrays.hashCode(microphoneScopes) + Long.hashCode(packageFlagStorage); } @Override @@ -139,6 +144,9 @@ public boolean equals(Object obj) { if (!Arrays.equals(contactScopes, o.contactScopes)) { return false; } + if (!Arrays.equals(microphoneScopes, o.microphoneScopes)) { + return false; + } if (packageFlagStorage != o.packageFlagStorage) { return false; } @@ -236,6 +244,7 @@ public static class Editor { private long packageFlagStorage; private byte[] storageScopes; private byte[] contactScopes; + private byte[] microphoneScopes; private int editorFlags; /** @hide */ @@ -246,6 +255,7 @@ public Editor(GosPackageState s, String packageName, int userId) { this.packageFlagStorage = s.packageFlagStorage; this.storageScopes = s.storageScopes; this.contactScopes = s.contactScopes; + this.microphoneScopes = s.microphoneScopes; } @NonNull @@ -304,6 +314,12 @@ public Editor setContactScopes(@Nullable byte[] contactScopes) { return this; } + @NonNull + public Editor setMicrophoneScopes(@Nullable byte[] microphoneScopes) { + this.microphoneScopes = microphoneScopes; + return this; + } + @NonNull public Editor killUidAfterApply() { return setKillUidAfterApply(true); @@ -334,7 +350,7 @@ public Editor setNotifyUidAfterApply(boolean v) { public boolean apply() { try { return ActivityThread.getPackageManager().setGosPackageState(packageName, userId, - new GosPackageState(flagStorage1, packageFlagStorage, storageScopes, contactScopes), + new GosPackageState(flagStorage1, packageFlagStorage, storageScopes, contactScopes, microphoneScopes), editorFlags); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); diff --git a/core/java/android/content/pm/GosPackageStateFlag.java b/core/java/android/content/pm/GosPackageStateFlag.java index 21abe5f9398d0..a371882245d83 100644 --- a/core/java/android/content/pm/GosPackageStateFlag.java +++ b/core/java/android/content/pm/GosPackageStateFlag.java @@ -36,6 +36,7 @@ public interface GosPackageStateFlag { /** @hide */ int PLAY_INTEGRITY_API_USED_AT_LEAST_ONCE = 26; /** @hide */ int SUPPRESS_PLAY_INTEGRITY_API_NOTIF = 27; /** @hide */ int BLOCK_PLAY_INTEGRITY_API = 28; + /* SysApi */ int MICROPHONE_SCOPES_ENABLED = 29; /** @hide */ @IntDef(value = { @@ -64,6 +65,7 @@ public interface GosPackageStateFlag { PLAY_INTEGRITY_API_USED_AT_LEAST_ONCE, SUPPRESS_PLAY_INTEGRITY_API_NOTIF, BLOCK_PLAY_INTEGRITY_API, + MICROPHONE_SCOPES_ENABLED, }) @Retention(RetentionPolicy.SOURCE) @interface Enum {} diff --git a/core/java/android/ext/DerivedPackageFlag.java b/core/java/android/ext/DerivedPackageFlag.java index 5258acf924327..97563d4c0deb8 100644 --- a/core/java/android/ext/DerivedPackageFlag.java +++ b/core/java/android/ext/DerivedPackageFlag.java @@ -27,6 +27,7 @@ public interface DerivedPackageFlag { int HAS_READ_CONTACTS_DECLARATION = 1 << 20; int HAS_WRITE_CONTACTS_DECLARATION = 1 << 21; int HAS_GET_ACCOUNTS_DECLARATION = 1 << 22; + int HAS_RECORD_AUDIO_DECLARATION = 1 << 23; /** @hide */ @IntDef(flag = true, value = { @@ -47,6 +48,7 @@ public interface DerivedPackageFlag { HAS_READ_CONTACTS_DECLARATION, HAS_WRITE_CONTACTS_DECLARATION, HAS_GET_ACCOUNTS_DECLARATION, + HAS_RECORD_AUDIO_DECLARATION, }) @Retention(RetentionPolicy.SOURCE) @interface Enum {} diff --git a/core/java/com/android/internal/app/MicrophoneScopes.java b/core/java/com/android/internal/app/MicrophoneScopes.java new file mode 100644 index 0000000000000..dba200e439a26 --- /dev/null +++ b/core/java/com/android/internal/app/MicrophoneScopes.java @@ -0,0 +1,72 @@ +package com.android.internal.app; + +import android.Manifest; +import android.annotation.AnyThread; +import android.app.AppOpsManager; +import android.content.Context; +import android.content.pm.GosPackageState; +import android.content.pm.GosPackageStateFlag; +import android.ext.DerivedPackageFlag; + +public class MicrophoneScopes { + private static volatile boolean isEnabled; + private static int gosPsDerivedFlags; + + public static boolean isEnabled() { + return isEnabled; + } + + @AnyThread + public static void maybeEnable(Context ctx, GosPackageState ps) { + synchronized (MicrophoneScopes.class) { + if (isEnabled) { + return; + } + + if (ps.hasFlag(GosPackageStateFlag.MICROPHONE_SCOPES_ENABLED)) { + gosPsDerivedFlags = ps.derivedFlags; + isEnabled = true; + } + } + } + + // call only if isEnabled is true + private static boolean shouldSpoofPermissionCheckInner(int permDflag) { + if (permDflag == 0) { + return false; + } + return (gosPsDerivedFlags & permDflag) != 0; + } + + public static boolean shouldSpoofSelfPermissionCheck(String permName) { + if (!isEnabled) { + return false; + } + return shouldSpoofPermissionCheckInner(getSpoofablePermissionDflag(permName)); + } + + public static boolean shouldSpoofSelfAppOpCheck(int op) { + if (!isEnabled) { + return false; + } + return shouldSpoofPermissionCheckInner(getSpoofableAppOpPermissionDflag(op)); + } + + public static int getSpoofablePermissionDflag(String permName) { + switch (permName) { + case Manifest.permission.RECORD_AUDIO: + return DerivedPackageFlag.HAS_RECORD_AUDIO_DECLARATION; + default: + return 0; + } + } + + private static int getSpoofableAppOpPermissionDflag(int op) { + switch (op) { + case AppOpsManager.OP_RECORD_AUDIO: + return DerivedPackageFlag.HAS_RECORD_AUDIO_DECLARATION; + default: + return 0; + } + } +} diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index 989a0ce31a20a..0fd293887e517 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -187,6 +187,8 @@ import android.content.pm.PackageManagerInternal; import android.content.pm.ParceledListSlice; import android.content.pm.ResolveInfo; +import android.content.pm.GosPackageState; +import android.content.pm.GosPackageStateFlag; import android.content.pm.ServiceInfo; import android.content.pm.ServiceInfo.ForegroundServiceType; import android.os.Build; @@ -2644,13 +2646,34 @@ must call startForeground() within a timeout anyway, so we don't need this } fgsTypeCheckCode = fgsTypeResult.first; if (fgsTypeResult.second != null) { - logFGSStateChangeLocked(r, - FOREGROUND_SERVICE_STATE_CHANGED__STATE__DENIED, - 0, FGS_STOP_REASON_UNKNOWN, fgsTypeResult.first, - FOREGROUND_SERVICE_STATE_CHANGED__FGS_START_API__FGSSTARTAPI_NA, - false /* fgsRestrictionRecalculated */ - ); - throw fgsTypeResult.second; + if ((foregroundServiceType + & ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE) != 0) { + int userId = UserHandle.getUserId(r.appInfo.uid); + GosPackageState gosPs = GosPackageState.get( + r.packageName, userId); + if (gosPs.hasFlag( + GosPackageStateFlag.MICROPHONE_SCOPES_ENABLED)) { + Slog.i(TAG, "Allowing microphone FGS for app with" + + " microphone scopes: " + r.packageName); + fgsTypeCheckCode = FGS_TYPE_POLICY_CHECK_OK; + } else { + logFGSStateChangeLocked(r, + FOREGROUND_SERVICE_STATE_CHANGED__STATE__DENIED, + 0, FGS_STOP_REASON_UNKNOWN, fgsTypeResult.first, + FOREGROUND_SERVICE_STATE_CHANGED__FGS_START_API__FGSSTARTAPI_NA, + false /* fgsRestrictionRecalculated */ + ); + throw fgsTypeResult.second; + } + } else { + logFGSStateChangeLocked(r, + FOREGROUND_SERVICE_STATE_CHANGED__STATE__DENIED, + 0, FGS_STOP_REASON_UNKNOWN, fgsTypeResult.first, + FOREGROUND_SERVICE_STATE_CHANGED__FGS_START_API__FGSSTARTAPI_NA, + false /* fgsRestrictionRecalculated */ + ); + throw fgsTypeResult.second; + } } } } diff --git a/services/core/java/com/android/server/pm/GosPackageStatePermission.java b/services/core/java/com/android/server/pm/GosPackageStatePermission.java index 1013ec825b420..740d8114c4ba7 100644 --- a/services/core/java/com/android/server/pm/GosPackageStatePermission.java +++ b/services/core/java/com/android/server/pm/GosPackageStatePermission.java @@ -25,9 +25,10 @@ class GosPackageStatePermission { static final int FIELD_STORAGE_SCOPES = 0; static final int FIELD_CONTACT_SCOPES = 1; static final int FIELD_PACKAGE_FLAGS = 2; + static final int FIELD_MICROPHONE_SCOPES = 3; @IntDef(prefix = "FIELD_", value = { - FIELD_STORAGE_SCOPES, FIELD_CONTACT_SCOPES, FIELD_PACKAGE_FLAGS + FIELD_STORAGE_SCOPES, FIELD_CONTACT_SCOPES, FIELD_PACKAGE_FLAGS, FIELD_MICROPHONE_SCOPES }) @Retention(RetentionPolicy.SOURCE) @interface Field {} @@ -214,6 +215,7 @@ GosPackageState filterRead(GosPackageState ps) { , canReadField(FIELD_PACKAGE_FLAGS) ? ps.packageFlagStorage : default_.packageFlagStorage , canReadField(FIELD_STORAGE_SCOPES) ? ps.storageScopes : default_.storageScopes , canReadField(FIELD_CONTACT_SCOPES) ? ps.contactScopes : default_.contactScopes + , canReadField(FIELD_MICROPHONE_SCOPES) ? ps.microphoneScopes : default_.microphoneScopes ); if (default_.equals(res)) { return default_; @@ -235,6 +237,7 @@ GosPackageState filterWrite(GosPackageState current, GosPackageState update) { , canWriteField(FIELD_PACKAGE_FLAGS) ? update.packageFlagStorage : current.packageFlagStorage , canWriteField(FIELD_STORAGE_SCOPES) ? update.storageScopes : current.storageScopes , canWriteField(FIELD_CONTACT_SCOPES) ? update.contactScopes : current.contactScopes + , canWriteField(FIELD_MICROPHONE_SCOPES) ? update.microphoneScopes : current.microphoneScopes ); var default_ = GosPackageState.DEFAULT; if (default_.equals(res)) { diff --git a/services/core/java/com/android/server/pm/GosPackageStatePermissions.java b/services/core/java/com/android/server/pm/GosPackageStatePermissions.java index f3fcd750cfe45..884ca796f3ab7 100644 --- a/services/core/java/com/android/server/pm/GosPackageStatePermissions.java +++ b/services/core/java/com/android/server/pm/GosPackageStatePermissions.java @@ -27,6 +27,7 @@ import static android.content.pm.GosPackageStateFlag.BLOCK_NATIVE_DEBUGGING_SUPPRESS_NOTIF; import static android.content.pm.GosPackageStateFlag.BLOCK_PLAY_INTEGRITY_API; import static android.content.pm.GosPackageStateFlag.CONTACT_SCOPES_ENABLED; +import static android.content.pm.GosPackageStateFlag.MICROPHONE_SCOPES_ENABLED; import static android.content.pm.GosPackageStateFlag.ENABLE_EXPLOIT_PROTECTION_COMPAT_MODE; import static android.content.pm.GosPackageStateFlag.FORCE_MEMTAG; import static android.content.pm.GosPackageStateFlag.FORCE_MEMTAG_NON_DEFAULT; @@ -49,6 +50,7 @@ import static com.android.server.pm.GosPackageStatePermission.ALLOW_CROSS_USER_PROFILE_READS; import static com.android.server.pm.GosPackageStatePermission.ALLOW_CROSS_USER_PROFILE_WRITES; import static com.android.server.pm.GosPackageStatePermission.FIELD_CONTACT_SCOPES; +import static com.android.server.pm.GosPackageStatePermission.FIELD_MICROPHONE_SCOPES; import static com.android.server.pm.GosPackageStatePermission.FIELD_PACKAGE_FLAGS; import static com.android.server.pm.GosPackageStatePermission.FIELD_STORAGE_SCOPES; @@ -76,7 +78,7 @@ static void init(PackageManagerService pm) { selfAccessPermission = builder() .readFlags(STORAGE_SCOPES_ENABLED, ALLOW_ACCESS_TO_OBB_DIRECTORY, - CONTACT_SCOPES_ENABLED) + CONTACT_SCOPES_ENABLED, MICROPHONE_SCOPES_ENABLED) .readFlags(playIntegrityFlags) .readWriteFlag(PLAY_INTEGRITY_API_USED_AT_LEAST_ONCE) .create(); @@ -103,13 +105,13 @@ static void init(PackageManagerService pm) { .readField(FIELD_CONTACT_SCOPES) .apply(ksp.contactsProvider, computer); builder() - .readFlags(STORAGE_SCOPES_ENABLED, CONTACT_SCOPES_ENABLED) + .readFlags(STORAGE_SCOPES_ENABLED, CONTACT_SCOPES_ENABLED, MICROPHONE_SCOPES_ENABLED) // user profiles are handled by the launcher instance in profile parent user .crossUserPermission(ALLOW_CROSS_USER_PROFILE_READS) .apply(ksp.launcher, computer); builder() - .readWriteFlags(STORAGE_SCOPES_ENABLED, CONTACT_SCOPES_ENABLED) - .readWriteFields(FIELD_STORAGE_SCOPES, FIELD_CONTACT_SCOPES, + .readWriteFlags(STORAGE_SCOPES_ENABLED, CONTACT_SCOPES_ENABLED, MICROPHONE_SCOPES_ENABLED) + .readWriteFields(FIELD_STORAGE_SCOPES, FIELD_CONTACT_SCOPES, FIELD_MICROPHONE_SCOPES, FIELD_PACKAGE_FLAGS) // in some cases PermissionController handles user profile from profile parent user .crossUserPermission(ALLOW_CROSS_USER_PROFILE_READS) @@ -144,7 +146,7 @@ static void init(PackageManagerService pm) { builder() .readWriteFlags(settingsReadWriteFlags) .readWriteFlags(playIntegrityFlags) - .readFlags(STORAGE_SCOPES_ENABLED, CONTACT_SCOPES_ENABLED) + .readFlags(STORAGE_SCOPES_ENABLED, CONTACT_SCOPES_ENABLED, MICROPHONE_SCOPES_ENABLED) .readFields(FIELD_PACKAGE_FLAGS) .crossUserPermission(ALLOW_CROSS_USER_PROFILE_READS) .crossUserPermission(ALLOW_CROSS_USER_PROFILE_WRITES) diff --git a/services/core/java/com/android/server/pm/GosPackageStatePersistence.java b/services/core/java/com/android/server/pm/GosPackageStatePersistence.java index ee90810b457cc..481a6aa93d650 100644 --- a/services/core/java/com/android/server/pm/GosPackageStatePersistence.java +++ b/services/core/java/com/android/server/pm/GosPackageStatePersistence.java @@ -21,6 +21,7 @@ class GosPackageStatePersistence { private static final String ATTR_PACKAGE_FLAG_STORAGE = "package-flags"; private static final String ATTR_STORAGE_SCOPES = "storage-scopes"; private static final String ATTR_CONTACT_SCOPES = "contact-scopes"; + private static final String ATTR_MICROPHONE_SCOPES = "microphone-scopes"; /** @see Settings#writePackageRestrictions */ static void serialize(PackageUserStateInternal packageUserState, TypedXmlSerializer serializer) throws IOException { @@ -50,6 +51,12 @@ private static void serializeInner(GosPackageState ps, TypedXmlSerializer serial serializer.attributeBytesHex(null, ATTR_CONTACT_SCOPES, s); } } + if (ps.hasFlag(GosPackageStateFlag.MICROPHONE_SCOPES_ENABLED)) { + byte[] s = ps.microphoneScopes; + if (s != null) { + serializer.attributeBytesHex(null, ATTR_MICROPHONE_SCOPES, s); + } + } long packageFlagStorage = ps.packageFlagStorage; if (packageFlagStorage != 0L) { serializer.attributeLong(null, ATTR_PACKAGE_FLAG_STORAGE, ps.packageFlagStorage); @@ -61,6 +68,7 @@ static GosPackageState deserialize(TypedXmlPullParser parser) throws XmlPullPars long packageFlagStorage = 0L; byte[] storageScopes = null; byte[] contactScopes = null; + byte[] microphoneScopes = null; for (int i = 0, numAttr = parser.getAttributeCount(); i < numAttr; ++i) { String attr = parser.getAttributeName(i); @@ -73,11 +81,13 @@ static GosPackageState deserialize(TypedXmlPullParser parser) throws XmlPullPars storageScopes = parser.getAttributeBytesHex(i); case ATTR_CONTACT_SCOPES -> contactScopes = parser.getAttributeBytesHex(i); + case ATTR_MICROPHONE_SCOPES -> + microphoneScopes = parser.getAttributeBytesHex(i); default -> Slog.e(TAG, "deserialize: unknown attribute " + attr); } } - return new GosPackageState(flagStorage1, packageFlagStorage, storageScopes, contactScopes); + return new GosPackageState(flagStorage1, packageFlagStorage, storageScopes, contactScopes, microphoneScopes); } // Compatibility with legacy serialized GosPackageState. @@ -91,7 +101,8 @@ static GosPackageState maybeDeserializeLegacy(TypedXmlPullParser parser) { long packageFlagStorage = parser.getAttributeLong(null, "GrapheneOS-package-flags", 0L); byte[] storageScopes = parser.getAttributeBytesHex(null, "GrapheneOS-storage-scopes", null); byte[] contactScopes = parser.getAttributeBytesHex(null, "GrapheneOS-contact-scopes", null); - return new GosPackageState(flagStorage1, packageFlagStorage, storageScopes, contactScopes); + // microphoneScopes was not present in legacy format + return new GosPackageState(flagStorage1, packageFlagStorage, storageScopes, contactScopes, null); } private static long migrateLegacyFlags(int flags) { diff --git a/services/core/java/com/android/server/pm/GosPackageStatePmHooks.java b/services/core/java/com/android/server/pm/GosPackageStatePmHooks.java index c5fcdea679ac8..a9947f00e78be 100644 --- a/services/core/java/com/android/server/pm/GosPackageStatePmHooks.java +++ b/services/core/java/com/android/server/pm/GosPackageStatePmHooks.java @@ -173,7 +173,9 @@ private static void maybeDeriveFlags(Computer snapshot, GosPackageState gosPs, P return; } - if (!gosPs.hasFlag(GosPackageStateFlag.STORAGE_SCOPES_ENABLED) && !gosPs.hasFlag(GosPackageStateFlag.CONTACT_SCOPES_ENABLED)) { + if (!gosPs.hasFlag(GosPackageStateFlag.STORAGE_SCOPES_ENABLED) + && !gosPs.hasFlag(GosPackageStateFlag.CONTACT_SCOPES_ENABLED) + && !gosPs.hasFlag(GosPackageStateFlag.MICROPHONE_SCOPES_ENABLED)) { return; } @@ -276,6 +278,10 @@ private static int deriveFlags(int flags, AndroidPackage pkg) { case Manifest.permission.GET_ACCOUNTS: flags |= DerivedPackageFlag.HAS_GET_ACCOUNTS_DECLARATION; continue; + + case Manifest.permission.RECORD_AUDIO: + flags |= DerivedPackageFlag.HAS_RECORD_AUDIO_DECLARATION; + continue; } } diff --git a/services/core/java/com/android/server/pm/PackageManagerNative.java b/services/core/java/com/android/server/pm/PackageManagerNative.java index 20592797dfbcd..de5de183d1872 100644 --- a/services/core/java/com/android/server/pm/PackageManagerNative.java +++ b/services/core/java/com/android/server/pm/PackageManagerNative.java @@ -20,9 +20,13 @@ import static com.android.server.pm.PackageManagerService.TAG; +import android.annotation.Nullable; import android.content.pm.ApplicationInfo; +import android.content.pm.GosPackageState; +import android.content.pm.GosPackageStateFlag; import android.content.pm.IPackageManagerNative; import android.content.pm.IStagedApexObserver; +import android.content.pm.MicrophoneScopeInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageInfoNative; import android.content.pm.PackageManager; @@ -37,6 +41,13 @@ import android.text.TextUtils; import android.util.Slog; +import com.android.server.pm.pkg.PackageStateInternal; + +import android.os.SELinux; + +import android.system.Os; + +import java.io.File; import java.util.Arrays; final class PackageManagerNative extends IPackageManagerNative.Stub { @@ -260,4 +271,87 @@ public void onDeniedSpecialRuntimePermissionOp(String permissionName, int uid, S com.android.server.ext.MissingSpecialRuntimePermissionNotification .maybeShow(mPm.getContext(), permissionName, uid, packageName); } + + @Override + @Nullable + public MicrophoneScopeInfo getMicrophoneScopeInfo(int uid, int userId) throws RemoteException { + final String micScopesDir = "/data/system/microphone_scopes/"; + int callingUid = Binder.getCallingUid(); + if (callingUid != android.os.Process.SYSTEM_UID + && callingUid != android.os.Process.AUDIOSERVER_UID + && callingUid != android.os.Process.MEDIA_UID) { + Slog.e(TAG, "getMicrophoneScopeInfo not allowed from uid " + callingUid); + throw new SecurityException("getMicrophoneScopeInfo not allowed from uid " + callingUid); + } + Slog.i(TAG, "getMicrophoneScopeInfo called for uid=" + uid + ", userId=" + userId + " from callingUid=" + callingUid); + + final Computer snapshot = mPm.snapshotComputer(); + String[] packages = snapshot.getPackagesForUid(uid); + if (packages == null || packages.length == 0) { + return null; + } + + for (String packageName : packages) { + PackageStateInternal psi = snapshot.getPackageStates().get(packageName); + if (psi == null) { + continue; + } + GosPackageState gps = psi.getUserStateOrDefault(userId).getGosPackageState(); + if (gps != null && gps.hasFlag(GosPackageStateFlag.MICROPHONE_SCOPES_ENABLED)) { + Slog.i(TAG, "getMicrophoneScopeInfo: microphone scopes enabled for " + packageName); + MicrophoneScopeInfo info = new MicrophoneScopeInfo(); + info.enabled = true; + + File baseDir = new File(micScopesDir); + File audioDir = new File(baseDir, userId + "/" + packageName); + File audioFile = new File(audioDir, "audio.dat"); + + byte[] scopeData = gps.microphoneScopes; + if (scopeData != null && scopeData.length > 0) { + java.io.DataInputStream dis = new java.io.DataInputStream( + new java.io.ByteArrayInputStream(scopeData)); + try { + String uriString = dis.readUTF(); + String resolvedPath = null; + try { + resolvedPath = dis.readUTF(); + } catch (Exception e) { + // old format without resolved path + } + + if (resolvedPath != null) { + if (resolvedPath.startsWith("/storage/emulated/")) { + resolvedPath = resolvedPath.replaceFirst( + "/storage/emulated/", "/data/media/"); + } + + audioFile.delete(); + + baseDir.mkdir(); + baseDir.setExecutable(true, false); + new File(baseDir, String.valueOf(userId)).mkdir(); + new File(baseDir, String.valueOf(userId)).setExecutable(true, false); + audioDir.mkdir(); + audioDir.setExecutable(true, false); + + Os.symlink(resolvedPath, audioFile.getAbsolutePath()); + SELinux.restoreconRecursive(baseDir); + Slog.i(TAG, "Created symlink " + audioFile.getAbsolutePath() + " -> " + resolvedPath); + + info.audioFilePath = audioFile.getAbsolutePath(); + Slog.i(TAG, "Returning audio file path: " + info.audioFilePath); + } + } catch (Exception e) { + Slog.w(TAG, "Failed to create microphone scope audio symlink", e); + } + } else { + audioFile.delete(); + } + + return info; + } + } + + return null; + } } diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java index e4958e83552bc..2130c8da6f7ee 100644 --- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java +++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageManagerSettingsTests.java @@ -1509,7 +1509,8 @@ private static GosPackageState createTestGosPackageState() { // argument values are random return new GosPackageState(0xf0_bc_06_f1_f1_67_2e_b8L, 0xf4_93_53_00_98_c8_f0_0cL, hf.parseHex("2d f6 37 f2 90 39 da ef"), - hf.parseHex("8b 9d 61 a3 3e 45 12") + hf.parseHex("8b 9d 61 a3 3e 45 12"), + hf.parseHex("a1 b2 c3 d4 e5") ); }