From c96a86752074be247e4667008b2c6884251799f5 Mon Sep 17 00:00:00 2001 From: Artem Smirnov Date: Wed, 4 Mar 2026 13:19:13 +0200 Subject: [PATCH 1/2] Fix TrustManagerServiceTest broken by API drift in 5e41574882b6 --- .../com/android/server/trust/TrustManagerServiceTest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java index 6da0cabe07c4c..5e9da9095e632 100644 --- a/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/trust/TrustManagerServiceTest.java @@ -30,6 +30,7 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock; import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify; import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; +import static com.android.internal.widget.LockDomain.Primary; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED; import static com.android.internal.widget.LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN; @@ -673,7 +674,7 @@ private void attemptSuccessfulUnlock(int userId) throws RemoteException { ArgumentCaptor.forClass(LockSettingsStateListener.class); verify(mLockSettingsInternal).registerLockSettingsStateListener(captor.capture()); LockSettingsStateListener listener = captor.getValue(); - listener.onAuthenticationSucceeded(userId); + listener.onAuthenticationSucceeded(userId, Primary); } else { mTrustManager.reportUnlockAttempt(/* successful= */ true, userId); } @@ -685,7 +686,7 @@ private void attemptFailedUnlock(int userId) throws RemoteException { ArgumentCaptor.forClass(LockSettingsStateListener.class); verify(mLockSettingsInternal).registerLockSettingsStateListener(captor.capture()); LockSettingsStateListener listener = captor.getValue(); - listener.onAuthenticationFailed(userId); + listener.onAuthenticationFailed(userId, Primary); } else { mTrustManager.reportUnlockAttempt(/* successful= */ false, userId); } From 6042156c734e62733e256b18e69ffedd96088c92 Mon Sep 17 00:00:00 2001 From: Artem Smirnov Date: Sat, 7 Mar 2026 12:31:42 +0200 Subject: [PATCH 2/2] 2133: Add microphone spoofing --- core/api/system-current.txt | 23 ++ .../java/android/app/ActivityThreadHooks.java | 3 +- .../content/pm/AppPermissionUtils.java | 42 ++ .../android/content/pm/GosPackageState.java | 26 +- .../content/pm/GosPackageStateFlag.java | 2 + .../content/pm/spoofing/MicSpoofing.java | 68 ++++ core/java/android/ext/DerivedPackageFlag.java | 2 + .../ext/micspoofing/MicSpoofingApi.java | 118 ++++++ .../android/permission/PermissionManager.java | 7 + .../ext/micspoofing/MicSpoofingApiTest.java | 57 +++ .../com/android/server/am/ActiveServices.java | 30 ++ .../server/pm/GosPackageStatePermission.java | 5 +- .../server/pm/GosPackageStatePermissions.java | 16 +- .../server/pm/GosPackageStatePersistence.java | 14 +- .../server/pm/GosPackageStatePmHooks.java | 12 +- .../server/pm/PackageManagerNative.java | 101 +++++ .../pm/PackageManagerSettingsTests.java | 3 +- .../PackageManagerNativeMicSpoofingTest.java | 368 ++++++++++++++++++ 18 files changed, 880 insertions(+), 17 deletions(-) create mode 100644 core/java/android/content/pm/spoofing/MicSpoofing.java create mode 100644 core/java/android/ext/micspoofing/MicSpoofingApi.java create mode 100644 core/tests/coretests/src/android/ext/micspoofing/MicSpoofingApiTest.java create mode 100644 services/tests/mockingservicestests/src/com/android/server/pm/PackageManagerNativeMicSpoofingTest.java diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 1e6f13b34a0fb..dee9255027f25 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -4319,6 +4319,7 @@ package android.content.om { package android.content.pm { public class AppPermissionUtils { + method public static boolean shouldHideRequestPermissionRationale(@NonNull android.content.pm.GosPackageState, @NonNull String); method public static boolean shouldSkipPermissionRequestDialog(@NonNull android.content.pm.GosPackageState, @NonNull String); method public static boolean shouldSpoofPermissionRequestResult(@NonNull android.content.pm.GosPackageState, @NonNull String); } @@ -4366,6 +4367,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[] micSpoofingConfig; field @Nullable public final byte[] storageScopes; } @@ -4379,6 +4381,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 setMicSpoofingConfig(@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[]); @@ -4386,6 +4389,7 @@ package android.content.pm { public interface GosPackageStateFlag { field public static final int CONTACT_SCOPES_ENABLED = 5; // 0x5 + field public static final int MIC_SPOOFING_ENABLED = 29; // 0x1d field public static final int STORAGE_SCOPES_ENABLED = 0; // 0x0 } @@ -5276,6 +5280,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 } @@ -5392,6 +5397,24 @@ package android.ext.dcl { } +package android.ext.micspoofing { + + public final class MicSpoofingApi { + method @NonNull public static byte[] buildCustomPathConfig(@NonNull String); + method @NonNull public static byte[] buildDefaultConfig(); + method @NonNull public static android.content.Intent createConfigActivityIntent(@NonNull String); + method @Nullable public static String getCustomAudioPathForApp(@NonNull String, int); + method @Nullable public static String getPath(@Nullable byte[]); + method public static int getSourceMode(@Nullable byte[]); + field public static final String MEDIA_PROVIDER_METHOD_OPEN_SOURCE = "MicSpoofing_openSource"; + field public static final String MEDIA_PROVIDER_RESULT_KEY_SOURCE_FD = "MicSpoofing_sourceFd"; + field public static final int MODE_CUSTOM_PATH = 1; // 0x1 + field public static final int MODE_DEFAULT = 0; // 0x0 + field public static final int VERSION = 1; // 0x1 + } + +} + package android.ext.settings { public class BoolSetting extends android.ext.settings.Setting { diff --git a/core/java/android/app/ActivityThreadHooks.java b/core/java/android/app/ActivityThreadHooks.java index ba92e565b13ae..daa8b9a5386dc 100644 --- a/core/java/android/app/ActivityThreadHooks.java +++ b/core/java/android/app/ActivityThreadHooks.java @@ -1,9 +1,9 @@ package android.app; -import android.annotation.Nullable; import android.content.Context; import android.content.pm.GosPackageState; import android.content.pm.SrtPermissions; +import android.content.pm.spoofing.MicSpoofing; import android.ext.dcl.DynCodeLoading; import android.location.HookedLocationManager; import android.os.Bundle; @@ -74,6 +74,7 @@ static void onBind2(Context appContext, Bundle appBindArgs) { static void onGosPackageStateChanged(Context ctx, GosPackageState state, boolean fromBind) { StorageScopesAppHooks.maybeEnable(state); ContactScopes.maybeEnable(ctx, state); + MicSpoofing.onGosPackageStateChanged(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..764d2a1ab5c75 100644 --- a/core/java/android/content/pm/AppPermissionUtils.java +++ b/core/java/android/content/pm/AppPermissionUtils.java @@ -16,9 +16,12 @@ package android.content.pm; +import android.Manifest; import android.annotation.NonNull; import android.annotation.SystemApi; import android.app.compat.gms.GmsCompat; +import android.content.pm.spoofing.MicSpoofing; +import android.ext.DerivedPackageFlag; import com.android.internal.app.ContactScopes; import com.android.internal.app.StorageScopesAppHooks; @@ -53,6 +56,10 @@ public static boolean shouldSpoofSelfCheck(String permName) { } } + if (MicSpoofing.shouldSpoofSelfPermissionCheck(permName)) { + return true; + } + return false; } @@ -70,6 +77,10 @@ public static boolean shouldSpoofSelfAppOpCheck(int op) { return true; } + if (MicSpoofing.shouldSpoofSelfAppOpCheck(op)) { + return true; + } + return false; } @@ -82,6 +93,30 @@ public static boolean shouldSkipPermissionRequestDialog(@NonNull GosPackageState return getSpoofablePermissionDflag(ps, perm, true) != 0; } + public static boolean shouldHideRequestPermissionRationale( + @NonNull GosPackageState packageState, + @NonNull String permission + ) { + int permDflag = getRationaleHidePermissionDflag(packageState, permission); + return permDflag != 0 && packageState.hasDerivedFlag(permDflag); + } + + private static int getRationaleHidePermissionDflag( + @NonNull GosPackageState packageState, + @NonNull String permission + ) { + //noinspection SwitchStatementWithTooFewBranches,EnhancedSwitchMigration + switch (permission) { + case Manifest.permission.RECORD_AUDIO: + if (!packageState.hasFlag(GosPackageStateFlag.MIC_SPOOFING_ENABLED)) { + return 0; + } + return DerivedPackageFlag.HAS_RECORD_AUDIO_DECLARATION; + default: + return 0; + } + } + // Controls spoofing of Activity#onRequestPermissionsResult() callback public static boolean shouldSpoofPermissionRequestResult(@NonNull GosPackageState ps, @NonNull String perm) { int dflag = getSpoofablePermissionDflag(ps, perm, false); @@ -108,6 +143,13 @@ private static int getSpoofablePermissionDflag(GosPackageState ps, String perm, } } + if (ps.hasFlag(GosPackageStateFlag.MIC_SPOOFING_ENABLED)) { + int permDflag = MicSpoofing.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..f28c6c22e6b46 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[] micSpoofingConfig; /** * 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[] micSpoofingConfig) { this.flagStorage1 = flagStorage1; this.packageFlagStorage = packageFlagStorage; this.storageScopes = storageScopes; this.contactScopes = contactScopes; + this.micSpoofingConfig = micSpoofingConfig; } 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(micSpoofingConfig); 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) + Long.hashCode(packageFlagStorage) + Arrays.hashCode(micSpoofingConfig); } @Override @@ -142,6 +147,9 @@ public boolean equals(Object obj) { if (packageFlagStorage != o.packageFlagStorage) { return false; } + if (!Arrays.equals(micSpoofingConfig, o.micSpoofingConfig)) { + return false; + } return true; } @@ -236,6 +244,7 @@ public static class Editor { private long packageFlagStorage; private byte[] storageScopes; private byte[] contactScopes; + private byte[] micSpoofingConfig; 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.micSpoofingConfig = s.micSpoofingConfig; } @NonNull @@ -304,6 +314,12 @@ public Editor setContactScopes(@Nullable byte[] contactScopes) { return this; } + @NonNull + public Editor setMicSpoofingConfig(@Nullable byte[] micSpoofingConfig) { + this.micSpoofingConfig = micSpoofingConfig; + 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, micSpoofingConfig), 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..0e344c860e9f6 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 MIC_SPOOFING_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, + MIC_SPOOFING_ENABLED, }) @Retention(RetentionPolicy.SOURCE) @interface Enum {} diff --git a/core/java/android/content/pm/spoofing/MicSpoofing.java b/core/java/android/content/pm/spoofing/MicSpoofing.java new file mode 100644 index 0000000000000..18a27a43c446d --- /dev/null +++ b/core/java/android/content/pm/spoofing/MicSpoofing.java @@ -0,0 +1,68 @@ +package android.content.pm.spoofing; + +import android.Manifest; +import android.annotation.NonNull; +import android.app.AppOpsManager; +import android.content.pm.GosPackageState; +import android.content.pm.GosPackageStateFlag; +import android.ext.DerivedPackageFlag; +import android.util.Log; + +/** @hide */ +public class MicSpoofing { + + private static final String TAG = "MicSpoofing"; + private static final boolean VERBOSE_LOGGING = true; + + private static volatile boolean isEnabled; + private static int gosPackageStateDerivedFlags; + + private MicSpoofing() { + } + + public static void onGosPackageStateChanged(GosPackageState gosPackageState) { + boolean shouldBeEnabled = gosPackageState.hasFlag(GosPackageStateFlag.MIC_SPOOFING_ENABLED); + if (shouldBeEnabled == isEnabled) return; + + if (shouldBeEnabled) { + gosPackageStateDerivedFlags = gosPackageState.derivedFlags; + } + + if (VERBOSE_LOGGING) { + Log.d(TAG, "Changing mic spoofing to " + shouldBeEnabled); + } + + isEnabled = shouldBeEnabled; + } + + public static boolean isEnabled() { + return isEnabled; + } + + public static boolean shouldSpoofSelfPermissionCheck(@NonNull String permissionName) { + if (!isEnabled) return false; + + return shouldSpoofPermissionCheckInner(getSpoofablePermissionDflag(permissionName)); + } + + public static boolean shouldSpoofSelfAppOpCheck(int op) { + if (!isEnabled) return false; + + return op == AppOpsManager.OP_RECORD_AUDIO && shouldSpoofPermissionCheckInner( + DerivedPackageFlag.HAS_RECORD_AUDIO_DECLARATION); + } + + public static int getSpoofablePermissionDflag(@NonNull String permissionName) { + if (permissionName.equals(Manifest.permission.RECORD_AUDIO)) { + return DerivedPackageFlag.HAS_RECORD_AUDIO_DECLARATION; + } + + return 0; + } + + private static boolean shouldSpoofPermissionCheckInner(int permissionDflag) { + if (permissionDflag == 0) return false; + + return (gosPackageStateDerivedFlags & permissionDflag) != 0; + } +} 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/android/ext/micspoofing/MicSpoofingApi.java b/core/java/android/ext/micspoofing/MicSpoofingApi.java new file mode 100644 index 0000000000000..294959a9a3a60 --- /dev/null +++ b/core/java/android/ext/micspoofing/MicSpoofingApi.java @@ -0,0 +1,118 @@ +package android.ext.micspoofing; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.app.ApplicationPackageManager; +import android.content.ComponentName; +import android.content.Intent; +import android.content.pm.GosPackageState; +import android.content.pm.GosPackageStateFlag; +import android.util.Log; +import android.util.Slog; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; + +/** @hide */ +@SystemApi +public final class MicSpoofingApi { + + private static final String TAG = "MicSpoofingApi"; + + public static final String MEDIA_PROVIDER_METHOD_OPEN_SOURCE = "MicSpoofing_openSource"; + public static final String MEDIA_PROVIDER_RESULT_KEY_SOURCE_FD = "MicSpoofing_sourceFd"; + + public static final int VERSION = 1; + + public static final int MODE_DEFAULT = 0; + public static final int MODE_CUSTOM_PATH = 1; + + private MicSpoofingApi() { + } + + @NonNull + public static Intent createConfigActivityIntent(@NonNull String targetPkg) { + var intent = new Intent(); + var componentName = ComponentName.createRelative( + ApplicationPackageManager.PERMISSION_CONTROLLER_RESOURCE_PACKAGE, + ".micspoofing.MicSpoofingActivity" + ); + intent.setComponent(componentName); + intent.putExtra(Intent.EXTRA_PACKAGE_NAME, targetPkg); + + return intent; + } + + @NonNull + public static byte[] buildDefaultConfig() { + return new byte[]{VERSION, MODE_DEFAULT}; + } + + @NonNull + public static byte[] buildCustomPathConfig(@NonNull String path) { + var byteArrayOutputStream = new ByteArrayOutputStream(2 + path.length() * 3); + var dataOutputStream = new DataOutputStream(byteArrayOutputStream); + try { + dataOutputStream.writeByte(VERSION); + dataOutputStream.writeByte(MODE_CUSTOM_PATH); + dataOutputStream.writeUTF(path); + } catch (IOException e) { + throw new IllegalStateException(e); + } + return byteArrayOutputStream.toByteArray(); + } + + public static int getSourceMode(@Nullable byte[] config) { + if (config == null || config.length < 2) { + return MODE_DEFAULT; + } + + if (config[0] != VERSION) { + return MODE_DEFAULT; + } + + return config[1] & 0xFF; + } + + @Nullable + public static String getCustomAudioPathForApp( + @NonNull String packageName, + int userId + ) { + var gosPs = GosPackageState.get(packageName, userId); + if (!gosPs.hasFlag(GosPackageStateFlag.MIC_SPOOFING_ENABLED)) { + Log.d(TAG, "getCustomAudioPathForApp: MIC_SPOOFING_ENABLED not set for " + + packageName + " userId " + userId); + return null; + } + + return MicSpoofingApi.getPath(gosPs.micSpoofingConfig); + } + + @Nullable + public static String getPath(@Nullable byte[] config) { + if (config == null || config.length < 2) { + return null; + } + if (config[0] != VERSION) { + return null; + } + if ((config[1] & 0xFF) != MODE_CUSTOM_PATH) { + return null; + } + + var byteArrayInputStream = new ByteArrayInputStream(config, 2, config.length - 2); + var dataInputStream = new DataInputStream(byteArrayInputStream); + + try { + return dataInputStream.readUTF(); + } catch (IOException e) { + Slog.w(TAG, "Failed to read custom path from config", e); + return null; + } + } +} diff --git a/core/java/android/permission/PermissionManager.java b/core/java/android/permission/PermissionManager.java index f13720c699eb5..ddecb07a82099 100644 --- a/core/java/android/permission/PermissionManager.java +++ b/core/java/android/permission/PermissionManager.java @@ -55,6 +55,7 @@ import android.content.Context; import android.content.PermissionChecker; import android.content.pm.AppPermissionUtils; +import android.content.pm.GosPackageState; import android.content.pm.IPackageManager; import android.content.pm.PackageManager; import android.content.pm.ParceledListSlice; @@ -1153,6 +1154,12 @@ public boolean setAutoRevokeExempted(@NonNull String packageName, boolean exempt */ //@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) public boolean shouldShowRequestPermissionRationale(@NonNull String permissionName) { + final GosPackageState gosPackageState = GosPackageState.getForSelf(mContext); + if (AppPermissionUtils.shouldHideRequestPermissionRationale(gosPackageState, + permissionName)) { + return false; + } + try { final String packageName = mContext.getPackageName(); return mPermissionManager.shouldShowRequestPermissionRationale(packageName, diff --git a/core/tests/coretests/src/android/ext/micspoofing/MicSpoofingApiTest.java b/core/tests/coretests/src/android/ext/micspoofing/MicSpoofingApiTest.java new file mode 100644 index 0000000000000..8e95fd826a151 --- /dev/null +++ b/core/tests/coretests/src/android/ext/micspoofing/MicSpoofingApiTest.java @@ -0,0 +1,57 @@ +package android.ext.micspoofing; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class MicSpoofingApiTest { + + @Test + public void customPathConfigRoundTrip() { + var path = "/storage/emulated/0/Music/test.wav"; + + var config = MicSpoofingApi.buildCustomPathConfig(path); + + assertEquals(MicSpoofingApi.MODE_CUSTOM_PATH, MicSpoofingApi.getSourceMode(config)); + assertEquals(path, MicSpoofingApi.getPath(config)); + } + + @Test + public void defaultConfigHasNoPath() { + var config = MicSpoofingApi.buildDefaultConfig(); + + assertNull(MicSpoofingApi.getPath(config)); + assertEquals(MicSpoofingApi.MODE_DEFAULT, MicSpoofingApi.getSourceMode(config)); + } + + @Test + public void getPathReturnsNullForNull() { + assertNull(MicSpoofingApi.getPath(null)); + } + + @Test + public void getPathReturnsNullForTruncatedConfig() { + assertNull(MicSpoofingApi.getPath(new byte[]{MicSpoofingApi.VERSION})); + } + + @Test + public void mediaProviderMethodOpenSource_hasExpectedValue() { + assertEquals( + "MicSpoofing_openSource", + MicSpoofingApi.MEDIA_PROVIDER_METHOD_OPEN_SOURCE + ); + } + + @Test + public void mediaProviderResultKeySourceFd_hasExpectedValue() { + assertEquals( + "MicSpoofing_sourceFd", + MicSpoofingApi.MEDIA_PROVIDER_RESULT_KEY_SOURCE_FD + ); + } +} diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java index 989a0ce31a20a..b385f6260080e 100644 --- a/services/core/java/com/android/server/am/ActiveServices.java +++ b/services/core/java/com/android/server/am/ActiveServices.java @@ -183,6 +183,8 @@ import android.content.IntentSender; import android.content.ServiceConnection; import android.content.pm.ApplicationInfo; +import android.content.pm.GosPackageState; +import android.content.pm.GosPackageStateFlag; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; import android.content.pm.ParceledListSlice; @@ -2949,6 +2951,9 @@ private Pair validateForegroundServiceType(ServiceRec Slog.w(TAG, msg); } break; case FGS_TYPE_POLICY_CHECK_PERMISSION_DENIED_ENFORCED: { + if (shouldSkipFgsPermissionDeniedEnforcement(type, r)) { + break; + } exception = new SecurityException("Starting FGS with type " + ServiceInfo.foregroundServiceTypeToLabel(type) + " callerApp=" + r.app @@ -2966,6 +2971,31 @@ private Pair validateForegroundServiceType(ServiceRec return Pair.create(code, exception); } + private boolean shouldSkipFgsPermissionDeniedEnforcement( + @ForegroundServiceType int serviceType, + @NonNull ServiceRecord serviceRecord + ) { + if (serviceType != ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE) { + return false; + } + + // Only bypass if the manifest permission is satisfied — the denial + // must be from the audio permission check ("any of" RECORD_AUDIO etc.) + int hasForegroundServiceMicrophonePermission = mAm.checkPermission( + Manifest.permission.FOREGROUND_SERVICE_MICROPHONE, + serviceRecord.app.getPid(), + serviceRecord.appInfo.uid + ); + if (hasForegroundServiceMicrophonePermission != PackageManager.PERMISSION_GRANTED) { + return false; + } + + GosPackageState gosPackageState = mAm + .getPackageManagerInternal() + .getGosPackageState(serviceRecord.packageName, serviceRecord.userId); + return gosPackageState.hasFlag(GosPackageStateFlag.MIC_SPOOFING_ENABLED); + } + private class SystemExemptedFgsTypePermission extends ForegroundServiceTypePermission { SystemExemptedFgsTypePermission() { super("System exempted"); diff --git a/services/core/java/com/android/server/pm/GosPackageStatePermission.java b/services/core/java/com/android/server/pm/GosPackageStatePermission.java index 1013ec825b420..e7df9c4be52b5 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_MIC_SPOOFING_CONFIG = 3; @IntDef(prefix = "FIELD_", value = { - FIELD_STORAGE_SCOPES, FIELD_CONTACT_SCOPES, FIELD_PACKAGE_FLAGS + FIELD_STORAGE_SCOPES, FIELD_CONTACT_SCOPES, FIELD_PACKAGE_FLAGS, FIELD_MIC_SPOOFING_CONFIG }) @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_MIC_SPOOFING_CONFIG) ? ps.micSpoofingConfig : default_.micSpoofingConfig ); 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_MIC_SPOOFING_CONFIG) ? update.micSpoofingConfig : current.micSpoofingConfig ); 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..e0814d1b1adf7 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.MIC_SPOOFING_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_MIC_SPOOFING_CONFIG; 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, MIC_SPOOFING_ENABLED) .readFlags(playIntegrityFlags) .readWriteFlag(PLAY_INTEGRITY_API_USED_AT_LEAST_ONCE) .create(); @@ -95,22 +97,23 @@ static void init(PackageManagerService pm) { KnownSystemPackages ksp = KnownSystemPackages.get(pm.getContext()); builder() - .readFlag(STORAGE_SCOPES_ENABLED) - .readField(FIELD_STORAGE_SCOPES) + .readFlags(STORAGE_SCOPES_ENABLED, MIC_SPOOFING_ENABLED) + .readFields(FIELD_STORAGE_SCOPES, FIELD_MIC_SPOOFING_CONFIG) .apply(ksp.mediaProvider, computer); builder() .readFlag(CONTACT_SCOPES_ENABLED) .readField(FIELD_CONTACT_SCOPES) .apply(ksp.contactsProvider, computer); builder() - .readFlags(STORAGE_SCOPES_ENABLED, CONTACT_SCOPES_ENABLED) + .readFlags(STORAGE_SCOPES_ENABLED, CONTACT_SCOPES_ENABLED, MIC_SPOOFING_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) + .readWriteFlags(STORAGE_SCOPES_ENABLED, CONTACT_SCOPES_ENABLED, + MIC_SPOOFING_ENABLED) .readWriteFields(FIELD_STORAGE_SCOPES, FIELD_CONTACT_SCOPES, - FIELD_PACKAGE_FLAGS) + FIELD_PACKAGE_FLAGS, FIELD_MIC_SPOOFING_CONFIG) // in some cases PermissionController handles user profile from profile parent user .crossUserPermission(ALLOW_CROSS_USER_PROFILE_READS) .apply(ksp.permissionController, computer); @@ -121,6 +124,7 @@ static void init(PackageManagerService pm) { @GosPackageStateFlag.Enum int[] settingsReadWriteFlags = { ALLOW_ACCESS_TO_OBB_DIRECTORY, + MIC_SPOOFING_ENABLED, BLOCK_NATIVE_DEBUGGING_NON_DEFAULT, BLOCK_NATIVE_DEBUGGING, BLOCK_NATIVE_DEBUGGING_SUPPRESS_NOTIF, diff --git a/services/core/java/com/android/server/pm/GosPackageStatePersistence.java b/services/core/java/com/android/server/pm/GosPackageStatePersistence.java index ee90810b457cc..77dcb26e25582 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_MIC_SPOOFING_CONFIG = "mic-spoofing-config"; /** @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.MIC_SPOOFING_ENABLED)) { + byte[] s = ps.micSpoofingConfig; + if (s != null) { + serializer.attributeBytesHex(null, ATTR_MIC_SPOOFING_CONFIG, 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[] micSpoofingConfig = 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_MIC_SPOOFING_CONFIG -> + micSpoofingConfig = 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, micSpoofingConfig); } // Compatibility with legacy serialized GosPackageState. @@ -91,7 +101,7 @@ 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); + 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..e1debb7952aa5 100644 --- a/services/core/java/com/android/server/pm/GosPackageStatePmHooks.java +++ b/services/core/java/com/android/server/pm/GosPackageStatePmHooks.java @@ -173,7 +173,11 @@ private static void maybeDeriveFlags(Computer snapshot, GosPackageState gosPs, P return; } - if (!gosPs.hasFlag(GosPackageStateFlag.STORAGE_SCOPES_ENABLED) && !gosPs.hasFlag(GosPackageStateFlag.CONTACT_SCOPES_ENABLED)) { + boolean hasStorageScopesEnabledFlag = gosPs.hasFlag(GosPackageStateFlag.STORAGE_SCOPES_ENABLED); + boolean hasContactScopesEnabledFlag = gosPs.hasFlag(GosPackageStateFlag.CONTACT_SCOPES_ENABLED); + boolean hasMicSpoofingEnabledFlag = gosPs.hasFlag(GosPackageStateFlag.MIC_SPOOFING_ENABLED); + + if (!hasStorageScopesEnabledFlag && !hasContactScopesEnabledFlag && !hasMicSpoofingEnabledFlag) { return; } @@ -276,6 +280,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; } } @@ -355,6 +363,8 @@ static int runShellCommand(PackageManagerShellCommand cmd) { ed.setStorageScopes(getByteArrArg(cmd)); case "set-contact-scopes" -> ed.setContactScopes(getByteArrArg(cmd)); + case "set-mic-spoofing-config" -> + ed.setMicSpoofingConfig(getByteArrArg(cmd)); case "set-kill-uid-after-apply" -> ed.setKillUidAfterApply(Boolean.parseBoolean(cmd.getNextArgRequired())); case "set-notify-uid-after-apply" -> diff --git a/services/core/java/com/android/server/pm/PackageManagerNative.java b/services/core/java/com/android/server/pm/PackageManagerNative.java index 20592797dfbcd..c2614bd51ecac 100644 --- a/services/core/java/com/android/server/pm/PackageManagerNative.java +++ b/services/core/java/com/android/server/pm/PackageManagerNative.java @@ -20,7 +20,9 @@ import static com.android.server.pm.PackageManagerService.TAG; +import android.content.Intent; import android.content.pm.ApplicationInfo; +import android.content.pm.GosPackageStateFlag; import android.content.pm.IPackageManagerNative; import android.content.pm.IStagedApexObserver; import android.content.pm.PackageInfo; @@ -30,20 +32,34 @@ import android.content.pm.SignatureNative; import android.content.pm.SigningInfoNative; import android.content.pm.StagedApexInfo; +import android.ext.micspoofing.MicSpoofingApi; import android.os.Binder; +import android.os.Bundle; +import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.UserHandle; +import android.provider.MediaStore; import android.system.virtualmachine.BuildFlags; import android.text.TextUtils; import android.util.Slog; import java.util.Arrays; +import java.util.function.IntSupplier; final class PackageManagerNative extends IPackageManagerNative.Stub { private final PackageManagerService mPm; + private final IntSupplier mCallingUidSupplier; + private final IntSupplier mLocalUidSupplier; PackageManagerNative(PackageManagerService pm) { + this(pm, Binder::getCallingUid, android.os.Process::myUid); + } + + PackageManagerNative(PackageManagerService pm, IntSupplier callingUidSupplier, + IntSupplier localUidSupplier) { mPm = pm; + mCallingUidSupplier = callingUidSupplier; + mLocalUidSupplier = localUidSupplier; } @Override @@ -260,4 +276,89 @@ public void onDeniedSpecialRuntimePermissionOp(String permissionName, int uid, S com.android.server.ext.MissingSpecialRuntimePermissionNotification .maybeShow(mPm.getContext(), permissionName, uid, packageName); } + + static boolean canAccessMicSpoofingStateForUid(int callingUid, int targetUid, int localUid) { + if (callingUid == targetUid || callingUid == localUid) { + return true; + } + + var callingAppId = UserHandle.getAppId(callingUid); + return callingAppId == android.os.Process.SYSTEM_UID + || callingAppId == android.os.Process.MEDIA_UID + || callingAppId == android.os.Process.AUDIOSERVER_UID; + } + + private void enforceMicSpoofingUidAccess(int uid) { + var callingUid = mCallingUidSupplier.getAsInt(); + if (canAccessMicSpoofingStateForUid(callingUid, uid, mLocalUidSupplier.getAsInt())) { + return; + } + + throw new SecurityException( + "UID " + callingUid + " cannot access mic spoofing state for UID " + uid); + } + + @Override + public boolean isMicSpoofingEnabledForUid(int uid) { + enforceMicSpoofingUidAccess(uid); + + var packages = mPm.snapshotComputer().getPackagesForUid(uid); + if (packages == null || packages.length == 0) { + return false; + } + var userId = UserHandle.getUserId(uid); + var gosPs = GosPackageStatePmHooks.getUnfiltered(mPm, packages[0], userId); + return gosPs.hasFlag(GosPackageStateFlag.MIC_SPOOFING_ENABLED); + } + + @Override + public ParcelFileDescriptor getMicSpoofingSourceFdForUid(int uid) { + enforceMicSpoofingUidAccess(uid); + + var packages = mPm.snapshotComputer().getPackagesForUid(uid); + if (packages == null || packages.length == 0) { + Slog.w(TAG, "getMicSpoofingSourceFdForUid: no package for uid " + uid); + return null; + } + + var packageName = packages[0]; + var userId = UserHandle.getUserId(uid); + var extras = new Bundle(); + extras.putString(Intent.EXTRA_PACKAGE_NAME, packageName); + extras.putInt(Intent.EXTRA_USER_HANDLE, userId); + + final var token = Binder.clearCallingIdentity(); + Bundle result; + Binder.allowBlockingForCurrentThread(); + try { + result = mPm.getContext().getContentResolver().call( + MediaStore.AUTHORITY, + MicSpoofingApi.MEDIA_PROVIDER_METHOD_OPEN_SOURCE, + null, + extras + ); + } finally { + Binder.defaultBlockingForCurrentThread(); + Binder.restoreCallingIdentity(token); + } + + if (result == null) { + Slog.w(TAG, "getMicSpoofingSourceFdForUid: MediaProvider returned null for package " + + packageName + " userId " + userId + " uid " + uid); + return null; + } + + var parcelFileDescriptor = result.getParcelable( + MicSpoofingApi.MEDIA_PROVIDER_RESULT_KEY_SOURCE_FD, + ParcelFileDescriptor.class + ); + if (parcelFileDescriptor == null) { + Slog.w(TAG, "getMicSpoofingSourceFdForUid: bundle missing source fd for package " + + packageName + " userId " + userId + " uid " + uid); + } else { + Slog.d(TAG, "getMicSpoofingSourceFdForUid: received source fd for package " + + packageName + " userId " + userId + " uid " + uid); + } + return parcelFileDescriptor; + } } 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..0584ce1faddd3 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("01 01 aa bb cc dd ee ff") ); } diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/PackageManagerNativeMicSpoofingTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/PackageManagerNativeMicSpoofingTest.java new file mode 100644 index 0000000000000..30eefd45b3509 --- /dev/null +++ b/services/tests/mockingservicestests/src/com/android/server/pm/PackageManagerNativeMicSpoofingTest.java @@ -0,0 +1,368 @@ +package com.android.server.pm; + +import static org.junit.Assert.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.when; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.pm.GosPackageState; +import android.content.pm.GosPackageStateFlag; +import android.ext.micspoofing.MicSpoofingApi; +import android.os.Bundle; +import android.os.ParcelFileDescriptor; +import android.os.UserHandle; +import android.platform.test.annotations.Presubmit; +import android.provider.MediaStore; +import android.util.ArrayMap; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.server.pm.pkg.PackageStateInternal; +import com.android.server.pm.pkg.PackageUserStateInternal; + +import com.google.common.truth.Truth; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +@Presubmit +@RunWith(AndroidJUnit4.class) +public class PackageManagerNativeMicSpoofingTest { + + private static final String TEST_PKG = "com.android.test.app"; + private static final int TEST_UID = UserHandle.getUid(10, 12345); + private static final String TEST_FUSE_PATH = "/storage/emulated/10/Music/test.wav"; + + @Rule + public final MockitoRule mockitoRule = MockitoJUnit.rule(); + + @Mock + private PackageManagerService packageManagerService; + + @Mock + private Computer computer; + + @Mock + private PackageStateInternal packageState; + + @Mock + private PackageUserStateInternal userState; + + @Mock + private Context context; + + @Mock + private ContentResolver contentResolver; + + @Test + public void canAccessMicSpoofingStateForUid_sameUid_returnsTrue() { + var localUid = UserHandle.getUid(0, 34567); + + var canAccess = PackageManagerNative.canAccessMicSpoofingStateForUid( + TEST_UID, TEST_UID, localUid); + + Truth.assertThat(canAccess).isTrue(); + } + + @Test + public void canAccessMicSpoofingStateForUid_callerIsLocalProcess_returnsTrue() { + var callingUid = UserHandle.getUid(11, 23456); + + var canAccess = PackageManagerNative.canAccessMicSpoofingStateForUid( + callingUid, TEST_UID, callingUid); + + Truth.assertThat(canAccess).isTrue(); + } + + @Test + public void canAccessMicSpoofingStateForUid_systemUidCrossUid_returnsTrue() { + var systemUid = UserHandle.getUid(0, android.os.Process.SYSTEM_UID); + var localUid = UserHandle.getUid(0, 34567); + + var canAccess = PackageManagerNative.canAccessMicSpoofingStateForUid( + systemUid, TEST_UID, localUid); + + Truth.assertThat(canAccess).isTrue(); + } + + @Test + public void canAccessMicSpoofingStateForUid_mediaUidCrossUid_returnsTrue() { + var mediaUid = UserHandle.getUid(0, android.os.Process.MEDIA_UID); + var localUid = UserHandle.getUid(0, 34567); + + var canAccess = PackageManagerNative.canAccessMicSpoofingStateForUid( + mediaUid, TEST_UID, localUid); + + Truth.assertThat(canAccess).isTrue(); + } + + @Test + public void canAccessMicSpoofingStateForUid_audioserverUidCrossUid_returnsTrue() { + var audioserverUid = UserHandle.getUid(0, android.os.Process.AUDIOSERVER_UID); + var localUid = UserHandle.getUid(0, 34567); + + var canAccess = PackageManagerNative.canAccessMicSpoofingStateForUid( + audioserverUid, TEST_UID, localUid); + + Truth.assertThat(canAccess).isTrue(); + } + + @Test + public void canAccessMicSpoofingStateForUid_untrustedCrossUid_returnsFalse() { + var callingUid = UserHandle.getUid(11, 23456); + var localUid = UserHandle.getUid(0, 34567); + + var canAccess = PackageManagerNative.canAccessMicSpoofingStateForUid( + callingUid, TEST_UID, localUid); + + Truth.assertThat(canAccess).isFalse(); + } + + @Test + public void isMicSpoofingEnabledForUid_enabled_returnsTrue() { + mockUidState(createGosPackageState( + true, MicSpoofingApi.buildCustomPathConfig(TEST_FUSE_PATH))); + + var pmNative = new PackageManagerNative(packageManagerService); + + Truth.assertThat(pmNative.isMicSpoofingEnabledForUid(TEST_UID)).isTrue(); + } + + @Test + public void isMicSpoofingEnabledForUid_disabled_returnsFalse() { + mockUidState(createGosPackageState(false, null)); + + var pmNative = new PackageManagerNative(packageManagerService); + + Truth.assertThat(pmNative.isMicSpoofingEnabledForUid(TEST_UID)).isFalse(); + } + + @Test + public void isMicSpoofingEnabledForUid_noPackages_returnsFalse() { + when(packageManagerService.snapshotComputer()).thenReturn(computer); + when(computer.getPackagesForUid(TEST_UID)).thenReturn(null); + + var pmNative = new PackageManagerNative(packageManagerService); + + Truth.assertThat(pmNative.isMicSpoofingEnabledForUid(TEST_UID)).isFalse(); + } + + @Test + public void isMicSpoofingEnabledForUid_emptyPackages_returnsFalse() { + when(packageManagerService.snapshotComputer()).thenReturn(computer); + when(computer.getPackagesForUid(TEST_UID)).thenReturn(new String[0]); + + var pmNative = new PackageManagerNative(packageManagerService); + + Truth.assertThat(pmNative.isMicSpoofingEnabledForUid(TEST_UID)).isFalse(); + } + + @Test + public void isMicSpoofingEnabledForUid_privilegedCrossUidCallers_allowed() { + var privilegedAppIds = new int[]{ + android.os.Process.SYSTEM_UID, + android.os.Process.MEDIA_UID, + android.os.Process.AUDIOSERVER_UID + }; + var localUid = UserHandle.getUid(0, 34567); + mockUidState(createGosPackageState( + true, MicSpoofingApi.buildCustomPathConfig(TEST_FUSE_PATH))); + + for (var appId : privilegedAppIds) { + var callingUid = UserHandle.getUid(11, appId); + var pmNative = new PackageManagerNative( + packageManagerService, + () -> callingUid, + () -> localUid + ); + + Truth.assertThat(pmNative.isMicSpoofingEnabledForUid(TEST_UID)).isTrue(); + } + } + + @Test + public void isMicSpoofingEnabledForUid_untrustedCrossUid_throwsSecurityException() { + var callingUid = UserHandle.getUid(11, 23456); + var localUid = UserHandle.getUid(0, 34567); + var pmNative = new PackageManagerNative( + packageManagerService, + () -> callingUid, + () -> localUid + ); + + var exception = assertThrows( + SecurityException.class, + () -> pmNative.isMicSpoofingEnabledForUid(TEST_UID) + ); + Truth.assertThat(exception).hasMessageThat().contains("UID " + callingUid); + Mockito.verify(packageManagerService, never()).snapshotComputer(); + } + + @Test + public void getMicSpoofingSourceFdForUid_noPackages_returnsNull() { + when(packageManagerService.snapshotComputer()).thenReturn(computer); + when(computer.getPackagesForUid(TEST_UID)).thenReturn(null); + + var pmNative = new PackageManagerNative(packageManagerService); + + Truth.assertThat(pmNative.getMicSpoofingSourceFdForUid(TEST_UID)).isNull(); + + Mockito.verify(packageManagerService, never()).getContext(); + } + + @Test + public void getMicSpoofingSourceFdForUid_emptyPackages_returnsNull() { + when(packageManagerService.snapshotComputer()).thenReturn(computer); + when(computer.getPackagesForUid(TEST_UID)).thenReturn(new String[0]); + + var pmNative = new PackageManagerNative(packageManagerService); + + Truth.assertThat(pmNative.getMicSpoofingSourceFdForUid(TEST_UID)).isNull(); + + Mockito.verify(packageManagerService, never()).getContext(); + } + + @Test + public void getMicSpoofingSourceFdForUid_mediaProviderReturnsNull_returnsNull() { + mockMicSpoofingSourceResponse(null); + + var pmNative = new PackageManagerNative(packageManagerService); + + Truth.assertThat(pmNative.getMicSpoofingSourceFdForUid(TEST_UID)).isNull(); + + verifyMicSpoofingOpenSourceCallWithExpectedExtras(); + } + + @Test + public void getMicSpoofingSourceFdForUid_mediaProviderReturnsBundleWithoutFd_returnsNull() { + mockMicSpoofingSourceResponse(new Bundle()); + + var pmNative = new PackageManagerNative(packageManagerService); + + Truth.assertThat(pmNative.getMicSpoofingSourceFdForUid(TEST_UID)).isNull(); + + verifyMicSpoofingOpenSourceCallWithExpectedExtras(); + } + + @Test + public void getMicSpoofingSourceFdForUid_mediaProviderReturnsFd_returnsFd() { + var expectedFd = Mockito.mock(ParcelFileDescriptor.class); + var result = new Bundle(); + result.putParcelable(MicSpoofingApi.MEDIA_PROVIDER_RESULT_KEY_SOURCE_FD, expectedFd); + mockMicSpoofingSourceResponse(result); + + var pmNative = new PackageManagerNative(packageManagerService); + + var actualFd = pmNative.getMicSpoofingSourceFdForUid(TEST_UID); + + Truth.assertThat(actualFd).isSameInstanceAs(expectedFd); + + verifyMicSpoofingOpenSourceCallWithExpectedExtras(); + } + + @Test + public void getMicSpoofingSourceFdForUid_privilegedCrossUidCallers_allowed() { + var privilegedAppIds = new int[]{ + android.os.Process.SYSTEM_UID, + android.os.Process.MEDIA_UID, + android.os.Process.AUDIOSERVER_UID + }; + var localUid = UserHandle.getUid(0, 34567); + var expectedFd = Mockito.mock(ParcelFileDescriptor.class); + var result = new Bundle(); + result.putParcelable(MicSpoofingApi.MEDIA_PROVIDER_RESULT_KEY_SOURCE_FD, expectedFd); + mockMicSpoofingSourceResponse(result); + + for (var appId : privilegedAppIds) { + var callingUid = UserHandle.getUid(11, appId); + var pmNative = new PackageManagerNative( + packageManagerService, + () -> callingUid, + () -> localUid + ); + + var actualFd = pmNative.getMicSpoofingSourceFdForUid(TEST_UID); + Truth.assertThat(actualFd).isSameInstanceAs(expectedFd); + } + + Mockito.verify(contentResolver, Mockito.times(privilegedAppIds.length)).call( + eq(MediaStore.AUTHORITY), + eq(MicSpoofingApi.MEDIA_PROVIDER_METHOD_OPEN_SOURCE), + isNull(), + any(Bundle.class)); + } + + @Test + public void getMicSpoofingSourceFdForUid_untrustedCrossUid_throwsSecurityException() { + var callingUid = UserHandle.getUid(11, 23456); + var localUid = UserHandle.getUid(0, 34567); + var pmNative = new PackageManagerNative( + packageManagerService, + () -> callingUid, + () -> localUid + ); + + var exception = assertThrows( + SecurityException.class, + () -> pmNative.getMicSpoofingSourceFdForUid(TEST_UID) + ); + Truth.assertThat(exception).hasMessageThat().contains("UID " + callingUid); + Mockito.verify(packageManagerService, never()).snapshotComputer(); + } + + private void mockUidState(GosPackageState gosPackageState) { + var userId = UserHandle.getUserId(TEST_UID); + when(packageManagerService.snapshotComputer()).thenReturn(computer); + when(computer.getPackagesForUid(TEST_UID)).thenReturn(new String[]{TEST_PKG}); + when(packageState.getUserStateOrDefault(userId)).thenReturn(userState); + when(userState.getGosPackageState()).thenReturn(gosPackageState); + var packageStates = new ArrayMap(); + packageStates.put(TEST_PKG, packageState); + Mockito.doReturn(packageStates).when(computer).getPackageStates(); + } + + private void mockMicSpoofingSourceResponse(Bundle response) { + when(packageManagerService.snapshotComputer()).thenReturn(computer); + when(computer.getPackagesForUid(TEST_UID)).thenReturn(new String[]{TEST_PKG}); + when(packageManagerService.getContext()).thenReturn(context); + when(context.getContentResolver()).thenReturn(contentResolver); + when(contentResolver.call( + eq(MediaStore.AUTHORITY), + eq(MicSpoofingApi.MEDIA_PROVIDER_METHOD_OPEN_SOURCE), + isNull(), + any(Bundle.class))).thenReturn(response); + } + + private void verifyMicSpoofingOpenSourceCallWithExpectedExtras() { + var extrasCaptor = ArgumentCaptor.forClass(Bundle.class); + Mockito.verify(contentResolver).call( + eq(MediaStore.AUTHORITY), + eq(MicSpoofingApi.MEDIA_PROVIDER_METHOD_OPEN_SOURCE), + isNull(), + extrasCaptor.capture()); + + var extras = extrasCaptor.getValue(); + Truth.assertThat(extras.getString(Intent.EXTRA_PACKAGE_NAME)).isEqualTo(TEST_PKG); + Truth.assertThat(extras.getInt(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL)) + .isEqualTo(UserHandle.getUserId(TEST_UID)); + } + + private static GosPackageState createGosPackageState( + boolean enabled, + byte[] micSpoofingConfig + ) { + var flagStorage1 = enabled ? (1L << GosPackageStateFlag.MIC_SPOOFING_ENABLED) : 0L; + return new GosPackageState(flagStorage1, 0L, null, null, micSpoofingConfig); + } +}