diff --git a/apex/jobscheduler/framework/java/android/app/AlarmManager.java b/apex/jobscheduler/framework/java/android/app/AlarmManager.java index 18a7b0f87cbcb..9c2d3655d859b 100644 --- a/apex/jobscheduler/framework/java/android/app/AlarmManager.java +++ b/apex/jobscheduler/framework/java/android/app/AlarmManager.java @@ -28,6 +28,7 @@ import android.annotation.SystemApi; import android.annotation.SystemService; import android.annotation.TestApi; +import android.app.compat.gms.GmsCompat; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; import android.compat.annotation.UnsupportedAppUsage; @@ -1129,6 +1130,17 @@ private void setImpl(@AlarmType int type, long triggerAtMillis, long windowMilli long intervalMillis, int flags, PendingIntent operation, final OnAlarmListener listener, String listenerTag, Executor targetExecutor, WorkSource workSource, AlarmClockInfo alarmClock) { + if (GmsCompat.isEnabled()) { + if (windowMillis == WINDOW_EXACT && !canScheduleExactAlarms()) { + windowMillis = WINDOW_HEURISTIC; + } + // non-null WorkSource requires privileged UPDATE_DEVICE_STATS permission + workSource = null; + + // requires privileged SCHEDULE_PRIORITIZED_ALARM permission + flags &= ~FLAG_PRIORITIZE; + } + if (triggerAtMillis < 0) { /* NOTYET if (mAlwaysExact) { diff --git a/apex/jobscheduler/framework/java/android/os/PowerExemptionManager.java b/apex/jobscheduler/framework/java/android/os/PowerExemptionManager.java index 362bf06e6d030..f898a9d5f1f70 100644 --- a/apex/jobscheduler/framework/java/android/os/PowerExemptionManager.java +++ b/apex/jobscheduler/framework/java/android/os/PowerExemptionManager.java @@ -30,8 +30,11 @@ import android.annotation.SystemApi; import android.annotation.SystemService; import android.annotation.UserHandleAware; +import android.app.compat.gms.GmsCompat; import android.content.Context; +import com.android.internal.gmscompat.GmsCompatApp; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Collections; @@ -626,6 +629,11 @@ public void removeFromPermanentAllowList(@NonNull String packageName) { @RequiresPermission(android.Manifest.permission.CHANGE_DEVICE_IDLE_TEMP_WHITELIST) public void addToTemporaryAllowList(@NonNull String packageName, @ReasonCode int reasonCode, @Nullable String reason, long durationMs) { + if (GmsCompat.isEnabled()) { + GmsCompatApp.raisePackageToForeground(packageName, durationMs, reason, reasonCode); + return; + } + try { mService.addPowerSaveTempWhitelistApp(packageName, durationMs, mContext.getUserId(), reasonCode, reason); diff --git a/apex/jobscheduler/framework/java/com/android/server/DeviceIdleInternal.java b/apex/jobscheduler/framework/java/com/android/server/DeviceIdleInternal.java index 1fc888b06ffd3..b18f43e0b5380 100644 --- a/apex/jobscheduler/framework/java/com/android/server/DeviceIdleInternal.java +++ b/apex/jobscheduler/framework/java/com/android/server/DeviceIdleInternal.java @@ -74,7 +74,7 @@ void addPowerSaveTempWhitelistAppDirect(int uid, long duration, boolean isAppOnWhitelist(int appid); - int[] getPowerSaveWhitelistUserAppIds(); + int[] getPowerSaveWhitelistAppIds(); int[] getPowerSaveTempWhitelistAppIds(); diff --git a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java index 9c185ff9e1694..5fd06150a7382 100644 --- a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java +++ b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java @@ -2415,14 +2415,14 @@ public String[] getFullPowerWhitelistExceptIdle() { } /** - * Returns the array of app ids whitelisted by user. Take care not to + * Returns the array of whitelisted app ids. Take care not to * modify this, as it is a reference to the original copy. But the reference * can change when the list changes, so it needs to be re-acquired when * {@link PowerManager#ACTION_POWER_SAVE_WHITELIST_CHANGED} is sent. */ @Override - public int[] getPowerSaveWhitelistUserAppIds() { - return DeviceIdleController.this.getPowerSaveWhitelistUserAppIds(); + public int[] getPowerSaveWhitelistAppIds() { + return DeviceIdleController.this.getAppIdWhitelistInternal(); } @Override diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java index 2b6e225e7fcb6..48bd8e747b8df 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/DeviceIdleJobsController.java @@ -97,7 +97,7 @@ public void onReceive(Context context, Intent intent) { case PowerManager.ACTION_POWER_SAVE_WHITELIST_CHANGED: synchronized (mLock) { mDeviceIdleWhitelistAppIds = - mLocalDeviceIdleController.getPowerSaveWhitelistUserAppIds(); + mLocalDeviceIdleController.getPowerSaveWhitelistAppIds(); if (DEBUG) { Slog.d(TAG, "Got whitelist " + Arrays.toString(mDeviceIdleWhitelistAppIds)); @@ -143,7 +143,7 @@ public DeviceIdleJobsController(JobSchedulerService service) { mPowerManager = (PowerManager) mContext.getSystemService(Context.POWER_SERVICE); mLocalDeviceIdleController = LocalServices.getService(DeviceIdleInternal.class); - mDeviceIdleWhitelistAppIds = mLocalDeviceIdleController.getPowerSaveWhitelistUserAppIds(); + mDeviceIdleWhitelistAppIds = mLocalDeviceIdleController.getPowerSaveWhitelistAppIds(); mPowerSaveTempWhitelistAppIds = mLocalDeviceIdleController.getPowerSaveTempWhitelistAppIds(); mDeviceIdleUpdateFunctor = new DeviceIdleUpdateFunctor(); @@ -205,7 +205,7 @@ public void setUidActiveLocked(int uid, boolean active) { } /** - * Checks if the given job's scheduling app id exists in the device idle user whitelist. + * Checks if the given job's scheduling app id exists in the device idle whitelist. */ boolean isWhitelistedLocked(JobStatus job) { return Arrays.binarySearch(mDeviceIdleWhitelistAppIds, diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java index f94e859d25ac6..69e024b91515b 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java @@ -42,6 +42,7 @@ import android.compat.annotation.ChangeId; import android.content.ClipData; import android.content.ComponentName; +import android.content.Context; import android.net.Network; import android.net.NetworkRequest; import android.net.Uri; @@ -2976,6 +2977,23 @@ private boolean isConstraintsSatisfied(int satisfiedConstraints) { return true; } + if ((mRequiredConstraintsOfInterest & CONSTRAINT_CONNECTIVITY) != 0) { + if ((satisfiedConstraints & CONSTRAINT_CONNECTIVITY) != 0) { + var pmi = LocalServices.getService( + com.android.server.pm.permission.PermissionManagerServiceInternal.class); + + if (pmi.checkUidPermission(getSourceUid(), android.Manifest.permission.INTERNET, Context.DEVICE_ID_DEFAULT) != + android.content.pm.PackageManager.PERMISSION_GRANTED) { + if (DEBUG) { + Slog.d(TAG, "skipping job " + getJobId() + " for " + getSourcePackageName() + + " in user " + getSourceUserId() + ": it has CONSTRAINT_CONNECTIVITY, " + + "but its UID doesn't have the INTERNET permission"); + } + return false; + } + } + } + int sat = satisfiedConstraints; if (overrideState == OVERRIDE_SOFT) { // override: pretend all 'soft' requirements are satisfied diff --git a/cmds/hid/jni/com_android_commands_hid_Device.cpp b/cmds/hid/jni/com_android_commands_hid_Device.cpp index a142450ac0c67..153f7c37467a2 100644 --- a/cmds/hid/jni/com_android_commands_hid_Device.cpp +++ b/cmds/hid/jni/com_android_commands_hid_Device.cpp @@ -380,7 +380,7 @@ static void closeDevice(JNIEnv* /* env */, jclass /* clazz */, jlong ptr) { } } -static JNINativeMethod sMethods[] = { +static const JNINativeMethod sMethods[] = { {"nativeOpenDevice", "(Ljava/lang/String;Ljava/lang/String;IIII[B" "Lcom/android/commands/hid/Device$DeviceCallback;)J", diff --git a/cmds/uinput/jni/com_android_commands_uinput_Device.cpp b/cmds/uinput/jni/com_android_commands_uinput_Device.cpp index bd61000186e5a..3179c8a28e479 100644 --- a/cmds/uinput/jni/com_android_commands_uinput_Device.cpp +++ b/cmds/uinput/jni/com_android_commands_uinput_Device.cpp @@ -325,7 +325,7 @@ static jint getEvdevInputPropByLabel(JNIEnv* env, jclass /* clazz */, jstring ra return InputEventLookup::getLinuxEvdevInputPropByLabel(label.c_str()).value_or(-1); } -static JNINativeMethod sMethods[] = { +static const JNINativeMethod sMethods[] = { {"nativeOpenUinputDevice", "(Ljava/lang/String;IIIIIILjava/lang/String;" "Lcom/android/commands/uinput/Device$DeviceCallback;)J", diff --git a/core/api/current.txt b/core/api/current.txt index 572012ca46433..a06e66ff817a9 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -253,6 +253,7 @@ package android { field public static final String NFC = "android.permission.NFC"; field public static final String NFC_PREFERRED_PAYMENT_INFO = "android.permission.NFC_PREFERRED_PAYMENT_INFO"; field public static final String NFC_TRANSACTION_EVENT = "android.permission.NFC_TRANSACTION_EVENT"; + field public static final String OTHER_SENSORS = "android.permission.OTHER_SENSORS"; field @FlaggedApi("com.android.media.mediasession.flags.override_media_session_owner") public static final String OVERRIDE_MEDIA_SESSION_OWNER = "android.permission.OVERRIDE_MEDIA_SESSION_OWNER"; field public static final String OVERRIDE_WIFI_CONFIG = "android.permission.OVERRIDE_WIFI_CONFIG"; field public static final String PACKAGE_USAGE_STATS = "android.permission.PACKAGE_USAGE_STATS"; @@ -388,7 +389,9 @@ package android { field public static final String LOCATION = "android.permission-group.LOCATION"; field public static final String MICROPHONE = "android.permission-group.MICROPHONE"; field public static final String NEARBY_DEVICES = "android.permission-group.NEARBY_DEVICES"; + field public static final String NETWORK = "android.permission-group.NETWORK"; field public static final String NOTIFICATIONS = "android.permission-group.NOTIFICATIONS"; + field public static final String OTHER_SENSORS = "android.permission-group.OTHER_SENSORS"; field public static final String PHONE = "android.permission-group.PHONE"; field public static final String READ_MEDIA_AURAL = "android.permission-group.READ_MEDIA_AURAL"; field public static final String READ_MEDIA_VISUAL = "android.permission-group.READ_MEDIA_VISUAL"; diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt index ee91e88c94955..f0c2f3ba03cf7 100644 --- a/core/api/module-lib-current.txt +++ b/core/api/module-lib-current.txt @@ -87,6 +87,20 @@ package android.app.admin { } +package android.app.compat.gms { + + public class GmsModuleHooks { + method @Nullable public static String deviceConfigGetProperty(@NonNull String, @NonNull String); + method public static boolean deviceConfigSetProperties(@NonNull android.provider.DeviceConfig.Properties); + method public static boolean deviceConfigSetProperty(@NonNull String, @NonNull String, @Nullable String); + method @Nullable public static Boolean enableBluetoothAdapter(); + method public static void enableNfc(); + method public static boolean interceptSynchronousResultReceiverException(@NonNull RuntimeException); + method public static void makeBluetoothAdapterDiscoverable(); + } + +} + package android.content { public abstract class ContentProvider implements android.content.ComponentCallbacks2 { @@ -100,7 +114,6 @@ package android.content { public abstract class Context { method @NonNull public android.content.Context createContextForSdkInSandbox(@NonNull android.content.pm.ApplicationInfo, int) throws android.content.pm.PackageManager.NameNotFoundException; method @NonNull public android.os.IBinder getProcessToken(); - method @NonNull public android.os.UserHandle getUser(); field @FlaggedApi("android.os.profiling.anomaly.flags.anomaly_detector_core_c") public static final String ANOMALY_DETECTOR_SERVICE = "anomaly_detector"; field @FlaggedApi("android.app.ondeviceintelligence.flags.enable_on_device_intelligence_module") public static final int BIND_FOREGROUND_SERVICE = 67108864; // 0x4000000 field public static final String PAC_PROXY_SERVICE = "pac_proxy"; @@ -141,6 +154,45 @@ package android.content.pm { } +package android.ext { + + public class ConnectivityUtil { + method public static boolean isRegularAppWithLockdownVpnEnabled(@NonNull android.content.Context, int); + method public static boolean isSystem(@NonNull android.content.Context, int); + } + +} + +package android.ext.settings { + + public class CertTransparencyDownloaderSetting { + field @NonNull public static final android.ext.settings.IntSetting SETTING; + field public static final int VAL_GRAPHENEOS = 0; // 0x0 + field public static final int VAL_OFF = 2; // 0x2 + field public static final int VAL_STANDARD = 1; // 0x1 + } + + public class ConnChecksSetting { + method public static int get(); + method public static boolean put(int); + field public static final int VAL_DEFAULT = 0; // 0x0 + field public static final int VAL_DISABLED = 2; // 0x2 + field public static final int VAL_GRAPHENEOS = 0; // 0x0 + field public static final int VAL_STANDARD = 1; // 0x1 + } + + public class RemoteKeyProvisioningSettings { + method @Nullable public static String getServerUrlOverride(@NonNull android.content.Context); + field public static final int GRAPHENEOS_PROXY = 0; // 0x0 + field public static final int STANDARD_SERVER = 1; // 0x1 + } + + public class WidevineProvisioningSettings { + method @Nullable public static String getServerHostnameOverride(@NonNull android.content.Context); + } + +} + package android.hardware.usb { @RestrictedForEnvironment(environments=android.annotation.RestrictedForEnvironment.ENVIRONMENT_SDK_RUNTIME, from=android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE) public class UsbManager { @@ -574,6 +626,11 @@ package android.os { method @FlaggedApi("android.os.update_engine_api") public void triggerPostinstall(@NonNull String); } + public final class UserHandle implements android.os.Parcelable { + method public static int getUid(int, int); + method public static int getUserId(int); + } + public class UserManager { field @FlaggedApi("android.multiuser.user_restriction_config_wifi_shared_private") public static final String DISALLOW_CONFIG_WIFI_PRIVATE = "no_config_wifi_private"; field @FlaggedApi("android.multiuser.user_restriction_config_wifi_shared_private") public static final String DISALLOW_CONFIG_WIFI_SHARED = "no_config_wifi_shared"; diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 66db41d0e89f1..a21a30f5b415f 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -122,6 +122,7 @@ package android { field public static final String BIND_WALLPAPER_EFFECTS_GENERATION_SERVICE = "android.permission.BIND_WALLPAPER_EFFECTS_GENERATION_SERVICE"; field public static final String BIND_WEARABLE_SENSING_SERVICE = "android.permission.BIND_WEARABLE_SENSING_SERVICE"; field public static final String BLUETOOTH_MAP = "android.permission.BLUETOOTH_MAP"; + field public static final String BLUETOOTH_PRIVILEGED_ANDROID_AUTO = "android.permission.BLUETOOTH_PRIVILEGED_ANDROID_AUTO"; field public static final String BRICK = "android.permission.BRICK"; field public static final String BRIGHTNESS_SLIDER_USAGE = "android.permission.BRIGHTNESS_SLIDER_USAGE"; field public static final String BROADCAST_CLOSE_SYSTEM_DIALOGS = "android.permission.BROADCAST_CLOSE_SYSTEM_DIALOGS"; @@ -387,6 +388,7 @@ package android { field public static final String READ_PRINT_SERVICES = "android.permission.READ_PRINT_SERVICES"; field public static final String READ_PRINT_SERVICE_RECOMMENDATIONS = "android.permission.READ_PRINT_SERVICE_RECOMMENDATIONS"; field public static final String READ_PRIVILEGED_PHONE_STATE = "android.permission.READ_PRIVILEGED_PHONE_STATE"; + field public static final String READ_PRIVILEGED_PHONE_STATE_ANDROID_AUTO = "android.permission.READ_PRIVILEGED_PHONE_STATE_ANDROID_AUTO"; field public static final String READ_PROJECTION_STATE = "android.permission.READ_PROJECTION_STATE"; field @FlaggedApi("android.companion.task_continuity") public static final String READ_REMOTE_TASKS = "android.permission.READ_REMOTE_TASKS"; field public static final String READ_RESTRICTED_STATS = "android.permission.READ_RESTRICTED_STATS"; @@ -508,6 +510,7 @@ package android { field public static final String WHITELIST_AUTO_REVOKE_PERMISSIONS = "android.permission.WHITELIST_AUTO_REVOKE_PERMISSIONS"; field public static final String WHITELIST_RESTRICTED_PERMISSIONS = "android.permission.WHITELIST_RESTRICTED_PERMISSIONS"; field public static final String WIFI_ACCESS_COEX_UNSAFE_CHANNELS = "android.permission.WIFI_ACCESS_COEX_UNSAFE_CHANNELS"; + field public static final String WIFI_PRIVILEGED_ANDROID_AUTO = "android.permission.WIFI_PRIVILEGED_ANDROID_AUTO"; field public static final String WIFI_SET_DEVICE_MOBILITY_STATE = "android.permission.WIFI_SET_DEVICE_MOBILITY_STATE"; field public static final String WIFI_UPDATE_COEX_UNSAFE_CHANNELS = "android.permission.WIFI_UPDATE_COEX_UNSAFE_CHANNELS"; field public static final String WIFI_UPDATE_USABILITY_STATS_SCORE = "android.permission.WIFI_UPDATE_USABILITY_STATS_SCORE"; @@ -757,6 +760,7 @@ package android.app { } public class AppOpsManager { + method public void checkHistoricalRegistryConfig(@NonNull String[]); method @Nullable @RequiresPermission(android.Manifest.permission.GET_APP_OPS_STATS) public android.app.RuntimeAppOpAccessMessage collectRuntimeAppOpAccessMessage(); method @RequiresPermission(android.Manifest.permission.GET_APP_OPS_STATS) public void getHistoricalOps(@NonNull android.app.AppOpsManager.HistoricalOpsRequest, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer); method public static String[] getOpStrs(); @@ -1423,6 +1427,24 @@ package android.app { method public boolean isStatusBarExpansionDisabled(); } + public final class StorageScope { + ctor public StorageScope(@NonNull String, int); + method @NonNull public static android.content.Intent createConfigActivityIntent(@NonNull String); + method @NonNull public static android.app.StorageScope[] deserializeArray(@NonNull android.content.pm.GosPackageState); + method public boolean isDirectory(); + method public boolean isFile(); + method public boolean isWritable(); + method public static int maxArrayLength(); + method @Nullable public static byte[] serializeArray(@NonNull android.app.StorageScope[]); + field public static final String EXTERNAL_STORAGE_PROVIDER_METHOD_CONVERT_DOC_ID_TO_PATH = "StorageScopes_convertDocIdToPath"; + field public static final int FLAG_ALLOW_WRITES = 1; // 0x1 + field public static final int FLAG_IS_DIR = 2; // 0x2 + field public static final String MEDIA_PROVIDER_METHOD_INVALIDATE_MEDIA_PROVIDER_CACHE = "StorageScopes_invalidateCache"; + field public static final String MEDIA_PROVIDER_METHOD_MEDIA_ID_TO_FILE_PATH = "StorageScopes_mediaIdToFilePath"; + field public final int flags; + field @NonNull public final String path; + } + public final class SystemServiceRegistry { method public static void registerContextAwareService(@NonNull String, @NonNull Class, @NonNull android.app.SystemServiceRegistry.ContextAwareServiceProducerWithBinder); method public static void registerContextAwareService(@NonNull String, @NonNull Class, @NonNull android.app.SystemServiceRegistry.ContextAwareServiceProducerWithoutBinder); @@ -2516,6 +2538,30 @@ package android.app.compat { } +package android.app.compat.gms { + + public interface AndroidAutoPackageFlag { + field public static final int GRANT_AUDIO_ROUTING_PERM = 2; // 0x2 + field public static final int GRANT_PERMS_FOR_ANDROID_AUTO_PHONE_CALLS = 3; // 0x3 + field public static final int GRANT_PERMS_FOR_WIRED_ANDROID_AUTO = 0; // 0x0 + field public static final int GRANT_PERMS_FOR_WIRELESS_ANDROID_AUTO = 1; // 0x1 + } + + public final class GmsCompat { + method public static void catchOrRethrow(@NonNull SecurityException); + method public static boolean hasPermission(@NonNull String); + method public static boolean isAndroidAuto(); + method public static boolean isEnabled(); + method public static boolean isEnabledFor(@NonNull android.content.pm.ApplicationInfo); + method public static boolean isEnabledFor(@NonNull String, int); + } + + public class GmsUtils { + method @NonNull public static android.content.Intent createAppPlayStoreIntent(@NonNull String); + } + +} + package android.app.contentsafety { @FlaggedApi("android.app.contentsafety.flags.enable_contentsafety") public final class ContentSafetyManager { @@ -3117,6 +3163,7 @@ package android.app.smartspace { field public static final int FEATURE_COMMUTE_TIME = 3; // 0x3 field public static final int FEATURE_CONSENT = 11; // 0xb field public static final int FEATURE_CROSS_DEVICE_TIMER = 32; // 0x20 + field public static final int FEATURE_DATE_ALARM_ZEN_MEDIA = 2147483647; // 0x7fffffff field public static final int FEATURE_DOORBELL = 30; // 0x1e field public static final int FEATURE_DRIVING_MODE = 26; // 0x1a field public static final int FEATURE_EARTHQUAKE_ALERT = 38; // 0x26 @@ -4502,6 +4549,7 @@ package android.content { method public abstract android.content.Context createCredentialProtectedStorageContext(); method @NonNull public android.content.Context createPackageContextAsUser(@NonNull String, int, @NonNull android.os.UserHandle) throws android.content.pm.PackageManager.NameNotFoundException; method @Nullable public abstract java.io.File getPreloadsFileCache(); + method @NonNull public android.os.UserHandle getUser(); method public abstract boolean isCredentialProtectedStorage(); method @Nullable @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL) public android.content.Intent registerReceiverForAllUsers(@Nullable android.content.BroadcastReceiver, @NonNull android.content.IntentFilter, @Nullable String, @Nullable android.os.Handler); method @Nullable @RequiresPermission(android.Manifest.permission.INTERACT_ACROSS_USERS_FULL) public android.content.Intent registerReceiverForAllUsers(@Nullable android.content.BroadcastReceiver, @NonNull android.content.IntentFilter, @Nullable String, @Nullable android.os.Handler, int); @@ -4752,7 +4800,13 @@ package android.content.om { package android.content.pm { + public class AppPermissionUtils { + method public static boolean shouldSkipPermissionRequestDialog(@NonNull android.content.pm.GosPackageState, @NonNull String); + method public static boolean shouldSpoofPermissionRequestResult(@NonNull android.content.pm.GosPackageState, @NonNull String); + } + public class ApplicationInfo extends android.content.pm.PackageItemInfo implements android.os.Parcelable { + method @NonNull public android.ext.AppInfoExt ext(); method @RequiresPermission(android.Manifest.permission.DELETE_PACKAGES) public boolean hasFragileUserData(); method public boolean isEncryptionAware(); method public boolean isInstantApp(); @@ -4777,6 +4831,46 @@ package android.content.pm { method @NonNull public final int getType(); } + public final class GosPackageState implements android.os.Parcelable { + method @NonNull public android.content.pm.GosPackageState.Editor createEditor(@NonNull String, @NonNull android.os.UserHandle); + method @NonNull public android.content.pm.GosPackageState.Editor createEditor(@NonNull String, int); + method public int describeContents(); + method @NonNull public static android.content.pm.GosPackageState.Editor edit(@NonNull String, @NonNull android.os.UserHandle); + method @NonNull public static android.content.pm.GosPackageState.Editor edit(@NonNull String, int); + method @NonNull public static android.content.pm.GosPackageState get(@NonNull String, @NonNull android.os.UserHandle); + method @NonNull public static android.content.pm.GosPackageState get(@NonNull String, int); + method @NonNull public static android.content.pm.GosPackageState getForSelf(@NonNull android.content.Context); + method public boolean hasDerivedFlag(int); + method public boolean hasDerivedFlags(int); + method public boolean hasFlag(int); + method public boolean hasPackageFlag(int); + method public boolean isNone(); + 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[] storageScopes; + } + + public static class GosPackageState.Editor { + method @NonNull public android.content.pm.GosPackageState.Editor addFlag(int); + method @NonNull public android.content.pm.GosPackageState.Editor addPackageFlag(int); + method public boolean apply(); + method @NonNull public android.content.pm.GosPackageState.Editor clearFlag(int); + method @NonNull public android.content.pm.GosPackageState.Editor clearPackageFlag(int); + method @NonNull public android.content.pm.GosPackageState.Editor killUidAfterApply(); + 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 setNotifyUidAfterApply(boolean); + method @NonNull public android.content.pm.GosPackageState.Editor setPackageFlagState(int, boolean); + method @NonNull public android.content.pm.GosPackageState.Editor setStorageScopes(@Nullable byte[]); + } + + public interface GosPackageStateFlag { + field public static final int CONTACT_SCOPES_ENABLED = 5; // 0x5 + field public static final int STORAGE_SCOPES_ENABLED = 0; // 0x0 + } + public final class InstallationFile { method public long getLengthBytes(); method public int getLocation(); @@ -5030,6 +5124,7 @@ package android.content.pm { method @FlaggedApi("android.content.pm.uid_based_provider_lookup") @Nullable @RequiresPermission(android.Manifest.permission.RESOLVE_COMPONENT_FOR_UID) public android.content.pm.ProviderInfo resolveContentProviderForUid(@NonNull String, @NonNull android.content.pm.PackageManager.ComponentInfoFlags, int); method @RequiresPermission(android.Manifest.permission.REVOKE_RUNTIME_PERMISSIONS) public abstract void revokeRuntimePermission(@NonNull String, @NonNull String, @NonNull android.os.UserHandle); method @RequiresPermission(android.Manifest.permission.REVOKE_RUNTIME_PERMISSIONS) public void revokeRuntimePermission(@NonNull String, @NonNull String, @NonNull android.os.UserHandle, @NonNull String); + method @RequiresPermission(android.Manifest.permission.GRANT_RUNTIME_PERMISSIONS) public void sendBootCompletedBroadcastToPackage(@NonNull String, boolean, int); method public void sendDeviceCustomizationReadyBroadcast(); method @RequiresPermission(allOf={android.Manifest.permission.SET_PREFERRED_APPLICATIONS, android.Manifest.permission.INTERACT_ACROSS_USERS_FULL}) public abstract boolean setDefaultBrowserPackageNameAsUser(@Nullable String, int); method @NonNull @RequiresPermission(android.Manifest.permission.SUSPEND_APPS) public String[] setDistractingPackageRestrictions(@NonNull String[], int); @@ -5246,6 +5341,10 @@ package android.content.pm { field @NonNull public static final android.os.Parcelable.Creator CREATOR; } + public class SpecialRuntimePermAppUtils { + method public static boolean isInternetCompatEnabled(); + } + public final class SuspendDialogInfo implements android.os.Parcelable { method public int describeContents(); method public void writeToParcel(android.os.Parcel, int); @@ -5679,6 +5778,226 @@ package android.debug { } +package android.ext { + + public final class AppInfoExt implements android.os.Parcelable { + ctor public AppInfoExt(int, int, long); + method public int describeContents(); + method public static int getInitialPackageId(); + method public int getPackageId(); + method public boolean hasCompatChange(int); + method public boolean hasCompatConfig(); + method public boolean hasFlag(int); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator CREATOR; + } + + public interface AppInfoExtFlag { + field public static final int HAS_GMSCORE_CLIENT_LIBRARY = 0; // 0x0 + } + + public interface DerivedPackageFlag { + field public static final int DFLAGS_SET = 1; // 0x1 + field public static final int EXPECTS_ACCESS_TO_MEDIA_FILES_ONLY = 4; // 0x4 + field public static final int EXPECTS_ALL_FILES_ACCESS = 2; // 0x2 + field public static final int EXPECTS_LEGACY_EXTERNAL_STORAGE = 8192; // 0x2000 + field public static final int EXPECTS_STORAGE_WRITE_ACCESS = 8; // 0x8 + field public static final int HAS_ACCESS_MEDIA_LOCATION_DECLARATION = 256; // 0x100 + field public static final int HAS_GET_ACCOUNTS_DECLARATION = 4194304; // 0x400000 + field public static final int HAS_MANAGE_EXTERNAL_STORAGE_DECLARATION = 64; // 0x40 + field public static final int HAS_MANAGE_MEDIA_DECLARATION = 128; // 0x80 + field public static final int HAS_READ_CONTACTS_DECLARATION = 1048576; // 0x100000 + field public static final int HAS_READ_EXTERNAL_STORAGE_DECLARATION = 16; // 0x10 + field public static final int HAS_READ_MEDIA_AUDIO_DECLARATION = 512; // 0x200 + 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_WRITE_CONTACTS_DECLARATION = 2097152; // 0x200000 + field public static final int HAS_WRITE_EXTERNAL_STORAGE_DECLARATION = 32; // 0x20 + } + + public interface KnownSystemPackage { + field public static final int SETTINGS = 0; // 0x0 + field public static final int SETUP_WIZARD = 3; // 0x3 + field public static final int SHELL = 1; // 0x1 + field public static final int SYSTEM_UI = 2; // 0x2 + } + + public final class KnownSystemPackages { + method @NonNull public static android.ext.KnownSystemPackages get(@NonNull android.content.Context); + method @NonNull public String getById(int); + field @NonNull public final String contactsProvider; + field @NonNull public final String launcher; + field @NonNull public final String mediaProvider; + field @NonNull public final String permissionController; + field @NonNull public final String settings; + field @NonNull public final String setupWizard; + field @NonNull public final String shell; + field @NonNull public final String systemUi; + } + + public interface PackageId { + field public static final int ANDROID_AUTO = 10; // 0xa + field public static final String ANDROID_AUTO_NAME = "com.google.android.projection.gearhead"; + field public static final int EUICC_SUPPORT_PIXEL = 5; // 0x5 + field public static final String EUICC_SUPPORT_PIXEL_NAME = "com.google.euiccpixel"; + field public static final int GMS_CORE = 2; // 0x2 + field public static final String GMS_CORE_NAME = "com.google.android.gms"; + field public static final String GSF_NAME = "com.google.android.gsf"; + field public static final int G_CAMERA = 8; // 0x8 + field public static final String G_CAMERA_NAME = "com.google.android.GoogleCamera"; + field public static final int G_CARRIER_SETTINGS = 7; // 0x7 + field public static final String G_CARRIER_SETTINGS_NAME = "com.google.android.carrier"; + field public static final int G_EUICC_LPA = 6; // 0x6 + field public static final String G_EUICC_LPA_NAME = "com.google.android.euicc"; + field public static final int G_SEARCH_APP = 4; // 0x4 + field public static final String G_SEARCH_APP_NAME = "com.google.android.googlequicksearchbox"; + field public static final int PIXEL_CAMERA_SERVICES = 9; // 0x9 + field public static final String PIXEL_CAMERA_SERVICES_NAME = "com.google.android.apps.camera.services"; + field public static final int PIXEL_HEALTH = 13; // 0xd + field public static final String PIXEL_HEALTH_NAME = "com.google.android.apps.pixel.health"; + field public static final int PLAY_STORE = 3; // 0x3 + field public static final String PLAY_STORE_NAME = "com.android.vending"; + field public static final int TYCHO = 11; // 0xb + field public static final String TYCHO_NAME = "com.google.android.apps.tycho"; + field public static final int UNKNOWN = 0; // 0x0 + } + +} + +package android.ext.cscopes { + + public final class ContactScope implements android.os.Parcelable { + ctor public ContactScope(int, long, @Nullable String, @Nullable String, @Nullable android.net.Uri); + method public int describeContents(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator CREATOR; + field public static final int TYPE_CONTACT = 1; // 0x1 + field public static final int TYPE_COUNT = 4; // 0x4 + field public static final int TYPE_EMAIL = 3; // 0x3 + field public static final int TYPE_GROUP = 0; // 0x0 + field public static final int TYPE_NUMBER = 2; // 0x2 + field @Nullable public final android.net.Uri detailsUri; + field public final long id; + field @Nullable public final String summary; + field @Nullable public final String title; + field public final int type; + } + + public class ContactScopesApi { + method @NonNull public static android.content.Intent createConfigActivityIntent(@NonNull String); + field public static final String ACTION_NOTIFY_CONTENT_OBSERVERS = "android.ext.cscopes.action.NOTIFY_CONTENT_OBSERVERS"; + field public static final String KEY_IDS = "ids"; + field public static final String KEY_RESULT = "result"; + field public static final String KEY_URIS = "uris"; + field public static final String METHOD_GET_GROUPS = "get_groups"; + field public static final String METHOD_GET_IDS_FROM_URIS = "get_ids_from_uris"; + field public static final String METHOD_GET_VIEW_MODEL = "get_view_model"; + field public static final String SCOPED_CONTACTS_PROVIDER_AUTHORITY = "com.android.contacts.scoped"; + } + + public final class ContactScopesStorage { + method public boolean add(int, long); + method @NonNull public static android.ext.cscopes.ContactScopesStorage deserialize(@NonNull android.content.pm.GosPackageState); + method public int getCount(); + method @NonNull public long[] getIds(int); + method public static boolean isEmpty(@NonNull android.content.pm.GosPackageState); + method public boolean remove(int, long); + method @Nullable public byte[] serialize(); + field public static final int MAX_COUNT_PER_PACKAGE = 100; // 0x64 + } + + public final class ContactsGroup implements android.os.Parcelable { + ctor public ContactsGroup(long, @Nullable String, @Nullable String); + method public int describeContents(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator CREATOR; + field public final long id; + field @Nullable public final String summary; + field @Nullable public final String title; + } + +} + +package android.ext.dcl { + + public class DynCodeLoading { + method public static void checkDexFileOpen(int, @NonNull String) throws Exception; + method public static void checkInMemoryDexFileOpen(int); + } + +} + +package android.ext.settings { + + public class BoolSetting extends android.ext.settings.Setting { + ctor public BoolSetting(@NonNull android.ext.settings.Setting.Scope, @NonNull String, boolean); + ctor public BoolSetting(@NonNull android.ext.settings.Setting.Scope, @NonNull String, @NonNull java.util.function.Function); + method public final boolean get(@NonNull android.content.Context); + method public final boolean get(@NonNull android.content.Context, int); + method public final boolean put(@NonNull android.content.Context, boolean); + } + + public class BoolSysProperty extends android.ext.settings.BoolSetting { + ctor public BoolSysProperty(@NonNull String, boolean); + ctor public BoolSysProperty(@NonNull String, @NonNull java.util.function.Function); + method public boolean get(); + method public boolean put(boolean); + } + + public class IntSetting extends android.ext.settings.Setting { + ctor public IntSetting(@NonNull android.ext.settings.Setting.Scope, @NonNull String, int); + ctor public IntSetting(@NonNull android.ext.settings.Setting.Scope, @NonNull String, int, @NonNull int...); + ctor public IntSetting(@NonNull android.ext.settings.Setting.Scope, @NonNull String, @NonNull java.util.function.ToIntFunction); + ctor public IntSetting(@NonNull android.ext.settings.Setting.Scope, @NonNull String, @NonNull java.util.function.ToIntFunction, @NonNull int...); + method public final int get(@NonNull android.content.Context); + method public final int get(@NonNull android.content.Context, int); + method public final boolean put(@NonNull android.content.Context, int); + method public boolean validateValue(int); + } + + public class IntSysProperty extends android.ext.settings.IntSetting { + ctor public IntSysProperty(@NonNull String, int); + ctor public IntSysProperty(@NonNull String, int, @NonNull int...); + ctor public IntSysProperty(@NonNull String, @NonNull java.util.function.ToIntFunction); + ctor public IntSysProperty(@NonNull String, @NonNull java.util.function.ToIntFunction, @NonNull int...); + method public int get(); + method public boolean put(int); + } + + public abstract class Setting { + method public final boolean canObserveState(); + method @NonNull public final String getKey(); + method @NonNull public final android.ext.settings.Setting.Scope getScope(); + method @NonNull public final Object registerObserver(@NonNull android.content.Context, @NonNull android.os.Handler, @NonNull java.util.function.Consumer); + method @NonNull public final Object registerObserver(@NonNull android.content.Context, int, @NonNull android.os.Handler, @NonNull java.util.function.Consumer); + method public final void unregisterObserver(@NonNull android.content.Context, @NonNull Object); + } + + public enum Setting.Scope { + enum_constant public static final android.ext.settings.Setting.Scope GLOBAL; + enum_constant public static final android.ext.settings.Setting.Scope PER_USER; + enum_constant public static final android.ext.settings.Setting.Scope SYSTEM_PROPERTY; + } + + public class StringSetting extends android.ext.settings.Setting { + ctor public StringSetting(@NonNull android.ext.settings.Setting.Scope, @NonNull String, @NonNull String); + ctor public StringSetting(@NonNull android.ext.settings.Setting.Scope, @NonNull String, @NonNull java.util.function.Function); + method @NonNull public final String get(@NonNull android.content.Context); + method @NonNull public final String get(@NonNull android.content.Context, int); + method public final boolean put(@NonNull android.content.Context, @NonNull String); + method public boolean validateValue(@NonNull String); + } + + public class StringSysProperty extends android.ext.settings.StringSetting { + ctor public StringSysProperty(@NonNull String, @NonNull String); + ctor public StringSysProperty(@NonNull String, @NonNull java.util.function.Function); + method @NonNull public String get(); + method public boolean put(@NonNull String); + } + +} + package android.graphics.fonts { @FlaggedApi("com.android.text.flags.insert_font_family") public final class FallbackFontUpdateRequest { @@ -12151,6 +12470,11 @@ package android.nfc.cardemulation { package android.os { + public class BaseBundle { + method @Nullable public Boolean getBoolean2(@NonNull String); + method @Nullable public T getNumber(@NonNull String); + } + public class BatteryManager { method @RequiresPermission(android.Manifest.permission.POWER_SAVER) public boolean setChargingStateUpdateDelayMillis(int); field @RequiresPermission(android.Manifest.permission.BATTERY_STATS) public static final int BATTERY_PROPERTY_CHARGING_POLICY = 9; // 0x9 @@ -12762,6 +13086,10 @@ package android.os { field @Deprecated public static final int TEMPORARY_ALLOWLIST_TYPE_FOREGROUND_SERVICE_NOT_ALLOWED = 1; // 0x1 } + public class Process { + method public static boolean isMemoryTaggingSupported(); + } + public class RecoverySystem { method @RequiresPermission(android.Manifest.permission.RECOVERY) public static void cancelScheduledUpdate(android.content.Context) throws java.io.IOException; method @RequiresPermission(anyOf={android.Manifest.permission.RECOVERY, android.Manifest.permission.REBOOT}) public static void clearPrepareForUnattendedUpdate(@NonNull android.content.Context) throws java.io.IOException; diff --git a/core/java/Android.bp b/core/java/Android.bp index aef8f8cc1cf3e..4d07d0bc6ba47 100644 --- a/core/java/Android.bp +++ b/core/java/Android.bp @@ -416,6 +416,11 @@ filegroup { visibility: ["//frameworks/base/test-mock"], } +filegroup { + name: "GosPackageStateFlags", + srcs: [ "android/content/pm/GosPackageStateFlag.java", ], +} + filegroup { name: "libincident_aidl", srcs: [ diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index 7dcebdb65b6dc..d7baf9e230d84 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -54,6 +54,7 @@ import android.app.assist.AssistContent; import android.app.compat.CompatChanges; import android.app.jank.JankTracker; +import android.app.compat.gms.GmsCompat; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; import android.compat.annotation.UnsupportedAppUsage; @@ -180,6 +181,9 @@ import com.android.internal.app.IVoiceInteractor; import com.android.internal.app.ToolbarActionBar; import com.android.internal.app.WindowDecorActionBar; +import com.android.internal.gmscompat.GmsCompatApp; +import com.android.internal.gmscompat.GmsHooks; +import com.android.internal.gmscompat.util.GmcActivityUtils; import com.android.internal.policy.DecorView; import com.android.internal.policy.PhoneWindow; import com.android.internal.util.dump.DumpableContainerImpl; @@ -1891,6 +1895,10 @@ private void notifyVoiceInteractionManagerServiceActivityEvent( @MainThread @CallSuper protected void onCreate(@Nullable Bundle savedInstanceState) { + if (GmsCompat.isEnabled()) { + GmsHooks.activityOnCreate(this); + } + if (DEBUG_LIFECYCLE) Slog.v(TAG, "onCreate " + this + ": " + savedInstanceState); if (mLastNonConfigurationInstances != null) { @@ -6064,6 +6072,44 @@ public void startActivityForResult(@RequiresPermission Intent intent, int reques */ public void startActivityForResult(@RequiresPermission Intent intent, int requestCode, @Nullable Bundle options) { + if (GmsCompat.isEnabled()) { + Intent orig = intent; + intent = GmcActivityUtils.overrideStartActivityIntent(intent); + if (intent == null) { + Log.d("GmsCompat", "skipped startActivity for " + orig, new Throwable()); + return; + } + } + + if (android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS.equals(intent.getAction())) { + Uri data = intent.getData(); + if (data != null && "package".equals(data.getScheme())) { + String pkg = data.getSchemeSpecificPart(); + + if (pkg != null) { + switch (pkg) { + case "com.google.android.tts": + if (GmsCompat.isClientOfGmsCore()) { + boolean installed; + try { + installed = ActivityThread.getPackageManager().getApplicationInfo(pkg, 0, getUserId()) != null; + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + + if (!installed) { + try { + GmsCompatApp.iClientOfGmsCore2Gca().showMissingAppNotification(pkg); + } catch (RemoteException e) { + GmsCompatApp.callFailed(e); + } + } + } + } + } + } + } + if (mParent == null) { options = transferSpringboardActivityOptions(options); Instrumentation.ActivityResult ar = @@ -6444,6 +6490,13 @@ public void startIntentSenderForResultInner(IntentSender intent, String who, int @Nullable Bundle options) throws IntentSender.SendIntentException { try { + if (intent != null) { + String pkg = intent.getCreatorPackage(); + if (pkg != null && GmsCompat.isGmsAppAndUnprivilegedProcess(pkg)) { + options = GmcActivityUtils.allowActivityLaunchFromPendingIntent(options); + } + } + options = transferSpringboardActivityOptions(options); String resolvedType = null; if (fillInIntent != null) { diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index 867b087c42835..9e40fddfebc91 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -42,6 +42,7 @@ import android.annotation.SystemService; import android.annotation.TestApi; import android.annotation.UserIdInt; +import android.app.compat.gms.GmsCompat; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; import android.compat.annotation.UnsupportedAppUsage; @@ -103,6 +104,8 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.app.LocalePicker; import com.android.internal.app.procstats.ProcessStats; +import com.android.internal.gmscompat.sysservice.GmcUserManager; +import com.android.internal.gmscompat.GmsHooks; import com.android.internal.os.RoSystemProperties; import com.android.internal.os.TransferPipe; import com.android.internal.util.FastPrintWriter; @@ -3957,6 +3960,10 @@ public static class ProcessErrorStateInfo implements Parcelable { */ public byte[] crashData = null; + /** @hide */ + @Nullable + public String tracesFilePath; + public ProcessErrorStateInfo() { } @@ -3975,6 +3982,7 @@ public void writeToParcel(Parcel dest, int flags) { dest.writeString(shortMsg); dest.writeString(longMsg); dest.writeString(stackTrace); + dest.writeString(tracesFilePath); } public void readFromParcel(Parcel source) { @@ -3986,6 +3994,7 @@ public void readFromParcel(Parcel source) { shortMsg = source.readString(); longMsg = source.readString(); stackTrace = source.readString(); + tracesFilePath = source.readString(); } public static final @android.annotation.NonNull Creator CREATOR = @@ -4569,7 +4578,11 @@ public boolean setProcessMemoryTrimLevel( public List getRunningAppProcesses() { return mRunningProcessesCache.get(() -> { try { - return getService().getRunningAppProcesses(); + List res = getService().getRunningAppProcesses(); + if (GmsCompat.isEnabled()) { + res = GmsHooks.addRecentlyBoundPids(mContext, res); + } + return res; } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -5620,6 +5633,10 @@ public static int checkUidPermission(String permission, int uid) { }) @RavenwoodRedirect public static int getCurrentUser() { + if (GmsCompat.isEnabled()) { + return GmcUserManager.amGetCurrentUser(); + } + return mGetCurrentUserIdCache.query(null); } @@ -5939,6 +5956,10 @@ public boolean stopUser(@UserIdInt int userId) { */ @UnsupportedAppUsage public boolean isUserRunning(int userId) { + if (GmsCompat.isEnabled()) { + return GmcUserManager.amIsUserRunning(userId); + } + try { return getService().isUserRunning(userId, 0); } catch (RemoteException e) { @@ -6106,6 +6127,11 @@ public static IActivityManager getService() { return IActivityManagerSingleton.get(); } + /** @hide */ + public static void clearCachedService() { + IActivityManagerSingleton.clear(); + } + private static IActivityTaskManager getTaskService() { return ActivityTaskManager.getService(); } diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java index 83077ffe76e21..e11ac64eee47e 100644 --- a/core/java/android/app/ActivityManagerInternal.java +++ b/core/java/android/app/ActivityManagerInternal.java @@ -40,6 +40,7 @@ import android.content.pm.ActivityInfo; import android.content.pm.ActivityPresentationInfo; import android.content.pm.ApplicationInfo; +import android.content.pm.GosPackageState; import android.content.pm.IPackageDataObserver; import android.content.pm.UserInfo; import android.net.Uri; @@ -1476,4 +1477,26 @@ public abstract void getExecutableMethodFileOffsets(@NonNull String processName, * @param clientUid the UID that has a bound client */ public abstract boolean hasServiceBindingOrProviderUse(int uid, int clientUid); + + public abstract void onGosPackageStateChanged(int uid, GosPackageState state); + + public static class ProcessRecordSnapshot { + public final int pid; + public final int uid; // process UID, might differ from app UID in appInfo.uid + public final int userId; + public final String name; + public final ApplicationInfo appInfo; // first loaded app in the process + public final String[] pkgList; + + public ProcessRecordSnapshot(int pid, int uid, int userId, String name, ApplicationInfo appInfo, String[] pkgList) { + this.pid = pid; + this.uid = uid; + this.userId = userId; + this.name = name; + this.appInfo = appInfo; + this.pkgList = pkgList; + } + } + + public abstract ProcessRecordSnapshot getProcessRecordByPid(int pid); } diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index b7d9f653023e9..8e225a181e46f 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -62,6 +62,7 @@ import android.app.backup.BackupAnnotations.BackupDestination; import android.app.backup.BackupAnnotations.OperationType; import android.app.compat.CompatChanges; +import android.app.compat.gms.GmsCompat; import android.app.sdksandbox.sandboxactivity.ActivityContextInfo; import android.app.sdksandbox.sandboxactivity.SdkSandboxActivityAuthority; import android.app.servertransaction.ActivityLifecycleItem; @@ -95,6 +96,7 @@ import android.content.pm.ActivityInfo; import android.content.pm.ApplicationInfo; import android.content.pm.ComponentInfo; +import android.content.pm.GosPackageState; import android.content.pm.IPackageManager; import android.content.pm.InstrumentationInfo; import android.content.pm.PackageInfo; @@ -248,6 +250,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.IVoiceInteractor; import com.android.internal.content.ReferrerIntent; +import com.android.internal.gmscompat.GmcDebug; import com.android.internal.os.ApplicationSharedMemory; import com.android.internal.os.BackgroundThread; import com.android.internal.os.BinderCallsStats; @@ -1017,6 +1020,7 @@ static final class AppBindData { @UnsupportedAppUsage AppBindData() { } + Bundle extraArgs; @UnsupportedAppUsage LoadedApk info; @UnsupportedAppUsage @@ -1423,6 +1427,7 @@ public final void scheduleTimeoutServiceForType(IBinder token, int startId, @Override @RavenwoodThrow(comment = "See ActivityThread_ravenwood for initialization on Ravenwood") public final void bindApplication( + Bundle extraArgs, String processName, ApplicationInfo appInfo, String sdkSandboxClientAppVolumeUuid, @@ -1509,6 +1514,7 @@ public final void bindApplication( } AppBindData data = new AppBindData(); + data.extraArgs = extraArgs; data.processName = processName; data.appInfo = appInfo; data.sdkSandboxClientAppVolumeUuid = sdkSandboxClientAppVolumeUuid; @@ -2509,6 +2515,12 @@ public void getExecutableMethodFileOffsets( throw e.rethrowFromSystemServer(); } } + + @Override + public void onGosPackageStateChanged(GosPackageState state) { + // this is a oneway method, caller (ActivityManager) will not be blocked + ActivityThreadHooks.onGosPackageStateChanged(mInitialApplication, state, false); + } } private @NonNull SafeCancellationTransport createSafeCancellationTransport( @@ -3202,6 +3214,11 @@ public static IPackageManager getPackageManager() { return sPackageManager; } + /** @hide */ + public static void clearCachedPackageManager() { + sPackageManager = null; + } + /** Returns the permission manager */ public static IPermissionManager getPermissionManager() { if (sPermissionManager != null) { @@ -4298,6 +4315,10 @@ public void updatePendingConfiguration(Configuration config) { } } + public int getProcessState() { + return mLastProcessState; + } + @Override public void updateProcessState(int processState, boolean fromIpc) { synchronized (mAppThread) { @@ -5383,6 +5404,9 @@ private void handleReceiver(ReceiverData data) { sCurrentBroadcastIntent.set(data.intent); receiver.setPendingResult(data); + if (GmsCompat.isEnabled()) { + GmcDebug.maybeLogReceiveBroadcast(receiver, data.intent, true); + } receiver.onReceive(context.getReceiverRestrictedContext(), data.intent); } catch (Exception e) { @@ -5612,8 +5636,14 @@ private void handleCreateService(CreateServiceData data) { } else { cl = packageInfo.getClassLoader(); } - service = packageInfo.getAppFactory() - .instantiateService(cl, data.info.name, data.intent); + { + String className = data.info.name; + service = ActivityThreadHooks.instantiateService(className); + if (service == null) { + service = packageInfo.getAppFactory() + .instantiateService(cl, className, data.intent); + } + } ContextImpl context = ContextImpl.getImpl(service .createServiceBaseContext(this, packageInfo)); if (data.info.splitName != null) { @@ -5793,6 +5823,15 @@ private void handleDumpProvider(DumpComponentInfo info) { r.mLocalProvider.dump(info.fd.getFileDescriptor(), pw, info.args); pw.flush(); } + } catch (NoSuchMethodError e) { + if (android.app.compat.gms.GmsCompat.isEnabled()) { + // one of the GSF content providers accesses a hidden method from ContentProvider.dump(), + // which leads to a confusing crash when a bugreport is being taken (dumps of all + // of the active ContentProviders are included in bugreports) + Log.d(TAG, "handleDumpProvider", e); + } else { + throw e; + } } finally { IoUtils.closeQuietly(info.fd); StrictMode.setThreadPolicy(oldPolicy); @@ -5811,6 +5850,9 @@ private void handleServiceArgs(ServiceArgsData data) { } int res; if (!data.taskRemoved) { + if (GmsCompat.isEnabled()) { + GmcDebug.maybeLogServiceOnStartCommand(s, data.args, data.flags, data.startId); + } res = s.onStartCommand(data.args, data.flags, data.startId); } else { s.onTaskRemoved(data.args); @@ -7972,6 +8014,8 @@ private String getInstrumentationLibrary(ApplicationInfo appInfo, Instrumentatio @UnsupportedAppUsage @RavenwoodThrow(comment = "See ActivityThread_ravenwood for initialization on Ravenwood") private void handleBindApplication(AppBindData data) { + final Bundle extraAppBindArgs = ActivityThreadHooks.onBind(data); + mDdmSyncStageUpdater.next(Stage.Bind); // Register the UI Thread as a sensitive thread to the runtime. @@ -8235,6 +8279,10 @@ private void handleBindApplication(AppBindData data) { dalvik.system.VMRuntime.getRuntime().clampGrowthLimit(); } + ActivityThreadHooks.onBind2(appContext, extraAppBindArgs); + + com.android.internal.os.ExecSpawning.maybeRunPendingZygotePreload(data.appInfo); + // Allow disk access during application and provider setup. This could // block processing ordered broadcasts, but later processing would // probably end up doing the same disk access. diff --git a/core/java/android/app/ActivityThreadHooks.java b/core/java/android/app/ActivityThreadHooks.java new file mode 100644 index 0000000000000..6542e25e887de --- /dev/null +++ b/core/java/android/app/ActivityThreadHooks.java @@ -0,0 +1,72 @@ +package android.app; + +import android.content.Context; +import android.content.pm.GosPackageState; +import android.content.pm.SrtPermissions; +import android.content.res.AssetManager; +import android.ext.dcl.DynCodeLoading; +import android.location.HookedLocationManager; +import android.os.Bundle; +import android.os.Process; + +import com.android.internal.app.ContactScopes; +import com.android.internal.app.StorageScopesAppHooks; +import com.android.internal.gmscompat.GmsHooks; + +import java.util.Objects; + +class ActivityThreadHooks { + + private static volatile boolean called; + + // called after the initial app context is constructed + // ActivityThread.handleBindApplication + static Bundle onBind(ActivityThread.AppBindData appBindData) { + Bundle args = appBindData.extraArgs; + Objects.requireNonNull(args, "args bundle is null"); + + if (called) { + throw new IllegalStateException("onBind called for the second time"); + } + called = true; + + AssetManager.systemIdmapPaths_ = args.getStringArray(AppBindArgs.KEY_SYSTEM_IDMAP_PATHS); + + AppGlobals.setInitialPackageId(appBindData.appInfo.ext().getPackageId()); + + int[] flags = Objects.requireNonNull(args.getIntArray(AppBindArgs.KEY_FLAGS_ARRAY)); + + SrtPermissions.setFlags(flags[AppBindArgs.FLAGS_IDX_SPECIAL_RUNTIME_PERMISSIONS]); + + HookedLocationManager.setFlags(flags[AppBindArgs.FLAGS_IDX_HOOKED_LOCATION_MANAGER]); + + DynCodeLoading.handleAppBindFlags(flags[AppBindArgs.FLAGS_IDX_DYN_CODE_LOADING]); + + return args; + } + + // called after ActivityThread instrumentation is inited, which happens before execution of any + // of app's code + // ActivityThread.handleBindApplication + static void onBind2(Context appContext, Bundle appBindArgs) { + GosPackageState gosPs = appBindArgs.getParcelable(AppBindArgs.KEY_GOS_PACKAGE_STATE, + GosPackageState.class); + onGosPackageStateChanged(appContext, gosPs, true); + } + + // called from both main and worker threads + static void onGosPackageStateChanged(Context ctx, GosPackageState state, boolean fromBind) { + if (!Process.isIsolated()) { + StorageScopesAppHooks.maybeEnable(state); + ContactScopes.maybeEnable(ctx, state); + } + } + + static Service instantiateService(String className) { + Service res = null; + if (res == null) { + res = GmsHooks.maybeInstantiateService(className); + } + return res; + } +} diff --git a/core/java/android/app/AppBindArgs.java b/core/java/android/app/AppBindArgs.java new file mode 100644 index 0000000000000..207e6946f5acf --- /dev/null +++ b/core/java/android/app/AppBindArgs.java @@ -0,0 +1,14 @@ +package android.app; + +/** @hide */ +public interface AppBindArgs { + String KEY_GOS_PACKAGE_STATE = "gosPs"; + String KEY_SYSTEM_IDMAP_PATHS = "idmapPaths"; + String KEY_FLAGS_ARRAY = "flagsArr"; + + int FLAGS_IDX_SPECIAL_RUNTIME_PERMISSIONS = 0; + int FLAGS_IDX_HOOKED_LOCATION_MANAGER = 1; + int FLAGS_IDX_DYN_CODE_LOADING = 2; + + int FLAGS_ARRAY_LEN = 10; +} diff --git a/core/java/android/app/AppGlobals.java b/core/java/android/app/AppGlobals.java index bbf6dfa5c9686..5feae4b733a6e 100644 --- a/core/java/android/app/AppGlobals.java +++ b/core/java/android/app/AppGlobals.java @@ -18,6 +18,7 @@ import android.compat.annotation.UnsupportedAppUsage; import android.content.pm.IPackageManager; +import android.ext.PackageId; import android.permission.IPermissionManager; /** @@ -43,6 +44,17 @@ public static String getInitialPackage() { return ActivityThread.currentPackageName(); } + private static int initialPackageId = PackageId.UNKNOWN; + + public static void setInitialPackageId(int value) { + initialPackageId = value; + } + + // PackageId of the first APK loaded into the process + public static int getInitialPackageId() { + return initialPackageId; + } + /** * Return the raw interface to the package manager. * @return The package manager. diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index dfdd8b063604e..42d30c373d4f4 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -40,6 +40,7 @@ import android.annotation.SystemApi; import android.annotation.SystemService; import android.annotation.TestApi; +import android.app.compat.gms.GmsCompat; import android.app.usage.UsageStatsManager; import android.companion.virtual.VirtualDeviceManager; import android.compat.Compatibility; @@ -50,6 +51,7 @@ import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; +import android.content.pm.AppPermissionUtils; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.pm.ParceledListSlice; @@ -9916,6 +9918,11 @@ private int checkOpRawNoThrow(int op, int uid, @NonNull String packageName, mode = mService.checkOperationRawForDevice( op, uid, packageName, attributionTag, virtualDeviceId); } + if (mode != MODE_ALLOWED && uid == Process.myUid()) { + if (AppPermissionUtils.shouldSpoofSelfAppOpCheck(op)) { + return MODE_ALLOWED; + } + } return mode; } catch (RemoteException e) { throw e.rethrowFromSystemServer(); @@ -10145,6 +10152,10 @@ private boolean batchDuplicateNoteOps(int op, int uid, @Nullable String packageN private int noteOpNoThrow(int op, int uid, @Nullable String packageName, @Nullable String attributionTag, int virtualDeviceId, @Nullable String message) { + if (GmsCompat.isEnabled() && uid != Process.myUid()) { + return noteProxyOpNoThrow(opToPublicName(op), packageName, uid, attributionTag, message); + } + try { collectNoteOpCallsForValidation(op); int collectionMode = getNotedOpCollectionMode(uid, packageName, op); @@ -10193,7 +10204,15 @@ private int noteOpNoThrow(int op, int uid, @Nullable String packageName, } } - return syncOp.getOpMode(); + final int mode = syncOp.getOpMode(); + + if (mode != MODE_ALLOWED && uid == Process.myUid()) { + if (AppPermissionUtils.shouldSpoofSelfAppOpCheck(op)) { + return MODE_ALLOWED; + } + } + + return mode; } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -10368,7 +10387,21 @@ public int noteProxyOpNoThrow(int op, @NonNull AttributionSource attributionSour } } - return syncOp.getOpMode(); + final int mode = syncOp.getOpMode(); + + if (mode != MODE_ALLOWED) { + int uid = attributionSource.getUid(); + int nextUid = attributionSource.getNextUid(); + boolean selfCheck = (uid == myUid) && (nextUid == myUid || nextUid == Process.INVALID_UID); + + if (selfCheck) { + if (AppPermissionUtils.shouldSpoofSelfAppOpCheck(op)) { + return MODE_ALLOWED; + } + } + } + + return mode; } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -10481,12 +10514,29 @@ private int checkOpNoThrow(int op, int uid, String packageName, @Nullable String mode = mService.checkOperationForDevice(op, uid, packageName, attributionTag, virtualDeviceId); } + + if (mode != MODE_ALLOWED && uid == Process.myUid()) { + if (AppPermissionUtils.shouldSpoofSelfAppOpCheck(op)) { + return MODE_ALLOWED; + } + } + return mode; } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } + /** @hide */ + @SystemApi + public void checkHistoricalRegistryConfig(@NonNull String[] extraPermissions) { + try { + mService.checkHistoricalRegistryConfig(extraPermissions); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + /** * @deprecated Use {@link PackageManager#getPackageUid} instead */ @@ -10744,6 +10794,10 @@ private int startOpNoThrow(@NonNull IBinder token, int op, int uid, @NonNull Str boolean startIfModeDefault, @Nullable String attributionTag, int virtualDeviceId, @Nullable String message, @AttributionFlags int attributionFlags, int attributionChainId) { + if (GmsCompat.isEnabled() && uid != Process.myUid()) { + return startProxyOpNoThrow(opToPublicName(op), uid, packageName, attributionTag, message); + } + try { collectNoteOpCallsForValidation(op); int collectionMode = getNotedOpCollectionMode(uid, packageName, op); @@ -10992,6 +11046,11 @@ public void finishOp(IBinder token, int op, int uid, @NonNull String packageName private void finishOp(IBinder token, int op, int uid, @NonNull String packageName, @Nullable String attributionTag, int virtualDeviceId ) { + if (GmsCompat.isEnabled() && uid != Process.myUid()) { + finishProxyOp(opToPublicName(op), uid, packageName, attributionTag); + return; + } + try { if (virtualDeviceId == Context.DEVICE_ID_DEFAULT) { mService.finishOperation(token, op, uid, packageName, attributionTag); diff --git a/core/java/android/app/Application.java b/core/java/android/app/Application.java index ed22fb961ab62..4e7925f285938 100644 --- a/core/java/android/app/Application.java +++ b/core/java/android/app/Application.java @@ -20,6 +20,7 @@ import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.compat.gms.GmsCompat; import android.compat.annotation.UnsupportedAppUsage; import android.content.ComponentCallbacks; import android.content.ComponentCallbacks2; @@ -242,6 +243,11 @@ public interface OnProvideAssistDataListener { public Application() { super(null); + + if (GmsCompat.isEnabled()) { + registerActivityLifecycleCallbacks(com.android.internal.gmscompat.util + .GmcActivityUtils.INSTANCE); + } } private String getLoadedApkInfo() { diff --git a/core/java/android/app/ApplicationErrorReport.aidl b/core/java/android/app/ApplicationErrorReport.aidl index 5b57457a33152..34a05ab33ae1f 100644 --- a/core/java/android/app/ApplicationErrorReport.aidl +++ b/core/java/android/app/ApplicationErrorReport.aidl @@ -17,4 +17,5 @@ package android.app; /** @hide */ -parcelable ApplicationErrorReport.ParcelableCrashInfo; \ No newline at end of file +parcelable ApplicationErrorReport; +parcelable ApplicationErrorReport.ParcelableCrashInfo; diff --git a/core/java/android/app/ApplicationErrorReport.java b/core/java/android/app/ApplicationErrorReport.java index 02955136d4636..c2853a789b45d 100644 --- a/core/java/android/app/ApplicationErrorReport.java +++ b/core/java/android/app/ApplicationErrorReport.java @@ -16,6 +16,7 @@ package android.app; +import android.annotation.Nullable; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -25,6 +26,8 @@ import android.os.Binder; import android.os.Parcel; import android.os.Parcelable; +import android.os.Process; +import android.os.SystemClock; import android.os.SystemProperties; import android.provider.Settings; import android.util.Printer; @@ -98,6 +101,9 @@ public class ApplicationErrorReport implements Parcelable { */ public String packageName; + /** @hide */ + public ApplicationInfo applicationInfo; + /** * Package name of the application which installed the application this * report pertains to. @@ -162,13 +168,19 @@ public static ComponentName getErrorReportReceiver(Context context, String packageName, int appFlags) { // check if error reporting is enabled in secure settings int enabled = Settings.Global.getInt(context.getContentResolver(), - Settings.Global.SEND_ACTION_APP_ERROR, 0); + Settings.Global.SEND_ACTION_APP_ERROR, 1); if (enabled == 0) { return null; } PackageManager pm = context.getPackageManager(); + ComponentName logViewerApp = getErrorReportReceiver(pm, packageName, + android.ext.LogViewerApp.getPackageName()); + if (logViewerApp != null) { + return logViewerApp; + } + // look for receiver in the installer package String candidate = null; ComponentName result = null; @@ -233,6 +245,11 @@ static ComponentName getErrorReportReceiver(PackageManager pm, String errorPacka public void writeToParcel(Parcel dest, int flags) { dest.writeInt(type); dest.writeString(packageName); + ApplicationInfo appInfo = applicationInfo; + dest.writeBoolean(appInfo != null); + if (appInfo != null) { + appInfo.writeToParcel(dest, 0); + } dest.writeString(installerPackageName); dest.writeString(processName); dest.writeLong(time); @@ -260,6 +277,9 @@ public void writeToParcel(Parcel dest, int flags) { public void readFromParcel(Parcel in) { type = in.readInt(); packageName = in.readString(); + if (in.readBoolean()) { + applicationInfo = ApplicationInfo.CREATOR.createFromParcel(in); + } installerPackageName = in.readString(); processName = in.readString(); time = in.readLong(); @@ -345,6 +365,11 @@ public static class CrashInfo { */ public String crashTag; + /** @hide */ + public long processUptimeMs; + /** @hide */ + public long processStartupLatencyMs; + /** * Create an uninitialized instance of CrashInfo. */ @@ -398,6 +423,9 @@ public CrashInfo(Throwable tr) { } exceptionMessage = sanitizeString(exceptionMessage); + + processUptimeMs = SystemClock.elapsedRealtime() - Process.getStartElapsedRealtime(); + processStartupLatencyMs = Process.getStartElapsedRealtime() - Process.getStartRequestedElapsedRealtime(); } /** @hide */ @@ -439,6 +467,8 @@ public CrashInfo(Parcel in) { throwLineNumber = in.readInt(); stackTrace = in.readString(); crashTag = in.readString(); + processUptimeMs = in.readLong(); + processStartupLatencyMs = in.readLong(); } /** @@ -455,6 +485,8 @@ public void writeToParcel(Parcel dest, int flags) { dest.writeInt(throwLineNumber); dest.writeString(stackTrace); dest.writeString(crashTag); + dest.writeLong(processUptimeMs); + dest.writeLong(processStartupLatencyMs); int total = dest.dataPosition()-start; if (Binder.CHECK_PARCEL_SIZE && total > 20*1024) { Slog.d("Error", "ERR: exHandler=" + exceptionHandlerClassName); @@ -543,6 +575,10 @@ public static class AnrInfo { */ public String info; + /** @hide */ + @Nullable + public String tracesFilePath; + /** * Create an uninitialized instance of AnrInfo. */ @@ -556,6 +592,7 @@ public AnrInfo(Parcel in) { activity = in.readString(); cause = in.readString(); info = in.readString(); + tracesFilePath = in.readString(); } /** @@ -565,6 +602,7 @@ public void writeToParcel(Parcel dest, int flags) { dest.writeString(activity); dest.writeString(cause); dest.writeString(info); + dest.writeString(tracesFilePath); } /** diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java index ce6929f0a2df1..b1f0fc042391b 100644 --- a/core/java/android/app/ApplicationPackageManager.java +++ b/core/java/android/app/ApplicationPackageManager.java @@ -90,6 +90,7 @@ import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; +import com.android.internal.ext.EuiccGoogleHooks; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Rect; @@ -140,6 +141,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.gmscompat.sysservice.GmcPackageManager; import com.android.internal.os.ApplicationSharedMemory; import com.android.internal.os.SomeArgs; import com.android.internal.pm.RoSystemFeatures; @@ -292,6 +294,7 @@ public PackageInfo getPackageInfoAsUser(String packageName, PackageInfoFlags fla if (pi == null) { throw new NameNotFoundException(packageName); } + GmcPackageManager.maybeAdjustPackageInfo(pi); return pi; } @@ -553,6 +556,9 @@ public ApplicationInfo getApplicationInfoAsUser(String packageName, ApplicationI if (ai == null) { throw new NameNotFoundException(packageName); } + + GmcPackageManager.maybeAdjustApplicationInfo(ai); + return maybeAdjustApplicationInfo(ai); } @@ -1801,12 +1807,17 @@ public ProviderInfo resolveContentProviderAsUser(String name, int flags, int use @Override public ProviderInfo resolveContentProviderAsUser(String name, ComponentInfoFlags flags, int userId) { + ProviderInfo res; try { - return mPM.resolveContentProvider(name, + res = mPM.resolveContentProvider(name, updateFlagsForComponent(flags.getValue(), userId, null), userId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } + if (res != null && res.applicationInfo != null && "com.google.android.gms.chimera".equals(name)) { + GmcPackageManager.maybeAdjustApplicationInfo(res.applicationInfo); + } + return res; } /** @hide **/ @@ -2284,8 +2295,8 @@ static void configurationChanged() { @UnsupportedAppUsage @RavenwoodKeep - protected ApplicationPackageManager(ContextImpl context, IPackageManager pm) { - mContext = context; + protected ApplicationPackageManager(Context context, IPackageManager pm) { + mContext = (ContextImpl) context; mPM = pm; mUseSystemFeaturesCache = isSystemFeaturesCacheAvailable(); } @@ -2529,7 +2540,13 @@ public CharSequence getText(String packageName, @StringRes int resid, } try { Resources r = getResourcesForApplication(appInfo); - text = r.getText(resid); + if (AppGlobals.getInitialPackageId() == android.ext.PackageId.G_EUICC_LPA) { + try (var s = new EuiccGoogleHooks.SuppressResourceFiltering()) { + text = r.getText(resid); + } + } else { + text = r.getText(resid); + } putCachedString(name, text); return text; } catch (NameNotFoundException e) { @@ -4330,6 +4347,27 @@ public TypedArray extractPackageItemInfoAttributes(PackageItemInfo info, String } } + @UnsupportedAppUsage + public PackageInfo findPackage(String packageName, long minVersion, Bundle validSignaturesSha256) { + try { + return mPM.findPackage(packageName, minVersion, validSignaturesSha256); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + } + + @UnsupportedAppUsage + public boolean updateListOfBusyPackages(boolean locked, List packageNames) { + // used for automatically removing packages after our process dies + android.os.Binder callerBinder = ActivityThread.currentActivityThread().getApplicationThread(); + + try { + return mPM.updateListOfBusyPackages(locked, packageNames, callerBinder); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + private static final class IntentActivitiesQuery { final Intent mIntent; final String mResolvedType; diff --git a/core/java/android/app/BroadcastOptions.java b/core/java/android/app/BroadcastOptions.java index 8bf95e0410c17..49304921fa43f 100644 --- a/core/java/android/app/BroadcastOptions.java +++ b/core/java/android/app/BroadcastOptions.java @@ -26,6 +26,7 @@ import android.annotation.SystemApi; import android.annotation.TestApi; import android.app.compat.CompatChanges; +import android.app.compat.gms.GmsCompat; import android.compat.annotation.ChangeId; import android.compat.annotation.Disabled; import android.compat.annotation.EnabledSince; @@ -586,6 +587,10 @@ public boolean isDontSendToRestrictedApps() { @SystemApi @RequiresPermission(android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND) public void setBackgroundActivityStartsAllowed(boolean allowBackgroundActivityStarts) { + if (GmsCompat.isEnabled()) { + return; + } + if (allowBackgroundActivityStarts) { mFlags |= FLAG_ALLOW_BACKGROUND_ACTIVITY_STARTS; } else { @@ -797,6 +802,10 @@ public boolean testRequireCompatChange(int uid) { @SystemApi @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RESPONSE_STATS) public void recordResponseEventWhileInBackground(@IntRange(from = 0) long id) { + if (GmsCompat.isEnabled()) { + return; + } + mIdForResponseEvent = id; } diff --git a/core/java/android/app/ContextImpl.java b/core/java/android/app/ContextImpl.java index 4c2db7d366ebb..f7315a4a0f1ad 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -21,6 +21,9 @@ import static android.os.StrictMode.vmIncorrectContextUseEnabled; import static android.permission.flags.Flags.shouldRegisterAttributionSource; import static android.view.WindowManager.LayoutParams.WindowType; +import static com.android.internal.app.ContentProviderRedirector.translateContentProviderAuthority; +import static com.android.internal.gmscompat.GmcDebug.maybeLogSendBroadcast; +import static com.android.internal.gmscompat.GmcDebug.maybeLogStartService; import android.annotation.CallbackExecutor; import android.annotation.IntDef; @@ -30,6 +33,7 @@ import android.annotation.SpecialUsers.CanBeCURRENT; import android.annotation.SuppressLint; import android.annotation.UiContext; +import android.app.compat.gms.GmsCompat; import android.companion.virtual.VirtualDeviceManager; import android.compat.annotation.UnsupportedAppUsage; import android.content.AttributionSource; @@ -113,6 +117,13 @@ import android.window.WindowTokenClientController; import com.android.internal.annotations.GuardedBy; +import com.android.internal.gmscompat.GmcDebug; +import com.android.internal.gmscompat.GmsCompatApp; +import com.android.internal.gmscompat.GmsCompatLib; +import com.android.internal.gmscompat.IGmsCompatLib; +import com.android.internal.gmscompat.sysservice.GmcPackageManager; +import com.android.internal.gmscompat.GmsHooks; +import com.android.internal.gmscompat.sysservice.GmcUserManager; import com.android.internal.util.Preconditions; import dalvik.system.BlockGuard; @@ -466,7 +477,9 @@ public PackageManager getPackageManager() { private PackageManager getPackageManagerInner() { final IPackageManager pm = ActivityThread.getPackageManager(); if (pm != null) { - return new ApplicationPackageManager(this, pm); + return GmsCompat.isEnabled() ? + new GmcPackageManager(this, pm) : + new ApplicationPackageManager(this, pm); } return null; } @@ -1322,6 +1335,9 @@ public void startIntentSender(IntentSender intent, Intent fillInIntent, @Override public void sendBroadcast(Intent intent) { + if (GmsCompat.isEnabled()) { + maybeLogSendBroadcast(intent, null, null, 0); + } warnIfCallingFromSystemProcess(); String resolvedType = intent.resolveTypeIfNeeded(getContentResolver()); try { @@ -1337,6 +1353,9 @@ public void sendBroadcast(Intent intent) { @Override public void sendBroadcast(Intent intent, String receiverPermission) { + if (GmsCompat.isEnabled()) { + maybeLogSendBroadcast(intent, receiverPermission, null, 0); + } warnIfCallingFromSystemProcess(); String resolvedType = intent.resolveTypeIfNeeded(getContentResolver()); String[] receiverPermissions = receiverPermission == null ? null @@ -1355,6 +1374,9 @@ public void sendBroadcast(Intent intent, String receiverPermission) { @Override public void sendBroadcastMultiplePermissions(Intent intent, String[] receiverPermissions) { + if (GmsCompat.isEnabled()) { + maybeLogSendBroadcast(intent, null, receiverPermissions, null, null, 0); + } warnIfCallingFromSystemProcess(); String resolvedType = intent.resolveTypeIfNeeded(getContentResolver()); try { @@ -1372,6 +1394,9 @@ public void sendBroadcastMultiplePermissions(Intent intent, String[] receiverPer @Override public void sendBroadcastMultiplePermissions(Intent intent, String[] receiverPermissions, Bundle options) { + if (GmsCompat.isEnabled()) { + maybeLogSendBroadcast(intent, null, receiverPermissions, options, null, 0); + } warnIfCallingFromSystemProcess(); String resolvedType = intent.resolveTypeIfNeeded(getContentResolver()); try { @@ -1389,6 +1414,9 @@ public void sendBroadcastMultiplePermissions(Intent intent, String[] receiverPer @Override public void sendBroadcastAsUserMultiplePermissions(Intent intent, UserHandle user, String[] receiverPermissions) { + if (GmsCompat.isEnabled()) { + maybeLogSendBroadcast(intent, null, receiverPermissions, null, null, 0); + } String resolvedType = intent.resolveTypeIfNeeded(getContentResolver()); try { intent.prepareToLeaveProcess(this); @@ -1405,6 +1433,9 @@ public void sendBroadcastAsUserMultiplePermissions(Intent intent, UserHandle use @Override public void sendBroadcastMultiplePermissions(Intent intent, String[] receiverPermissions, String[] excludedPermissions, String[] excludedPackages, BroadcastOptions options) { + if (GmsCompat.isEnabled()) { + maybeLogSendBroadcast(intent, null, receiverPermissions, null, options, 0); + } warnIfCallingFromSystemProcess(); String resolvedType = intent.resolveTypeIfNeeded(getContentResolver()); try { @@ -1421,6 +1452,11 @@ public void sendBroadcastMultiplePermissions(Intent intent, String[] receiverPer @Override public void sendBroadcast(Intent intent, String receiverPermission, Bundle options) { + if (GmsCompat.isEnabled()) { + maybeLogSendBroadcast(intent, receiverPermission, options, 0); + options = GmsHooks.filterBroadcastOptions(intent, options); + } + warnIfCallingFromSystemProcess(); String resolvedType = intent.resolveTypeIfNeeded(getContentResolver()); String[] receiverPermissions = receiverPermission == null ? null @@ -1449,6 +1485,9 @@ public void sendBroadcast(Intent intent, String receiverPermission, Bundle optio @Override public void sendBroadcast(Intent intent, String receiverPermission, int appOp) { + if (GmsCompat.isEnabled()) { + maybeLogSendBroadcast(intent, receiverPermission, null, appOp); + } warnIfCallingFromSystemProcess(); String resolvedType = intent.resolveTypeIfNeeded(getContentResolver()); String[] receiverPermissions = receiverPermission == null ? null @@ -1472,6 +1511,9 @@ public void sendOrderedBroadcast(Intent intent, String receiverPermission) { @Override public void sendOrderedBroadcast(Intent intent, String receiverPermission, Bundle options) { + if (GmsCompat.isEnabled()) { + maybeLogSendBroadcast(intent, receiverPermission, options, 0); + } warnIfCallingFromSystemProcess(); String resolvedType = intent.resolveTypeIfNeeded(getContentResolver()); String[] receiverPermissions = receiverPermission == null ? null @@ -1519,6 +1561,12 @@ void sendOrderedBroadcast(Intent intent, String receiverPermission, int appOp, BroadcastReceiver resultReceiver, Handler scheduler, int initialCode, String initialData, Bundle initialExtras, Bundle options) { + if (GmsCompat.isEnabled()) { + maybeLogSendBroadcast(intent, receiverPermission, null, options, null, appOp, + initialCode, initialData, initialExtras); + options = GmsHooks.filterBroadcastOptions(intent, options); + } + warnIfCallingFromSystemProcess(); IIntentReceiver rd = null; if (resultReceiver != null) { @@ -1554,6 +1602,11 @@ resultReceiver, getOuterContext(), scheduler, null, false) @Override public void sendBroadcastAsUser(Intent intent, UserHandle user) { + if (GmsCompat.isEnabled()) { + maybeLogSendBroadcast(intent, null, null, 0); + user = GmcUserManager.translateUserHandle(user); + } + String resolvedType = intent.resolveTypeIfNeeded(getContentResolver()); try { intent.prepareToLeaveProcess(this); @@ -1575,6 +1628,12 @@ public void sendBroadcastAsUser(Intent intent, UserHandle user, @Override public void sendBroadcastAsUser(Intent intent, UserHandle user, String receiverPermission, Bundle options) { + if (GmsCompat.isEnabled()) { + maybeLogSendBroadcast(intent, receiverPermission, options, 0); + options = GmsHooks.filterBroadcastOptions(intent, options); + user = GmcUserManager.translateUserHandle(user); + } + String resolvedType = intent.resolveTypeIfNeeded(getContentResolver()); String[] receiverPermissions = receiverPermission == null ? null : new String[] {receiverPermission}; @@ -1593,6 +1652,11 @@ public void sendBroadcastAsUser(Intent intent, UserHandle user, String receiverP @Override public void sendBroadcastAsUser(Intent intent, UserHandle user, String receiverPermission, int appOp) { + if (GmsCompat.isEnabled()) { + maybeLogSendBroadcast(intent, receiverPermission, null, null, null, appOp); + user = GmcUserManager.translateUserHandle(user); + } + String resolvedType = intent.resolveTypeIfNeeded(getContentResolver()); String[] receiverPermissions = receiverPermission == null ? null : new String[] {receiverPermission}; @@ -1646,6 +1710,12 @@ private void sendOrderedBroadcastAsUserMultiplePermissions(Intent intent, UserHa String[] receiverPermissions, String[] excludedPermissions, int appOp, Bundle options, BroadcastReceiver resultReceiver, Handler scheduler, int initialCode, String initialData, Bundle initialExtras) { + if (GmsCompat.isEnabled()) { + maybeLogSendBroadcast(intent, null, receiverPermissions, options, null, appOp, initialCode, initialData, initialExtras); + options = GmsHooks.filterBroadcastOptions(intent, options); + user = GmcUserManager.translateUserHandle(user); + } + IIntentReceiver rd = null; if (resultReceiver != null) { if (mPackageInfo != null) { @@ -2104,27 +2174,42 @@ public ComponentName startForegroundServiceAsUser(Intent service, UserHandle use private ComponentName startServiceCommon(Intent service, boolean requireForeground, UserHandle user) { + if (GmsCompat.isEnabled()) { + maybeLogStartService(service, requireForeground); + } // Keep this in sync with ActivityManagerLocal.startSdkSandboxService try { validateServiceIntent(service); service.prepareToLeaveProcess(this); - ComponentName cn = ActivityManager.getService().startService( - mMainThread.getApplicationThread(), service, - service.resolveTypeIfNeeded(getContentResolver()), requireForeground, - getOpPackageName(), getAttributionTag(), user.getIdentifier()); - if (cn != null) { - if (cn.getPackageName().equals("!")) { - throw new SecurityException( - "Not allowed to start service " + service - + " without permission " + cn.getClassName()); - } else if (cn.getPackageName().equals("!!")) { - throw new SecurityException( - "Unable to start service " + service - + ": " + cn.getClassName()); - } else if (cn.getPackageName().equals("?")) { - throw ServiceStartNotAllowedException.newInstance(requireForeground, - "Not allowed to start service " + service + ": " + cn.getClassName()); + ComponentName cn; + for (int i = 0;; ++i) { + cn = ActivityManager.getService().startService( + mMainThread.getApplicationThread(), service, + service.resolveTypeIfNeeded(getContentResolver()), requireForeground, + getOpPackageName(), getAttributionTag(), user.getIdentifier()); + if (cn != null) { + if (cn.getPackageName().equals("!")) { + throw new SecurityException( + "Not allowed to start service " + service + + " without permission " + cn.getClassName()); + } else if (cn.getPackageName().equals("!!")) { + throw new SecurityException( + "Unable to start service " + service + + ": " + cn.getClassName()); + } else if (cn.getPackageName().equals("?")) { + if (GmsCompat.isEnabled() && i == 0) { + Log.d("GmsCompat", "unable to start " + service + ", requireForeground: " + requireForeground); + String reason = "GmsCompat: " + service + ", requireForeground: " + requireForeground; + // foreground apps are always allowed to start services + GmsCompatApp.raisePackageToForeground(GmsCompat.appContext().getPackageName(), + 30_000, reason, android.os.PowerExemptionManager.REASON_OTHER); + continue; + } + throw ServiceStartNotAllowedException.newInstance(requireForeground, + "Not allowed to start service " + service + ": " + cn.getClassName()); + } } + break; } // If we started a foreground service in the same package, remember the stack trace. if (cn != null && requireForeground) { @@ -2146,6 +2231,9 @@ public boolean stopServiceAsUser(Intent service, UserHandle user) { } private boolean stopServiceCommon(Intent service, UserHandle user) { + if (GmsCompat.isEnabled()) { + GmcDebug.maybeLogStopService(service); + } // // Keep this in sync with ActivityManagerLocal.stopSdkSandboxService try { validateServiceIntent(service); @@ -2287,6 +2375,15 @@ private boolean bindServiceCommon(Intent service, ServiceConnection conn, long f if (handler != null && executor != null) { throw new IllegalArgumentException("Handler and Executor both supplied"); } + + IGmsCompatLib gmcLib = GmsCompatLib.get(); + if (gmcLib != null) { + ServiceConnection connOverride = gmcLib.maybeReplaceServiceConnection(service, flags, user, conn); + if (connOverride != null) { + conn = connOverride; + } + } + if (mPackageInfo != null) { if (executor != null) { sd = mPackageInfo.getServiceDispatcher(conn, getOuterContext(), executor, flags); @@ -2297,6 +2394,26 @@ private boolean bindServiceCommon(Intent service, ServiceConnection conn, long f throw new RuntimeException("Not supported in system context"); } validateServiceIntent(service); + + if (GmsCompat.isEnabled()) { + if (!GmsCompat.hasPermission(android.Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND)) { + flags &= ~BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS; + } + GmcDebug.maybeLogBindService(service, conn, flags, instanceName); + } + + String pkg = service.getPackage(); + if (pkg == null) { + ComponentName cn = service.getComponent(); + if (cn != null) { + pkg = cn.getPackageName(); + } + } + + if (pkg != null && GmsCompat.isGmsAppAndUnprivilegedProcess(pkg)) { + flags |= BIND_ALLOW_ACTIVITY_STARTS; + } + try { IBinder token = getActivityToken(); if (token == null && (flags&BIND_AUTO_CREATE) == 0 && mPackageInfo != null @@ -2375,6 +2492,9 @@ public void unbindService(ServiceConnection conn) { if (conn == null) { throw new IllegalArgumentException("connection is null"); } + if (GmsCompat.isEnabled()) { + GmcDebug.maybeLogUnbindService(conn); + } if (mPackageInfo != null) { IServiceConnection sd = mPackageInfo.forgetServiceDispatcher( getOuterContext(), conn); @@ -2432,6 +2552,12 @@ public boolean startInstrumentation(ComponentName className, @Override @RavenwoodKeep public Object getSystemService(String name) { + if (GmsCompat.isEnabled()) { + if (GmsHooks.isHiddenSystemService(name)) { + return null; + } + } + if (vmIncorrectContextUseEnabled()) { // Check incorrect Context usage. if (WINDOW_SERVICE.equals(name) && !isUiContext()) { @@ -2493,12 +2619,44 @@ private static boolean isSystemOrSystemUI(Context context) { Binder.getCallingUid()) == PERMISSION_GRANTED; } + private final static String TAG_SPOOF = "GmcContext"; + @Override @RavenwoodRedirect(comment = "Returns PERMISSION_DENIED by default on Ravenwood") public int checkPermission(String permission, int pid, int uid) { if (permission == null) { 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)) { + Log.v(TAG_SPOOF, "checking " + permission + " for pid " + pid + ", uid " + uid + + ", thread name " + Thread.currentThread().getName(), new Throwable()); + } + // The TS43 verifier calls this permission check for READ_PRIVILEGED_PHONE_STATE. If + // not granted, it does not run any of its verification code, since it needs the + // permission for TelephonyManager#getIccAuthentication to complete EAP-AKA + // challenge-response. However, getIccAuthentication also accepts the narrower + // USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER permission, and says READ_PRIVILEGED_PHONE_STATE + // is deprecated for it. + if (pid == Process.myPid() && uid == Process.myUid()) { + // Scope the READ_PRIVILEGED_PHONE_STATE spoofing to phone verification requests so + // that we don't cause crashes if GmsCore expects READ_PRIVILEGED_PHONE_STATE + // elsewhere + if (GmsCompat.isConstellationPhoneVerifyThread(Thread.currentThread())) { + String newPerm = android.Manifest.permission.USE_ICC_AUTH_WITH_DEVICE_IDENTIFIER; + Log.d(TAG_SPOOF, "checkPermission: checking " + newPerm + " instead of " + permission); + permission = newPerm; + } + } + } + if (mParams.isRenouncedPermission(permission) && pid == android.os.Process.myPid() && uid == android.os.Process.myUid()) { Log.v(TAG, "Treating renounced permission " + permission + " as denied"); @@ -2566,6 +2724,12 @@ public int checkSelfPermission(String permission) { return PERMISSION_DENIED; } + if (GmsCompat.isEnabled()) { + if (GmsHooks.shouldSpoofSelfPermissionCheck(permission)) { + return PERMISSION_GRANTED; + } + } + return checkPermission(permission, Process.myPid(), Process.myUid()); } @@ -2942,6 +3106,13 @@ public Context createContextForSdkInSandbox(ApplicationInfo sdkInfo, int flags) @Override public Context createPackageContext(String packageName, int flags) throws NameNotFoundException { + if (GmsCompat.isEnabled()) { + Context res = GmcPackageManager.maybeOverrideGsfPackageContext(packageName); + if (res != null) { + return res; + } + } + return createPackageContextAsUser(packageName, flags, mUser); } @@ -4054,6 +4225,7 @@ public ApplicationContentResolver(Context context, ActivityThread mainThread) { @UnsupportedAppUsage @RavenwoodRedirect protected IContentProvider acquireProvider(Context context, String auth) { + auth = translateContentProviderAuthority(auth); return mMainThread.acquireProvider(context, ContentProvider.getAuthorityWithoutUserId(auth), resolveUserIdFromAuthority(auth), true); @@ -4062,6 +4234,7 @@ protected IContentProvider acquireProvider(Context context, String auth) { @Override @RavenwoodRedirect protected IContentProvider acquireExistingProvider(Context context, String auth) { + auth = translateContentProviderAuthority(auth); return mMainThread.acquireExistingProvider(context, ContentProvider.getAuthorityWithoutUserId(auth), resolveUserIdFromAuthority(auth), true); @@ -4076,6 +4249,7 @@ public boolean releaseProvider(IContentProvider provider) { @Override @RavenwoodRedirect protected IContentProvider acquireUnstableProvider(Context c, String auth) { + auth = translateContentProviderAuthority(auth); return mMainThread.acquireProvider(c, ContentProvider.getAuthorityWithoutUserId(auth), resolveUserIdFromAuthority(auth), false); diff --git a/core/java/android/app/DownloadManager.java b/core/java/android/app/DownloadManager.java index c12f48c1c0c18..b4ee8b6c00409 100644 --- a/core/java/android/app/DownloadManager.java +++ b/core/java/android/app/DownloadManager.java @@ -24,6 +24,7 @@ import android.annotation.SystemApi; import android.annotation.SystemService; import android.annotation.TestApi; +import android.app.compat.gms.GmsCompat; import android.compat.annotation.UnsupportedAppUsage; import android.content.ClipDescription; import android.content.ContentProviderClient; @@ -34,6 +35,7 @@ import android.database.Cursor; import android.database.CursorWrapper; import android.database.DatabaseUtils; +import android.database.MatrixCursor; import android.net.ConnectivityManager; import android.net.NetworkPolicyManager; import android.net.Uri; @@ -53,6 +55,8 @@ import android.util.Pair; import android.webkit.MimeTypeMap; +import android.content.pm.SpecialRuntimePermAppUtils; + import java.io.File; import java.io.FileNotFoundException; import java.util.ArrayList; @@ -709,6 +713,13 @@ public Request setShowRunningNotification(boolean show) { * @return this object */ public Request setNotificationVisibility(int visibility) { + if (GmsCompat.isEnabled()) { + // requires the privileged DOWNLOAD_WITHOUT_NOTIFICATION permission + if (visibility == VISIBILITY_HIDDEN) { + return this; + } + } + mNotificationVisibility = visibility; return this; } @@ -1128,6 +1139,11 @@ public void onMediaStoreDownloadsDeleted(@NonNull LongSparseArray idToMi * future calls related to this download. Returns -1 if the operation fails. */ public long enqueue(Request request) { + if (SpecialRuntimePermAppUtils.isInternetCompatEnabled()) { + // invalid id (DownloadProvider uses SQLite and returns a row id) + return -1; + } + ContentValues values = request.toContentValues(mPackageName); Uri downloadUri = mResolver.insert(Downloads.Impl.CONTENT_URI, values); if (downloadUri == null) { @@ -1165,6 +1181,11 @@ public int markRowDeleted(long... ids) { * @return the number of downloads actually removed */ public int remove(long... ids) { + if (SpecialRuntimePermAppUtils.isInternetCompatEnabled()) { + // underlying provider is protected by the INTERNET permission + return 0; + } + return markRowDeleted(ids); } @@ -1180,6 +1201,11 @@ public Cursor query(Query query) { /** @hide */ public Cursor query(Query query, String[] projection) { + if (SpecialRuntimePermAppUtils.isInternetCompatEnabled()) { + // underlying provider is protected by the INTERNET permission + return new MatrixCursor(projection); + } + Cursor underlyingCursor = query.runQuery(mResolver, projection, mBaseUri); if (underlyingCursor == null) { return null; @@ -1558,6 +1584,11 @@ public long addCompletedDownload(String title, String description, throw new IllegalArgumentException(" invalid value for param: totalBytes"); } + if (SpecialRuntimePermAppUtils.isInternetCompatEnabled()) { + // underlying provider is protected by the INTERNET permission + return -1; + } + // if there is already an entry with the given path name in downloads.db, return its id Request request; if (uri != null) { diff --git a/core/java/android/app/IActivityManager.aidl b/core/java/android/app/IActivityManager.aidl index 8703ccbea576e..b40af0871a843 100644 --- a/core/java/android/app/IActivityManager.aidl +++ b/core/java/android/app/IActivityManager.aidl @@ -1051,6 +1051,9 @@ interface IActivityManager { @EnforcePermission("INTERACT_ACROSS_USERS_FULL") IBinder refreshIntentCreatorToken(in Intent intent); + oneway void showDynCodeLoadingNotification(int type, String pkgName, @nullable String path, + in List reportBody, String denialType); + /** * Reports ART optimization info. */ diff --git a/core/java/android/app/IApplicationThread.aidl b/core/java/android/app/IApplicationThread.aidl index c3cd2191292ca..9967cb52547af 100644 --- a/core/java/android/app/IApplicationThread.aidl +++ b/core/java/android/app/IApplicationThread.aidl @@ -83,7 +83,7 @@ oneway interface IApplicationThread { in CompatibilityInfo compatInfo, int processState); @UnsupportedAppUsage void scheduleStopService(IBinder token); - void bindApplication(in String packageName, in ApplicationInfo info, + void bindApplication(in Bundle extraArgs, in String packageName, in ApplicationInfo info, in String sdkSandboxClientAppVolumeUuid, in String sdkSandboxClientAppPackage, in boolean isSdkInSandbox, in ProviderInfoList providerList, in ComponentName testName, @@ -190,5 +190,8 @@ oneway interface IApplicationThread { void schedulePing(in RemoteCallback pong); void getExecutableMethodFileOffsets(in MethodDescriptor methodDescriptor, in IOffsetCallback resultCallback); + + void onGosPackageStateChanged(in android.content.pm.GosPackageState state); + void requestHandoffActivityData(in IBinder requestToken, in List activityTokens); } diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java index 7402ca5cfa101..44ed49b58c171 100644 --- a/core/java/android/app/Instrumentation.java +++ b/core/java/android/app/Instrumentation.java @@ -21,6 +21,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SpecialUsers.CanBeCURRENT; +import android.app.compat.gms.GmsCompat; import android.compat.annotation.UnsupportedAppUsage; import android.content.ActivityNotFoundException; import android.content.ComponentName; @@ -69,7 +70,9 @@ import android.view.Window; import android.view.WindowManagerGlobal; +import com.android.internal.app.StorageScopesAppHooks; import com.android.internal.content.ReferrerIntent; +import com.android.internal.gmscompat.GmsHooks; import java.io.File; import java.lang.annotation.Retention; @@ -1358,8 +1361,17 @@ public void sendTrackballEventSync(MotionEvent event) { public Application newApplication(ClassLoader cl, String className, Context context) throws InstantiationException, IllegalAccessException, ClassNotFoundException { - Application app = getFactory(context.getPackageName()) - .instantiateApplication(cl, className); + GmsCompat.maybeEnable(context); + className = com.android.internal.app.PairipHooks.maybeReplaceApplicationClassName( + context, cl, className); + final Application app; + if (GmsCompat.isInGmsCompatProcess()) { + // GmsCompat process should never run app's code + app = new Application(); + } else { + app = getFactory(context.getPackageName()) + .instantiateApplication(cl, className); + } app.attach(context); return app; } @@ -2014,12 +2026,18 @@ public ActivityResult execStartActivity( try { intent.migrateExtraStreamToClipData(who); intent.prepareToLeaveProcess(who); + StorageScopesAppHooks.maybeModifyActivityIntent(who, intent); int result = ActivityTaskManager.getService().startActivity(whoThread, who.getOpPackageName(), who.getAttributionTag(), intent, intent.resolveTypeIfNeeded(who.getContentResolver()), token, target != null ? target.mEmbeddedID : null, requestCode, 0, null, options); notifyStartActivityResult(result, options); checkStartActivityResult(result, intent); + + if (GmsCompat.isEnabled()) { + GmsHooks.onActivityStart(result, intent, requestCode, options); + } + } catch (RemoteException e) { throw new RuntimeException("Failure from system", e); } diff --git a/core/java/android/app/LoadedApk.java b/core/java/android/app/LoadedApk.java index cd8cd2cf9e0cb..13637ec9e16b4 100644 --- a/core/java/android/app/LoadedApk.java +++ b/core/java/android/app/LoadedApk.java @@ -23,6 +23,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.app.appfunctions.AppFunctionManager; +import android.app.compat.gms.GmsCompat; import android.compat.annotation.UnsupportedAppUsage; import android.content.BroadcastReceiver; import android.content.ComponentName; @@ -72,6 +73,7 @@ import com.android.internal.R; import com.android.internal.annotations.GuardedBy; +import com.android.internal.gmscompat.GmcDebug; import com.android.internal.os.DebugStore; import com.android.internal.os.RuntimeInit; import com.android.internal.util.ArrayUtils; @@ -2047,6 +2049,9 @@ public final Runnable getRunnable() { mContext.getAttributionSource()); setExtrasClassLoader(cl); receiver.setPendingResult(this); + if (GmsCompat.isEnabled()) { + GmcDebug.maybeLogReceiveBroadcast(receiver, intent, false); + } receiver.onReceive(mContext, intent); } catch (Exception e) { if (ActivityThread.DEBUG_BROADCAST) Slog.i(ActivityThread.TAG, @@ -2443,19 +2448,30 @@ public void doConnected(ComponentName name, IBinder service, IBinderSession sess // If there was an old service, it is now disconnected. if (old != null) { + if (GmsCompat.isEnabled()) { + GmcDebug.maybeLogServiceConnCallback("onServiceDisconnected_old", name); + } mConnection.onServiceDisconnected(name); } + + String op; if (dead) { + op = "onBindingDied"; mConnection.onBindingDied(name); } else { // If there is a new viable service, it is now connected. if (service != null) { + op = "onServiceConnected"; mConnection.onServiceConnected(name, service, session); } else { + op = "onNullBinding"; // The binding machinery worked, but the remote returned null from onBind(). mConnection.onNullBinding(name); } } + if (GmsCompat.isEnabled()) { + GmcDebug.maybeLogServiceConnCallback(op, name); + } } public void doDeath(ComponentName name, IBinder service) { @@ -2470,6 +2486,10 @@ public void doDeath(ComponentName name, IBinder service) { old.binder.unlinkToDeath(old.deathMonitor, 0); } + if (GmsCompat.isEnabled()) { + GmcDebug.maybeLogServiceConnCallback("onServiceDisconnected", name); + } + mConnection.onServiceDisconnected(name); } diff --git a/core/java/android/app/LocaleManager.java b/core/java/android/app/LocaleManager.java index e865af789b152..6abbac848e95e 100644 --- a/core/java/android/app/LocaleManager.java +++ b/core/java/android/app/LocaleManager.java @@ -24,11 +24,14 @@ import android.annotation.SystemService; import android.annotation.TestApi; import android.annotation.UserHandleAware; +import android.app.compat.gms.GmsCompat; import android.content.Context; import android.content.res.Configuration; import android.os.LocaleList; import android.os.RemoteException; +import com.android.internal.gmscompat.PlayStoreHooks; + /** * This class gives access to system locale services. These services allow applications to * control granular locale settings (such as per-app locales) or override their list of supported @@ -145,11 +148,19 @@ public LocaleList getApplicationLocales() { @UserHandleAware @NonNull public LocaleList getApplicationLocales(@NonNull String appPackageName) { + LocaleList res; try { - return mService.getApplicationLocales(appPackageName, mContext.getUserId()); + res = mService.getApplicationLocales(appPackageName, mContext.getUserId()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } + if (GmsCompat.isPlayStore()) { + LocaleList override = PlayStoreHooks.overrideApplicationLocales(res, appPackageName); + if (override != null) { + res = override; + } + } + return res; } /** diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java index 47d61adc2b3cd..6498a6eb71f92 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -130,6 +130,7 @@ import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.app.ContactScopes; import com.android.internal.graphics.ColorUtils; import com.android.internal.util.ArrayUtils; import com.android.internal.util.ContrastColorUtil; @@ -6200,6 +6201,11 @@ public Builder setCategory(String category) { * @deprecated use {@link #addPerson(Person)} */ public Builder addPerson(String uri) { + if (ContactScopes.isEnabled()) { + // See comment in Person.Builder#setUri() + return this; + } + addPerson(new Person.Builder().setUri(uri).build()); return this; } @@ -6226,6 +6232,13 @@ public Builder addPerson(String uri) { */ @NonNull public Builder addPerson(Person person) { + if (ContactScopes.isEnabled()) { + if (person.getName() == null && person.getKey() == null) { + // See comment in Person.Builder#setUri() + return this; + } + + } mPersonList.add(person); return this; } diff --git a/core/java/android/app/NotificationHistory.java b/core/java/android/app/NotificationHistory.java index bbaef65d4ebe3..ba3e13ff7e891 100644 --- a/core/java/android/app/NotificationHistory.java +++ b/core/java/android/app/NotificationHistory.java @@ -314,11 +314,15 @@ public void poolStringsFromNotifications() { mStringsToWrite.clear(); for (int i = 0; i < mNotificationsToWrite.size(); i++) { final HistoricalNotification notification = mNotificationsToWrite.get(i); - mStringsToWrite.add(notification.getPackage()); + if (!TextUtils.isEmpty(notification.getPackage())) { + mStringsToWrite.add(notification.getPackage()); + } if (!TextUtils.isEmpty(notification.getChannelName())) { mStringsToWrite.add(notification.getChannelName()); } - mStringsToWrite.add(notification.getChannelId()); + if (!TextUtils.isEmpty(notification.getChannelId())) { + mStringsToWrite.add(notification.getChannelId()); + } if (!TextUtils.isEmpty(notification.getConversationId())) { mStringsToWrite.add(notification.getConversationId()); } diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java index 5bce9766f8985..30e6cf239d0b3 100644 --- a/core/java/android/app/NotificationManager.java +++ b/core/java/android/app/NotificationManager.java @@ -22,6 +22,7 @@ import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.service.notification.Flags.splitSoundVibrationForNotificationBreakthrough; +import android.Manifest; import android.annotation.CallbackExecutor; import android.annotation.FlaggedApi; import android.annotation.IntDef; @@ -42,6 +43,7 @@ import android.annotation.WorkerThread; import android.app.Notification.Builder; import android.app.compat.CompatChanges; +import android.app.compat.gms.GmsCompat; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; import android.compat.annotation.UnsupportedAppUsage; @@ -51,6 +53,7 @@ import android.content.pm.PackageManager; import android.content.pm.ParceledListSlice; import android.content.pm.ShortcutInfo; +import android.ext.PackageId; import android.graphics.drawable.Icon; import android.net.Uri; import android.os.Binder; @@ -83,6 +86,7 @@ import android.util.proto.ProtoOutputStream; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.gmscompat.GmsCompatApp; import com.android.internal.notification.NotificationChannelGroupsHelper; import com.android.internal.util.ArrayUtils; @@ -918,6 +922,17 @@ public void notifyAsPackage(@NonNull String targetPackage, @Nullable String tag, public void notifyAsUser(@Nullable String tag, int id, Notification notification, @CanBeALL @CanBeCURRENT UserHandle user) { + if (GmsCompat.isEnabled()) { + if (!GmsCompat.hasPermission(Manifest.permission.POST_NOTIFICATIONS)) { + String pkg = GmsCompat.appContext().getPackageName(); + try { + GmsCompatApp.iGms2Gca().showMissingPostNotifsPermissionNotification(pkg); + } catch (RemoteException e) { + GmsCompatApp.callFailed(e); + } + } + } + INotificationManager service = service(); String pkg = mContext.getPackageName(); if (discardNotify(user, pkg, tag, id, notification)) { @@ -2152,6 +2167,12 @@ public boolean isNotificationPolicyAccessGranted() { * {@link android.provider.Settings#ACTION_NOTIFICATION_LISTENER_SETTINGS}. */ public boolean isNotificationListenerAccessGranted(ComponentName listener) { + if (GmsCompat.isAndroidAuto()) { + if (PackageId.ANDROID_AUTO_NAME.equals(listener.getPackageName())) { + return true; + } + } + INotificationManager service = service(); try { return service.isNotificationListenerAccessGranted(listener); diff --git a/core/java/android/app/PendingIntent.java b/core/java/android/app/PendingIntent.java index ab5284cc3c81b..8786ce77964d3 100644 --- a/core/java/android/app/PendingIntent.java +++ b/core/java/android/app/PendingIntent.java @@ -33,6 +33,7 @@ import android.annotation.SystemApi.Client; import android.annotation.TestApi; import android.app.ActivityManager.PendingIntentInfo; +import android.app.compat.gms.GmsCompat; import android.compat.Compatibility; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledAfter; @@ -63,6 +64,9 @@ import android.util.proto.ProtoOutputStream; import com.android.internal.annotations.GuardedBy; +import com.android.internal.gmscompat.GmsHooks; +import com.android.internal.gmscompat.GmsInfo; +import com.android.internal.gmscompat.util.GmcActivityUtils; import com.android.internal.os.IResultReceiver; import java.lang.annotation.Retention; @@ -439,8 +443,8 @@ static void removeOnMarshaledListener(OnMarshaledListener listener) { sOnMarshaledListener.get().remove(listener); } - private static void checkPendingIntent(int flags, @NonNull Intent intent, - @NonNull Context context, boolean isActivityResultType) { + private static int checkPendingIntent2(int flags, @NonNull Intent intent, + @NonNull Context context, boolean isActivityResultType) { final boolean isFlagImmutableSet = (flags & PendingIntent.FLAG_IMMUTABLE) != 0; final boolean isFlagMutableSet = (flags & PendingIntent.FLAG_MUTABLE) != 0; final String packageName = context.getPackageName(); @@ -458,7 +462,9 @@ private static void checkPendingIntent(int flags, @NonNull Intent intent, + " using FLAG_IMMUTABLE, only use FLAG_MUTABLE if some functionality" + " depends on the PendingIntent being mutable, e.g. if it needs to" + " be used with inline replies or bubbles."; - throw new IllegalArgumentException(msg); + + Log.e(TAG, msg); + return flags | PendingIntent.FLAG_IMMUTABLE; } // For apps with target SDK < U, warn that creation or retrieval of a mutable implicit @@ -475,6 +481,7 @@ private static void checkPendingIntent(int flags, @NonNull Intent intent, + " for security reasons."; Log.w(TAG, new StackTrace(msg)); } + return flags; } /** @hide */ @@ -575,7 +582,7 @@ public static PendingIntent getActivityAsUser(Context context, int requestCode, @CanBeCURRENT UserHandle user) { String packageName = context.getPackageName(); String resolvedType = intent.resolveTypeIfNeeded(context.getContentResolver()); - checkPendingIntent(flags, intent, context, /* isActivityResultType */ false); + flags = checkPendingIntent2(flags, intent, context, /* isActivityResultType */ false); try { intent.migrateExtraStreamToClipData(context); intent.prepareToLeaveProcess(context); @@ -709,7 +716,7 @@ public static PendingIntent getActivitiesAsUser(Context context, int requestCode intents[i].migrateExtraStreamToClipData(context); intents[i].prepareToLeaveProcess(context); resolvedTypes[i] = intents[i].resolveTypeIfNeeded(context.getContentResolver()); - checkPendingIntent(flags, intents[i], context, /* isActivityResultType */ false); + flags = checkPendingIntent2(flags, intents[i], context, /* isActivityResultType */ false); } try { IIntentSender target = @@ -762,7 +769,7 @@ public static PendingIntent getBroadcastAsUser(Context context, int requestCode, Intent intent, int flags, @CanBeALL @CanBeCURRENT UserHandle userHandle) { String packageName = context.getPackageName(); String resolvedType = intent.resolveTypeIfNeeded(context.getContentResolver()); - checkPendingIntent(flags, intent, context, /* isActivityResultType */ false); + flags = checkPendingIntent2(flags, intent, context, /* isActivityResultType */ false); try { intent.prepareToLeaveProcess(context); IIntentSender target = @@ -841,7 +848,7 @@ private static PendingIntent buildServicePendingIntent(Context context, int requ Intent intent, int flags, int serviceKind) { String packageName = context.getPackageName(); String resolvedType = intent.resolveTypeIfNeeded(context.getContentResolver()); - checkPendingIntent(flags, intent, context, /* isActivityResultType */ false); + flags = checkPendingIntent2(flags, intent, context, /* isActivityResultType */ false); try { intent.prepareToLeaveProcess(context); IIntentSender target = @@ -1083,6 +1090,22 @@ public void send(Context context, int code, @Nullable Intent intent, @Nullable OnFinished onFinished, @Nullable Handler handler, @Nullable String requiredPermission, @Nullable Bundle options) throws CanceledException { + if (GmsCompat.isEnabled()) { + if (options != null && intent != null && isBroadcast()) { + String targetPkg = getCreatorPackage(); + if (targetPkg != null) { + options = GmsHooks.filterBroadcastOptions(options, targetPkg); + } + } + } + + if (isActivity()) { + String pkg = getCreatorPackage(); + if (pkg != null && GmsCompat.isGmsAppAndUnprivilegedProcess(pkg)) { + options = GmcActivityUtils.allowActivityLaunchFromPendingIntent(options); + } + } + if (sendAndReturnResult(context, code, intent, onFinished, handler, requiredPermission, options) < 0) { throw new CanceledException(); diff --git a/core/java/android/app/Person.java b/core/java/android/app/Person.java index c7432c571e439..76d97b42c33b8 100644 --- a/core/java/android/app/Person.java +++ b/core/java/android/app/Person.java @@ -23,6 +23,8 @@ import android.os.Parcel; import android.os.Parcelable; +import com.android.internal.app.ContactScopes; + import java.util.Objects; import java.util.function.Consumer; @@ -258,6 +260,19 @@ public Person.Builder setIcon(@Nullable Icon icon) { */ @NonNull public Person.Builder setUri(@Nullable String uri) { + if (ContactScopes.isEnabled()) { + // The app doesn't have direct access to the contact's uri when Contact Scopes is enabled. + // + // The Person object can be attached to a notification via Notification.Builder#addPerson. + // Before showing a notification, system_server checks whether the calling app has + // permission to access all uris that are attached to that notification. system_server + // doesn't show notifications that fail this permission check. + // + // Before November 2024 security patches, Person uris were exempted from these checks: + // https://android.googlesource.com/platform/frameworks/base/+/3a240992e42dc2c7d1920a8207b00935d0558b0d + return this; + } + mUri = uri; return this; } diff --git a/core/java/android/app/Service.java b/core/java/android/app/Service.java index 9f09e1d8713e9..94bb499c77f74 100644 --- a/core/java/android/app/Service.java +++ b/core/java/android/app/Service.java @@ -20,10 +20,12 @@ import static android.os.Trace.TRACE_TAG_ACTIVITY_MANAGER; import static android.text.TextUtils.formatSimple; +import android.Manifest; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; +import android.app.compat.gms.GmsCompat; import android.compat.annotation.UnsupportedAppUsage; import android.content.ComponentCallbacks2; import android.content.ComponentName; @@ -857,6 +859,20 @@ public final void startForeground(int id, Notification notification) { */ public final void startForeground(int id, @NonNull Notification notification, @RequiresPermission @ForegroundServiceType int foregroundServiceType) { + if (GmsCompat.isEnabled()) { + if ((foregroundServiceType & ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE) != 0) { + if (!GmsCompat.hasPermission(Manifest.permission.RECORD_AUDIO)) { + foregroundServiceType &= ~ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE; + } + } + + if ((foregroundServiceType & ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION) != 0) { + if (!GmsCompat.hasPermission(Manifest.permission.ACCESS_COARSE_LOCATION)) { + foregroundServiceType &= ~ServiceInfo.FOREGROUND_SERVICE_TYPE_LOCATION; + } + } + } + try { final ComponentName comp = new ComponentName(this, mClassName); mActivityManager.setServiceForeground( @@ -865,6 +881,12 @@ public final void startForeground(int id, @NonNull Notification notification, clearStartForegroundServiceStackTrace(); logForegroundServiceStart(comp, foregroundServiceType); } catch (RemoteException ex) { + } catch (SecurityException e) { + if (GmsCompat.isEnabled()) { + Log.e(TAG, "fgsType: " + Integer.toHexString(foregroundServiceType), e); + return; + } + throw e; } } diff --git a/core/java/android/app/StorageScope.java b/core/java/android/app/StorageScope.java new file mode 100644 index 0000000000000..c9f1af1249541 --- /dev/null +++ b/core/java/android/app/StorageScope.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2022 GrapheneOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.annotation.SystemApi; +import android.content.Intent; +import android.content.pm.GosPackageState; +import android.util.Log; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.DataOutputStream; + +/** + * @hide + */ +@SystemApi +public final class StorageScope { + private static final String TAG = "StorageScope"; + + @NonNull + public final String path; + public final int flags; // note that flags are cast to short during serialization + + public static final int FLAG_ALLOW_WRITES = 1; + public static final int FLAG_IS_DIR = 1 << 1; + + public StorageScope(@NonNull String path, int flags) { + this.path = path; + this.flags = flags; + } + + @NonNull + public static Intent createConfigActivityIntent(@NonNull String targetPkg) { + Intent i = new Intent(Intent.ACTION_MAIN); + i.setClassName("com.android.permissioncontroller", + "com.android.permissioncontroller.sscopes.StorageScopesActivity"); + i.putExtra(Intent.EXTRA_PACKAGE_NAME, targetPkg); + return i; + } + + public static int maxArrayLength() { + // Should be less than Byte.MAX_VALUE (it is cast to byte during serialization). + // Note that the MediaProvider filtering based on StorageScopes is O(n), + // where n is the number of the StorageScopes + return 20; + } + + public boolean isWritable() { + return (flags & FLAG_ALLOW_WRITES) != 0; + } + + public boolean isDirectory() { + return (flags & FLAG_IS_DIR) != 0; + } + + public boolean isFile() { + return (flags & FLAG_IS_DIR) == 0; + } + + private static final int VERSION = 0; + + @Nullable + public static byte[] serializeArray(@NonNull @SuppressLint("ArrayReturn") StorageScope[] array) { + if (array.length == 0) { + return null; // special case to minimize the size of persistent state + } + + ByteArrayOutputStream bos = new ByteArrayOutputStream(1000); + DataOutputStream s = new DataOutputStream(bos); + try { + s.writeByte(VERSION); + + final int cnt = array.length; + if (cnt > maxArrayLength()) { + throw new IllegalStateException(); + } + s.writeByte(cnt); + + for (int i = 0; i < cnt; ++i) { + StorageScope scope = array[i]; + s.writeUTF(scope.path); + s.writeShort(scope.flags); + } + return bos.toByteArray(); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + @NonNull @SuppressLint("ArrayReturn") + public static StorageScope[] deserializeArray(@NonNull GosPackageState gosPackageState) { + byte[] ser = gosPackageState.storageScopes; + + if (ser == null) { + return new StorageScope[0]; + } + + DataInputStream s = new DataInputStream(new ByteArrayInputStream(ser)); + try { + final int version = s.readByte(); + if (version != StorageScope.VERSION) { + Log.e(TAG, "unexpected version " + version); + return new StorageScope[0]; + } + + int cnt = s.readByte(); + StorageScope[] arr = new StorageScope[cnt]; + for (int i = 0; i < cnt; ++i) { + String path = s.readUTF(); + short pathFlags = (short) s.readUnsignedShort(); + + arr[i] = new StorageScope(path, pathFlags); + } + + return arr; + } catch (Exception e) { + Log.e(TAG, "deserialization failed", e); + return new StorageScope[0]; + } + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof StorageScope)) { + return false; + } + StorageScope o = (StorageScope) obj; + return path.equals(o.path) && flags == o.flags; + } + + @Override + public int hashCode() { + return 31 * flags + path.hashCode(); + } + + public static final String MEDIA_PROVIDER_METHOD_INVALIDATE_MEDIA_PROVIDER_CACHE = "StorageScopes_invalidateCache"; + public static final String MEDIA_PROVIDER_METHOD_MEDIA_ID_TO_FILE_PATH = "StorageScopes_mediaIdToFilePath"; + + public static final String EXTERNAL_STORAGE_PROVIDER_METHOD_CONVERT_DOC_ID_TO_PATH = "StorageScopes_convertDocIdToPath"; +} diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index cc0e7b5cb7fa7..e407f3e4f5103 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -45,6 +45,7 @@ import android.app.appfunctions.IAppFunctionManager; import android.app.appsearch.AppSearchManagerFrameworkInitializer; import android.app.blob.BlobStoreManagerFrameworkInitializer; +import android.app.compat.gms.GmsCompat; import android.app.contentrestriction.ContentRestrictionManager; import android.app.contentrestriction.IContentRestrictionManager; import android.app.contentsafety.ContentSafetyManager; @@ -157,6 +158,7 @@ import android.location.ICountryDetector; import android.location.ILocationManager; import android.location.LocationManager; +import android.location.HookedLocationManager; import android.media.AudioDeviceVolumeManager; import android.media.AudioManager; import android.media.MediaFrameworkInitializer; @@ -327,6 +329,7 @@ import com.android.internal.app.ISoundTriggerService; import com.android.internal.app.IVoiceInteractionManagerService; import com.android.internal.appwidget.IAppWidgetService; +import com.android.internal.gmscompat.sysservice.GmcUserManager; import com.android.internal.graphics.fonts.IFontManager; import com.android.internal.net.INetworkWatchlistManager; import com.android.internal.os.IBinaryTransparencyService; @@ -666,7 +669,17 @@ public ColorDisplayManager createService(ContextImpl ctx) { @Override public InputMethodManager getService(ContextImpl ctx) { return InputMethodManager.forContext(ctx.getOuterContext()); - }}); + } + + @Override + public void clearCache(ContextImpl ctx) { + // clearCache() is expected to be called at an early phase of app process init, + // before InputMethodManager is fetched + if (!InputMethodManager.isInstanceCacheEmpty()) { + Log.e(TAG, "ignoring clearCache() for INPUT_METHOD_SERVICE"); + } + } + }); registerService(Context.TEXT_SERVICES_MANAGER_SERVICE, TextServicesManager.class, new CachedServiceFetcher() { @@ -699,7 +712,11 @@ public LayoutInflater createService(ContextImpl ctx) { @Override public LocationManager createService(ContextImpl ctx) throws ServiceNotFoundException { IBinder b = ServiceManager.getServiceOrThrow(Context.LOCATION_SERVICE); - return new LocationManager(ctx, ILocationManager.Stub.asInterface(b)); + if (HookedLocationManager.isEnabled()) { + return new HookedLocationManager(ctx, ILocationManager.Stub.asInterface(b)); + } else { + return new LocationManager(ctx, ILocationManager.Stub.asInterface(b)); + } }}); registerService(Context.NETWORK_POLICY_SERVICE, NetworkPolicyManager.class, @@ -1036,6 +1053,11 @@ public WindowManager createService(ContextImpl ctx) { public UserManager createService(ContextImpl ctx) throws ServiceNotFoundException { IBinder b = ServiceManager.getServiceOrThrow(Context.USER_SERVICE); IUserManager service = IUserManager.Stub.asInterface(b); + + if (GmsCompat.isEnabled()) { + return new GmcUserManager(ctx, service); + } + return new UserManager(ctx, service); }}); @@ -2256,6 +2278,15 @@ public static Object[] createServiceCache() { return new Object[sServiceCacheSize]; } + /** @hide */ + public static void clearServiceCache(Context ctx) { + for (ServiceFetcher fetcher : SYSTEM_SERVICE_FETCHERS.values()) { + if (ctx instanceof ContextImpl ctxImpl) { + fetcher.clearCache(ctxImpl); + } + } + } + @RavenwoodRedirect private static void onUnknownSystemServiceError(String name) { if (sEnableServiceNotFoundWtf) { @@ -2688,6 +2719,8 @@ public TServiceClass createService(ContextImpl ctx) { static abstract interface ServiceFetcher { T getService(ContextImpl ctx); + void clearCache(ContextImpl ctx); + /** * Should this service fetcher support being fetched via {@link #getSystemService(String)}, * without a Context? @@ -2823,6 +2856,15 @@ public final T getService(ContextImpl ctx) { return ret; } + @Override + public void clearCache(ContextImpl ctx) { + final Object[] cache = ctx.mServiceCache; + synchronized (cache) { + cache[mCacheIndex] = null; + ctx.mServiceInitializationStateArray[mCacheIndex] = ContextImpl.STATE_UNINITIALIZED; + } + } + public abstract T createService(ContextImpl ctx) throws ServiceNotFoundException; // Services that explicitly use a Context can never be fetched without one. @@ -2875,6 +2917,13 @@ public final T getService(ContextImpl ctx) { } } + @Override + public void clearCache(ContextImpl ctx) { + synchronized (StaticServiceFetcher.this) { + mCachedInstance = null; + } + } + protected boolean isServiceEnabled(ContextImpl ctx) { return true; } diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java index ed506edd1b567..2e1f13c8ab79d 100644 --- a/core/java/android/app/WallpaperManager.java +++ b/core/java/android/app/WallpaperManager.java @@ -44,6 +44,7 @@ import android.annotation.TestApi; import android.annotation.UiContext; import android.app.compat.CompatChanges; +import android.app.compat.gms.GmsCompat; import android.app.wallpaper.WallpaperDescription; import android.app.wallpaper.WallpaperInstance; import android.compat.annotation.ChangeId; @@ -101,6 +102,7 @@ import com.android.internal.R; import com.android.internal.annotations.Keep; +import com.android.internal.app.StorageScopesAppHooks; import libcore.io.IoUtils; @@ -798,6 +800,16 @@ public Bitmap peekWallpaperBitmap(Context context, boolean returnDefault, return getDefaultWallpaper(context, FLAG_SYSTEM); } + if (GmsCompat.isEnabled()) { + Log.d("GmsCompat", "", e); + return getDefaultWallpaper(context, FLAG_SYSTEM); + } + + if (StorageScopesAppHooks.isEnabled()) { + Log.d("StorageScopes", "returning default wallpaper"); + return getDefaultWallpaper(context, FLAG_SYSTEM); + } + if (context.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.O_MR1) { Log.w(TAG, "No permission to access wallpaper, suppressing" + " exception to avoid crashing legacy app."); @@ -2007,6 +2019,17 @@ private ParcelFileDescriptor getWallpaperFile(@SetWallpaperFlags int which, int + " wallpaper file to avoid crashing legacy app."); return getDefaultSystemWallpaperFile(); } + + if (GmsCompat.isEnabled()) { + Log.d("GmsCompat", "", e); + return getDefaultSystemWallpaperFile(); + } + + if (StorageScopesAppHooks.isEnabled()) { + Log.d("StorageScopes", "returning default wallpaper file"); + return getDefaultSystemWallpaperFile(); + } + if (mContext.getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.O_MR1) { Log.w(TAG, "No permission to access wallpaper, suppressing" + " exception to avoid crashing legacy app."); diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index a61f516bae47f..a78d59f2e3926 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -74,6 +74,7 @@ import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; +import static com.android.internal.widget.LockDomain.Primary; import android.Manifest.permission; import android.accounts.Account; @@ -172,6 +173,7 @@ import com.android.internal.os.Zygote; import com.android.internal.util.ArrayUtils; import com.android.internal.util.Preconditions; +import com.android.internal.widget.LockDomain; import com.android.org.conscrypt.TrustedCertificateStore; import java.io.ByteArrayInputStream; @@ -3945,7 +3947,7 @@ public void onInstallUpdateError( * Maximum supported password length. Kind-of arbitrary. * @hide */ - public static final int MAX_PASSWORD_LENGTH = 16; + public static final int MAX_PASSWORD_LENGTH = 128; /** * Service Action: Service implemented by a device owner or profile owner supervision app to @@ -6372,10 +6374,15 @@ public int getCurrentFailedPasswordAttempts() { */ @UnsupportedAppUsage public int getCurrentFailedPasswordAttempts(int userHandle) { + return getCurrentFailedPasswordAttempts(userHandle, Primary); + } + + /** @hide */ + public int getCurrentFailedPasswordAttempts(int userHandle, LockDomain lockDomain) { if (mService != null) { try { return mService.getCurrentFailedPasswordAttempts( - mContext.getPackageName(), userHandle, mParentInstance); + mContext.getPackageName(), lockDomain, userHandle, mParentInstance); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -9902,9 +9909,18 @@ public void getRemoveWarning(@Nullable ComponentName admin, RemoteCallback resul */ @RequiresFeature(PackageManager.FEATURE_SECURE_LOCK_SCREEN) public void reportPasswordChanged(PasswordMetrics metrics, @UserIdInt int userId) { + reportPasswordChanged(metrics, userId, Primary); + } + + /** + * @hide + */ + @RequiresFeature(PackageManager.FEATURE_SECURE_LOCK_SCREEN) + public void reportPasswordChanged(PasswordMetrics metrics, @UserIdInt int userId, + LockDomain lockDomain) { if (mService != null) { try { - mService.reportPasswordChanged(metrics, userId); + mService.reportPasswordChanged(metrics, userId, lockDomain); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -9917,9 +9933,18 @@ public void reportPasswordChanged(PasswordMetrics metrics, @UserIdInt int userId @UnsupportedAppUsage @RequiresFeature(PackageManager.FEATURE_SECURE_LOCK_SCREEN) public void reportFailedPasswordAttempt(int userHandle) { + reportFailedPasswordAttempt(userHandle, Primary); + } + + /** + * @hide + */ + @RequiresFeature(PackageManager.FEATURE_SECURE_LOCK_SCREEN) + public void reportFailedPasswordAttempt(int userHandle, LockDomain lockDomain) { if (mService != null) { try { - mService.reportFailedPasswordAttempt(userHandle, mParentInstance); + mService.reportFailedPasswordAttempt(userHandle, lockDomain, + mParentInstance); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -9932,9 +9957,17 @@ public void reportFailedPasswordAttempt(int userHandle) { @UnsupportedAppUsage @RequiresFeature(PackageManager.FEATURE_SECURE_LOCK_SCREEN) public void reportSuccessfulPasswordAttempt(int userHandle) { + reportSuccessfulPasswordAttempt(userHandle, Primary); + } + + /** + * @hide + */ + @RequiresFeature(PackageManager.FEATURE_SECURE_LOCK_SCREEN) + public void reportSuccessfulPasswordAttempt(int userHandle, LockDomain lockDomain) { if (mService != null) { try { - mService.reportSuccessfulPasswordAttempt(userHandle); + mService.reportSuccessfulPasswordAttempt(userHandle, lockDomain); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/core/java/android/app/admin/IDevicePolicyManager.aidl b/core/java/android/app/admin/IDevicePolicyManager.aidl index ec632c4792ba5..7b108ca794471 100644 --- a/core/java/android/app/admin/IDevicePolicyManager.aidl +++ b/core/java/android/app/admin/IDevicePolicyManager.aidl @@ -61,6 +61,7 @@ import com.android.internal.infra.AndroidFuture; import android.app.admin.DevicePolicyState; import android.app.admin.EnforcingAdmin; import android.app.admin.PolicyValueTransport; +import com.android.internal.widget.LockDomain; import java.util.List; @@ -111,7 +112,7 @@ interface IDevicePolicyManager { int getRequiredPasswordComplexity(String callerPackageName, boolean parent); int getAggregatedPasswordComplexityForUser(int userId, boolean deviceWideOnly); boolean isUsingUnifiedPassword(in ComponentName admin); - int getCurrentFailedPasswordAttempts(String callerPackageName, int userHandle, boolean parent); + int getCurrentFailedPasswordAttempts(String callerPackageName, in LockDomain lockDomain, int userHandle, boolean parent); int getProfileWithMinimumFailedPasswordsForWipe(int userHandle, boolean parent); void setMaximumFailedPasswordsForWipe( @@ -178,9 +179,9 @@ interface IDevicePolicyManager { void clearMultiuserDeviceManagement(in String deviceControllerPackageName); boolean hasGrantedPolicy(in ComponentName policyReceiver, int usesPolicy, int userHandle); - void reportPasswordChanged(in PasswordMetrics metrics, int userId); - void reportFailedPasswordAttempt(int userHandle, boolean parent); - void reportSuccessfulPasswordAttempt(int userHandle); + void reportPasswordChanged(in PasswordMetrics metrics, int userId, in LockDomain lockDomain); + void reportFailedPasswordAttempt(int userHandle, in LockDomain lockDomain, boolean parent); + void reportSuccessfulPasswordAttempt(int userHandle, in LockDomain lockDomain); void reportFailedBiometricAttempt(int userHandle); void reportSuccessfulBiometricAttempt(int userHandle); void reportKeyguardDismissed(int userHandle); diff --git a/core/java/android/app/compat/gms/AndroidAutoPackageFlag.java b/core/java/android/app/compat/gms/AndroidAutoPackageFlag.java new file mode 100644 index 0000000000000..d4e46d4db05a4 --- /dev/null +++ b/core/java/android/app/compat/gms/AndroidAutoPackageFlag.java @@ -0,0 +1,12 @@ +package android.app.compat.gms; + +import android.annotation.SystemApi; + +/** @hide */ +@SystemApi +public interface AndroidAutoPackageFlag { + int GRANT_PERMS_FOR_WIRED_ANDROID_AUTO = 0; + int GRANT_PERMS_FOR_WIRELESS_ANDROID_AUTO = 1; + int GRANT_AUDIO_ROUTING_PERM = 2; + int GRANT_PERMS_FOR_ANDROID_AUTO_PHONE_CALLS = 3; +} diff --git a/core/java/android/app/compat/gms/GmsCompat.java b/core/java/android/app/compat/gms/GmsCompat.java new file mode 100644 index 0000000000000..cdc408fca1cb4 --- /dev/null +++ b/core/java/android/app/compat/gms/GmsCompat.java @@ -0,0 +1,374 @@ +package android.app.compat.gms; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.app.ActivityThread; +import android.app.AppGlobals; +import android.app.Application; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.IPackageManager; +import android.content.pm.PackageManager; +import android.ext.AppInfoExt; +import android.ext.AppInfoExtFlag; +import android.ext.PackageId; +import android.os.Binder; +import android.os.Build; +import android.os.Process; +import android.os.RemoteException; +import android.os.SystemProperties; +import android.os.UserHandle; +import android.util.Log; + +import com.android.internal.gmscompat.GmsCompatLib; +import com.android.internal.gmscompat.GmsHooks; +import com.android.internal.gmscompat.GmsInfo; +import com.android.internal.gmscompat.fileservice.GmsCoreFileServerClientHooks; +import com.android.internal.util.ArrayUtils; + +/** + * This class provides helpers for GMS ("Google Mobile Services") compatibility. + *

+ * It allows the following apps to work as regular, unprivileged user apps: + *

    + *
  • GSF ("Google Services Framework")
  • + *
  • GmsCore ("Google Play services")
  • + *
  • Google Play Store
  • + *
  • GSA ("Google Search app", com.google.android.googlequicksearchbox)
  • + *
  • Apps that depend on the above
  • + *
+ *

+ * + * @hide + */ +@SystemApi +public final class GmsCompat { + private static final String TAG = "GmsCompat/Core"; + + private static boolean isGmsCompatEnabled; + private static int curPackageId; + private static boolean isGmsCore; + + private static boolean isEligibleForClientCompat; + private static boolean isInGmsCompatProcess; + + // Static only + private GmsCompat() { } + + public static boolean isEnabled() { + return isGmsCompatEnabled; + } + + /** @hide */ + public static boolean isDevBuild() { + return !Build.IS_USER; + } + + /** @hide */ + public static boolean isGmsCore() { + return curPackageId == PackageId.GMS_CORE; + } + + /** @hide */ + public static boolean isPlayStore() { + return curPackageId == PackageId.PLAY_STORE; + } + + /** @hide */ + public static boolean isGCarrierSettings() { + return curPackageId == PackageId.G_CARRIER_SETTINGS; + } + + public static boolean isAndroidAuto() { + return curPackageId == PackageId.ANDROID_AUTO; + } + + /** @hide */ + public static int getCurrentPackageId() { + return curPackageId; + } + + /** @hide */ + public static boolean isInGmsCompatProcess() { + return isInGmsCompatProcess; + } + + private static Context appContext; + + /** @hide */ + public static Context appContext() { + return appContext; + } + + /** + * Call from Instrumentation.newApplication() before Application class in instantiated to + * make sure init is completed in GMS processes before any of the app's code is executed. + * + * @hide + */ + public static void maybeEnable(Context appCtx) { + if (!Process.isApplicationUid(Process.myUid())) { + // note that isApplicationUid() returns false for processes of services that have + // 'android:isolatedProcess="true"' directive in AndroidManifest, which is fine, + // because they have no need for GmsCompat + return; + } + + appContext = appCtx; + ApplicationInfo appInfo = appCtx.getApplicationInfo(); + AppInfoExt appInfoExt = appInfo.ext(); + + curPackageId = appInfoExt.getPackageId(); + + if (isEnabledFor(appInfo)) { + String processName = Application.getProcessName(); + if (isGmsCompatProcess(processName)) { + isInGmsCompatProcess = true; + } else { + isGmsCompatEnabled = true; + GmsHooks.init(appCtx, appInfo.packageName, processName); + } + } + + isEligibleForClientCompat = !isGmsCore() && + appInfoExt.hasFlag(AppInfoExtFlag.HAS_GMSCORE_CLIENT_LIBRARY); + + if (isEligibleForClientCompat && GmsCompatLib.get() == null) { + GmsCompatLib.init(appCtx, Application.getProcessName()); + } + + if (GmsCompat.isClientOfGmsCore()) { + GmsCoreFileServerClientHooks.init(); + } + } + + public static boolean isEnabledFor(@NonNull ApplicationInfo app) { + return isEnabledFor(app.ext().getPackageId(), app.packageName, app.isPrivilegedApp()); + } + + /** @hide */ + public static boolean isEnabledFor(int packageId, String packageName, boolean isPrivileged) { + if (isDevBuild()) { + if (isTestPackage(packageName)) { + return true; + } + } + + if (isPrivileged) { + // don't enable GmsCompat for privileged GMS + return false; + } + + return switch (packageId) { + case + PackageId.GMS_CORE, + PackageId.PLAY_STORE, + PackageId.G_SEARCH_APP, + PackageId.ANDROID_AUTO, + PackageId.G_CARRIER_SETTINGS -> + true; + default -> + false; + }; + } + + /** @hide */ + public static boolean canBeEnabledFor(String pkgName) { + if (isDevBuild()) { + if (isTestPackage(pkgName)) { + return true; + } + } + + return switch (pkgName) { + case + PackageId.GMS_CORE_NAME, + PackageId.PLAY_STORE_NAME, + PackageId.G_SEARCH_APP_NAME, + PackageId.ANDROID_AUTO_NAME, + PackageId.G_CARRIER_SETTINGS_NAME -> + true; + default -> + false; + }; + + } + + /** @hide */ + public static boolean isGmsAppAndUnprivilegedProcess(@NonNull String packageName) { + if (!isEnabledFor(packageName, UserHandle.USER_CURRENT)) { + return false; + } + + Application a = AppGlobals.getInitialApplication(); + if (a == null) { + return false; + } + + ApplicationInfo ai = a.getApplicationInfo(); + if (ai == null) { + return false; + } + + return !ai.isPrivilegedApp(); + } + + public static boolean isEnabledFor(@NonNull String packageName, int userId) { + return isEnabledFor(packageName, userId, false); + } + + /** @hide */ + public static boolean isEnabledFor(@NonNull String packageName, int userId, boolean matchDisabledApp) { + if (isDevBuild()) { + if (isTestPackage(packageName, userId, matchDisabledApp)) { + return true; + } + } + + if (!canBeEnabledFor(packageName)) { + return false; + } + + if (userId == UserHandle.USER_CURRENT) { + userId = UserHandle.myUserId(); + } + + Context ctx = appContext; + if (ctx == null) { + ctx = AppGlobals.getInitialApplication(); + } + + if (ctx == null) { + throw new IllegalStateException("GmsCompat.isEnabledFor: Context is null"); + } + + PackageManager pm = ctx.getPackageManager(); + + ApplicationInfo appInfo; + long token = Binder.clearCallingIdentity(); + try { + appInfo = pm.getApplicationInfoAsUser(packageName, 0, userId); + } catch (PackageManager.NameNotFoundException e) { + return false; + } finally { + Binder.restoreCallingIdentity(token); + } + + return isEnabledFor(appInfo, matchDisabledApp); + } + + /** @hide */ + public static boolean isEnabledFor(@Nullable ApplicationInfo app, boolean matchDisabledApp) { + if (app == null) { + return false; + } + if (!app.enabled && !matchDisabledApp) { + return false; + } + return isEnabledFor(app); + } + + private static volatile boolean cachedIsClientOfGmsCore; + + /** @hide */ + public static boolean isClientOfGmsCore() { + return isClientOfGmsCore(null); + } + + /** @hide */ + public static boolean isClientOfGmsCore(@Nullable ApplicationInfo gmsCoreAppInfo) { + if (cachedIsClientOfGmsCore) { + return true; + } + + if (!isEligibleForClientCompat) { + return false; + } + + boolean res = (gmsCoreAppInfo != null) ? + isEnabledFor(gmsCoreAppInfo) : + isEnabledFor(GmsInfo.PACKAGE_GMS_CORE, appContext().getUserId()); + + cachedIsClientOfGmsCore = res; + return res; + } + + public static boolean hasPermission(@NonNull String perm) { + Context ctx = appContext(); + + if (GmsHooks.config().shouldSpoofSelfPermissionCheck(perm)) { + // result of checkSelfPermission() below would be spoofed, ask the PackageManager directly + IPackageManager pm = ActivityThread.getPackageManager(); + try { + return pm.checkPermission(perm, ctx.getPackageName(), ctx.getUserId()) + == PackageManager.PERMISSION_GRANTED; + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + return ctx.checkSelfPermission(perm) == PackageManager.PERMISSION_GRANTED; + } + + private static boolean isTestPackage(String packageName) { + String testPkgs = SystemProperties.get("persist.gmscompat_test_pkgs"); + return ArrayUtils.contains(testPkgs.split(","), packageName); + } + + /** @hide */ + public static boolean isTestPackage(String packageName, int userId, boolean matchDisabledApp) { + if (!isDevBuild()) { + return false; + } + if (!isTestPackage(packageName)) { + return false; + } + + IPackageManager pm = ActivityThread.getPackageManager(); + ApplicationInfo ai; + + long token = Binder.clearCallingIdentity(); + try { + ai = pm.getApplicationInfo(packageName, 0, userId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } finally { + Binder.restoreCallingIdentity(token); + } + + if (ai == null) { + return false; + } + return ai.enabled || matchDisabledApp; + } + + public static void catchOrRethrow(@NonNull SecurityException se) { + if (isEnabled()) { + Log.d("GmsCompat", "caught SecurityException", se); + return; + } + throw se; + } + + private static final String GMSCOMPAT_PROCESS_SUFFIX = ":gmscompat"; + + /** @hide */ + public static String gmsCompatProcessNameForPackage(String pkgName) { + return pkgName + GMSCOMPAT_PROCESS_SUFFIX; + } + + /** @hide */ + public static boolean isGmsCompatProcess(String processName) { + return processName.endsWith(GMSCOMPAT_PROCESS_SUFFIX); + } + + /** @hide */ + public static boolean isConstellationPhoneVerifyThread(Thread t) { + var tName = t.getName(); + // Full thread name: + // [com.google.android.gms.chimera.container.intentoperation.GmsIntentOperationChimeraService-Executor] processing com.google.android.gms.constellation.EventManager for action com.google.android.gms.constellation.eventmanager.SYNC_VERIFICATION + return tName != null && + tName.endsWith("processing com.google.android.gms.constellation.EventManager for action com.google.android.gms.constellation.eventmanager.SYNC_VERIFICATION"); + } +} diff --git a/core/java/android/app/compat/gms/GmsCorePackageFlag.java b/core/java/android/app/compat/gms/GmsCorePackageFlag.java new file mode 100644 index 0000000000000..e8ffcb3140ae6 --- /dev/null +++ b/core/java/android/app/compat/gms/GmsCorePackageFlag.java @@ -0,0 +1,6 @@ +package android.app.compat.gms; + +/** @hide */ +public interface GmsCorePackageFlag { + int GRANT_PERMS_FOR_ICC_AUTHENTICATION = 0; +} diff --git a/core/java/android/app/compat/gms/GmsModuleHooks.java b/core/java/android/app/compat/gms/GmsModuleHooks.java new file mode 100644 index 0000000000000..540e15962b759 --- /dev/null +++ b/core/java/android/app/compat/gms/GmsModuleHooks.java @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2022 GrapheneOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.app.compat.gms; + +import android.Manifest; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.annotation.SystemApi; +import android.app.Activity; +import android.bluetooth.BluetoothAdapter; +import android.content.Intent; +import android.os.Build; +import android.os.RemoteException; +import android.provider.Settings; +import android.util.Log; + +import com.android.internal.gmscompat.GmsCompatApp; +import com.android.internal.gmscompat.GmsHooks; +import com.android.internal.gmscompat.StubDef; +import com.android.internal.gmscompat.util.GmcActivityUtils; + +/** + * Hooks that are accessed from APEX modules. + * + * @hide + */ +@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) +public class GmsModuleHooks { + private static final String TAG = "GmsCompat/MHooks"; + + // BluetoothAdapter#enable() + // BluetoothAdapter#enableBLE() + @SuppressLint("AutoBoxing") + @Nullable + // returns null if hook wasn't applied, otherwise returns boxed return value for the original method + public static Boolean enableBluetoothAdapter() { + if (!GmsCompat.isGmsCore()) { + // others handle this themselves + return null; + } + + Activity activity = GmcActivityUtils.getMostRecentVisibleActivity(); + + if (activity != null) { + if (GmsCompat.hasPermission(Manifest.permission.BLUETOOTH_CONNECT)) { + activity.startActivity(new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)); + } else { + try { + GmsCompatApp.iGms2Gca().showGmsCoreMissingNearbyDevicesPermissionGeneric(); + } catch (RemoteException e) { + GmsCompatApp.callFailed(e); + } + } + } // else don't bother the user + + return Boolean.TRUE; + } + + // BluetoothAdapter#setScanMode() + public static void makeBluetoothAdapterDiscoverable() { + // don't use BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE intent here, this method is often + // called at a time when the user wouldn't expect to see it + Log.d(TAG, "makeBluetoothAdapterDiscoverable", new Throwable()); + } + + // com.android.modules.utils.SynchronousResultReceiver.Result#getValue() + public static boolean interceptSynchronousResultReceiverException(@NonNull RuntimeException origException) { + if (!(origException instanceof SecurityException)) { + return false; + } + + // origException contains service-side stack trace, need to obtain an app-side one + var stackTrace = new Throwable(); + StubDef stub = StubDef.find(stackTrace.getStackTrace(), GmsHooks.config(), StubDef.FIND_MODE_SynchronousResultReceiver); + + if (stub == null) { + return false; + } + + if (stub.type != StubDef.DEFAULT) { + Log.d(TAG, "interceptSynchronousResultReceiverException: unexpected stub type " + stub.type, stackTrace); + return false; + } + + if (GmsCompat.isDevBuild()) { + Log.i(TAG, "intercepted " + origException, stackTrace); + } + + return true; + } + + @Nullable + public static String deviceConfigGetProperty(@NonNull String namespace, @NonNull String name) { + return GmsCompatApp.getString(GmsCompatApp.deviceConfigNamespace(namespace), name); + } + + public static boolean deviceConfigSetProperty(@NonNull String namespace, @NonNull String name, @Nullable String value) { + return GmsCompatApp.putString(GmsCompatApp.deviceConfigNamespace(namespace), name, value); + } + + public static boolean deviceConfigSetProperties(@NonNull android.provider.DeviceConfig.Properties properties) { + return GmsCompatApp.setProperties(properties); + } + + private GmsModuleHooks() {} + + // NfcAdapter#enable() + public static void enableNfc() { + Activity activity = GmcActivityUtils.getMostRecentVisibleActivity(); + if (activity != null) { + activity.runOnUiThread(() -> { + Intent i = new Intent(Settings.ACTION_NFC_SETTINGS); + activity.startActivity(i); + }); + } + } +} diff --git a/core/java/android/app/compat/gms/GmsUtils.java b/core/java/android/app/compat/gms/GmsUtils.java new file mode 100644 index 0000000000000..e7fa4b9378ad2 --- /dev/null +++ b/core/java/android/app/compat/gms/GmsUtils.java @@ -0,0 +1,20 @@ +package android.app.compat.gms; + +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.content.Intent; +import android.ext.PackageId; +import android.net.Uri; + +/** @hide */ +@SystemApi +public class GmsUtils { + + public static @NonNull Intent createAppPlayStoreIntent(@NonNull String pkgName) { + var i = new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + pkgName)); + i.setPackage(PackageId.PLAY_STORE_NAME); + return i; + } + + private GmsUtils() {} +} diff --git a/core/java/android/app/smartspace/SmartspaceTarget.java b/core/java/android/app/smartspace/SmartspaceTarget.java index 2c1cc2ff33e11..541553a1a57ed 100644 --- a/core/java/android/app/smartspace/SmartspaceTarget.java +++ b/core/java/android/app/smartspace/SmartspaceTarget.java @@ -184,6 +184,10 @@ public final class SmartspaceTarget implements Parcelable { public static final int FEATURE_STEP_DATE = 39; // This represents a DATE. "STEP" is a typo. public static final int FEATURE_BLAZE_BUILD_PROGRESS = 40; public static final int FEATURE_EARTHQUAKE_OCCURRED = 41; + // Our current iteration of smartspace is intended to just be a port of the legacy + // KeyguardSlice, so merging it all as a single feature was the path of least resistance. It can + // easily be split later. + public static final int FEATURE_DATE_ALARM_ZEN_MEDIA = Integer.MAX_VALUE; /** * @hide @@ -230,7 +234,8 @@ public final class SmartspaceTarget implements Parcelable { FEATURE_EARTHQUAKE_ALERT, FEATURE_STEP_DATE, FEATURE_BLAZE_BUILD_PROGRESS, - FEATURE_EARTHQUAKE_OCCURRED + FEATURE_EARTHQUAKE_OCCURRED, + FEATURE_DATE_ALARM_ZEN_MEDIA }) @Retention(RetentionPolicy.SOURCE) public @interface FeatureType { diff --git a/core/java/android/app/supervision/SupervisionManager.java b/core/java/android/app/supervision/SupervisionManager.java index bec99a4e9e244..5c21b9842200a 100644 --- a/core/java/android/app/supervision/SupervisionManager.java +++ b/core/java/android/app/supervision/SupervisionManager.java @@ -35,6 +35,7 @@ import android.annotation.TestApi; import android.annotation.UserHandleAware; import android.annotation.UserIdInt; +import android.app.compat.gms.GmsCompat; import android.app.supervision.flags.Flags; import android.content.Context; import android.content.Intent; @@ -256,6 +257,9 @@ public boolean isSupervisionEnabledForUser(@UserIdInt int userId) { return mService.isSupervisionEnabledForUser(userId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); + } catch (SecurityException e) { + GmsCompat.catchOrRethrow(e); + return false; } } return false; diff --git a/core/java/android/app/usage/StorageStatsManager.java b/core/java/android/app/usage/StorageStatsManager.java index 9ae95a727e10d..27093f125218c 100644 --- a/core/java/android/app/usage/StorageStatsManager.java +++ b/core/java/android/app/usage/StorageStatsManager.java @@ -24,6 +24,7 @@ import android.annotation.SystemService; import android.annotation.TestApi; import android.annotation.WorkerThread; +import android.app.compat.gms.GmsCompat; import android.content.Context; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; @@ -35,6 +36,9 @@ import android.os.storage.CrateInfo; import android.os.storage.StorageManager; +import com.android.internal.gmscompat.GmsInfo; +import com.android.internal.gmscompat.PlayStoreHooks; + import java.io.File; import java.io.IOException; import java.util.Collection; @@ -207,6 +211,12 @@ public long getCacheBytes(String uuid) throws IOException { public @NonNull StorageStats queryStatsForPackage(@NonNull UUID storageUuid, @NonNull String packageName, @NonNull UserHandle user) throws PackageManager.NameNotFoundException, IOException { + if (GmsCompat.isPlayStore()) { + if (!GmsInfo.PACKAGE_PLAY_STORE.equals(packageName)) { + return PlayStoreHooks.queryStatsForPackage(packageName); + } + } + try { return mService.queryStatsForPackage(convert(storageUuid), packageName, user.getIdentifier(), mContext.getOpPackageName()); diff --git a/core/java/android/content/ContentProvider.java b/core/java/android/content/ContentProvider.java index 48b351f6e0aab..b4a6c3d599e06 100644 --- a/core/java/android/content/ContentProvider.java +++ b/core/java/android/content/ContentProvider.java @@ -32,6 +32,7 @@ import android.annotation.SystemApi; import android.annotation.TestApi; import android.app.AppOpsManager; +import android.app.compat.gms.GmsCompat; import android.compat.annotation.UnsupportedAppUsage; import android.content.pm.PackageManager; import android.content.pm.PathPermission; @@ -967,8 +968,35 @@ private int checkPermission(String permission, if (Binder.getCallingPid() == Process.myPid()) { return PermissionChecker.PERMISSION_GRANTED; } + AttributionSource selfAttributionSource = getContext().getAttributionSource(); + if (GmsCompat.isAndroidAuto()) { + if (Manifest.permission.RECORD_AUDIO.equals(permission)) { + int deviceId = selfAttributionSource.getDeviceId(); + if (deviceId != Context.DEVICE_ID_DEFAULT) { + // RECORD_AUDIO is a device-aware permission, but there's currently no way to + // grant it in the UI for non-default deviceId. This leads to permission denial + // when Google app tries to access car microphone data provider in Android Auto. + // + // This issue doesn't affect fully privileged Android Auto because it has the + // privileged UPDATE_APP_OPS_STATS permissions, which makes the OS use a + // different codepath for permission checks that use a chained AttributionSource. + selfAttributionSource = new AttributionSource( + selfAttributionSource.getUid(), + selfAttributionSource.getPid(), + selfAttributionSource.getPackageName(), + selfAttributionSource.getAttributionTag(), + selfAttributionSource.getToken(), + selfAttributionSource.getRenouncedPermissions().toArray(new String[0]), + Context.DEVICE_ID_DEFAULT, + selfAttributionSource.getNext() + ); + Log.d("GmsCompat", "replaced AttributionSource deviceId (" + + deviceId + ") with default deviceId in " + getClass().getName()); + } + } + } return PermissionChecker.checkPermissionForDataDeliveryFromDataSource(getContext(), - permission, -1, new AttributionSource(getContext().getAttributionSource(), + permission, -1, new AttributionSource(selfAttributionSource, attributionSource), /*message*/ null); } @@ -2692,7 +2720,9 @@ private void attachInfo(Context context, ProviderInfo info, boolean testing) { setTransportLoggingEnabled(Log.isLoggable(getClass().getSimpleName(), Log.VERBOSE)); } - ContentProvider.this.onCreate(); + if (!com.android.internal.app.PairipHooks.shouldSkipOnCreate(ContentProvider.this)) { + ContentProvider.this.onCreate(); + } } } @@ -2804,7 +2834,8 @@ public void dump(FileDescriptor fd, PrintWriter writer, String[] args) { writer.println("nothing to dump"); } - private void validateIncomingAuthority(String authority) throws SecurityException { + /** @hide */ + protected void validateIncomingAuthority(String authority) throws SecurityException { if (!matchesOurAuthorities(getAuthorityWithoutUserId(authority))) { String message = "The authority " + authority + " does not match the one of the " + "contentProvider: "; diff --git a/core/java/android/content/ContentProviderClient.java b/core/java/android/content/ContentProviderClient.java index 8838c03043359..b4afcae010048 100644 --- a/core/java/android/content/ContentProviderClient.java +++ b/core/java/android/content/ContentProviderClient.java @@ -25,6 +25,7 @@ import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.annotation.SystemApi; +import android.app.compat.gms.GmsCompat; import android.compat.annotation.UnsupportedAppUsage; import android.content.res.AssetFileDescriptor; import android.database.CrossProcessCursorWrapper; @@ -46,6 +47,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.gmscompat.GmsHooks; import dalvik.system.CloseGuard; @@ -368,6 +370,13 @@ private CancellationSignal maybeWrapNotRespondingSignal(CancellationSignal calle final Cursor cursor = mContentProvider.query( mAttributionSource, uri, projection, queryArgs, remoteCancellationSignal); + if (GmsCompat.isEnabled()) { + Cursor override = GmsHooks.maybeModifyQueryResult(uri, projection, queryArgs, cursor); + if (override != null) { + // original cursor is closed if it wasn't null + return override; + } + } if (cursor == null) { return null; } diff --git a/core/java/android/content/ContentResolver.java b/core/java/android/content/ContentResolver.java index 93900e24a3903..7c20321ad0e78 100644 --- a/core/java/android/content/ContentResolver.java +++ b/core/java/android/content/ContentResolver.java @@ -33,6 +33,7 @@ import android.app.ActivityThread; import android.app.AppGlobals; import android.app.UriGrantsManager; +import android.app.compat.gms.GmsCompat; import android.compat.annotation.UnsupportedAppUsage; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; @@ -79,6 +80,11 @@ import android.util.SparseArray; import com.android.internal.annotations.GuardedBy; +import com.android.internal.app.ContentProviderRedirector; +import com.android.internal.gmscompat.GmsCompatApp; +import com.android.internal.gmscompat.GmsHooks; +import com.android.internal.gmscompat.PlayStoreHooks; +import com.android.internal.gmscompat.dynamite.GmsDynamiteClientHooks; import com.android.internal.util.MimeIconUtils; import dalvik.system.CloseGuard; @@ -1281,6 +1287,10 @@ protected Uri getResultFromBundle(Bundle result) { queryArgs, remoteCancellationSignal); } if (qCursor == null) { + if (GmsCompat.isEnabled()) { + return GmsHooks.maybeModifyQueryResult(uri, projection, queryArgs, null); + } + return null; } @@ -1295,12 +1305,27 @@ protected Uri getResultFromBundle(Bundle result) { final CursorWrapperInner wrapper = new CursorWrapperInner(qCursor, provider); stableProvider = null; qCursor = null; + + if (GmsCompat.isEnabled()) { + Cursor modified = GmsHooks.maybeModifyQueryResult(uri, projection, queryArgs, wrapper); + if (modified != null) { + return modified; + } + } + return wrapper; } catch (RemoteException e) { // Arbitrary and not worth documenting, as Activity // Manager will kill this process shortly anyway. return null; - } finally { + } catch (SecurityException se) { + if (GmsCompat.isEnabled()) { + Log.d("GmsCompat", "", se); + return null; + } + throw se; + } + finally { if (qCursor != null) { qCursor.close(); } @@ -2266,6 +2291,9 @@ public OpenResourceIdResult getResourceId(Uri uri) throws FileNotFoundException public final @Nullable Uri insert(@RequiresPermission.Write @NonNull Uri url, @Nullable ContentValues values, @Nullable Bundle extras) { Objects.requireNonNull(url, "url"); + if (GmsCompat.isEnabled()) { + GmsHooks.filterContentValues(url, values); + } try { if (mWrapped != null) return mWrapped.insert(url, values, extras); @@ -2572,6 +2600,8 @@ public final IContentProvider acquireProvider(Uri uri) { } final String auth = uri.getAuthority(); if (auth != null) { + GmsDynamiteClientHooks.maybeInit(auth); + return acquireProvider(mContext, auth); } return null; @@ -2624,7 +2654,13 @@ public final IContentProvider acquireUnstableProvider(Uri uri) { } String auth = uri.getAuthority(); if (auth != null) { - return acquireUnstableProvider(mContext, uri.getAuthority()); + GmsDynamiteClientHooks.maybeInit(auth); + + try { + return acquireUnstableProvider(mContext, uri.getAuthority()); + } catch (SecurityException e) { + GmsCompat.catchOrRethrow(e); + } } return null; } @@ -2655,7 +2691,18 @@ public final IContentProvider acquireUnstableProvider(String name) { @RavenwoodKeep public final @Nullable ContentProviderClient acquireContentProviderClient(@NonNull Uri uri) { Objects.requireNonNull(uri, "uri"); - IContentProvider provider = acquireProvider(uri); + + IContentProvider provider; + try { + provider = acquireProvider(uri); + } catch (SecurityException se) { + if (GmsCompat.isEnabled()) { + Log.d("GmsCompat", "uri: " + uri, se); + return null; + } + throw se; + } + if (provider != null) { return new ContentProviderClient(this, provider, uri.getAuthority(), true); } @@ -2814,11 +2861,25 @@ public final void registerContentObserverAsUser(@NonNull Uri uri, @UnsupportedAppUsage public final void registerContentObserver(Uri uri, boolean notifyForDescendants, ContentObserver observer, @CanBeALL @CanBeCURRENT @UserIdInt int userHandle) { + if (ContentProviderRedirector.shouldSkipRegisterContentObserver(uri, notifyForDescendants, + observer, userHandle)) { + return; + } + if (GmsCompat.isEnabled()) { + if (GmsCompatApp.registerObserver(uri, observer)) { + return; + } + } try { getContentService().registerContentObserver(uri, notifyForDescendants, observer.getContentObserver(), userHandle, mTargetSdkVersion); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); + } catch (SecurityException se) { + if ("com.google.android.gsf.gservices".equals(uri.getAuthority())) { + return; + } + throw se; } } @@ -2830,6 +2891,17 @@ public final void registerContentObserver(Uri uri, boolean notifyForDescendants, */ public final void unregisterContentObserver(@NonNull ContentObserver observer) { Objects.requireNonNull(observer, "observer"); + + if (ContentProviderRedirector.shouldSkipUnregisterContentObserver(observer)) { + return; + } + + if (GmsCompat.isEnabled()) { + if (GmsCompatApp.unregisterObserver(observer)) { + return; + } + } + try { IContentObserver contentObserver = observer.releaseContentObserver(); if (contentObserver != null) { @@ -2924,6 +2996,11 @@ public void notifyChange(@NonNull Uri uri, @Nullable ContentObserver observer, public void notifyChange(@NonNull Uri uri, @Nullable ContentObserver observer, @NotifyFlags int flags) { Objects.requireNonNull(uri, "uri"); + + if (ContentProviderRedirector.shouldSkipNotifyChange(uri, observer, flags)) { + return; + } + notifyChange( ContentProvider.getUriWithoutUserId(uri), observer, @@ -2971,6 +3048,10 @@ public void notifyChange(@NonNull Collection uris, @Nullable ContentObserve // Cluster based on user ID final SparseArray> clusteredByUser = new SparseArray<>(); for (Uri uri : uris) { + if (ContentProviderRedirector.shouldSkipNotifyChange(uri, observer, flags)) { + continue; + } + final int userId = ContentProvider.getUserIdFromUri(uri, mContext.getUserId()); ArrayList list = clusteredByUser.get(userId); if (list == null) { @@ -4049,6 +4130,11 @@ public String getPackageName() { @UnsupportedAppUsage private final Context mContext; + /** @hide */ + public Context getContext() { + return mContext; + } + @Deprecated @UnsupportedAppUsage final String mPackageName; diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index f0bd86d90e1b3..438a277e1d01e 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -8205,7 +8205,7 @@ public abstract Context createContextForSplit(String splitName) * @hide */ @NonNull - @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + @SystemApi @TestApi @RavenwoodSupported(type = SupportType.SUBCLASS, subclass = "ContextImpl") public @CanBeALL @CanBeCURRENT UserHandle getUser() { diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 0376980afcc05..5ca94259fe348 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -53,6 +53,7 @@ import android.app.ActivityThread; import android.app.AppGlobals; import android.app.StatusBarManager; +import android.app.compat.gms.GmsCompat; import android.bluetooth.BluetoothDevice; import android.compat.annotation.UnsupportedAppUsage; import android.content.pm.ActivityInfo; @@ -90,6 +91,7 @@ import android.provider.MediaStore; import android.provider.OpenableColumns; import android.service.chooser.AdditionalContentContract; +import android.provider.Settings; import android.service.chooser.ChooserAction; import android.service.chooser.ChooserResult; import android.telecom.PhoneAccount; @@ -10362,6 +10364,14 @@ public boolean isExcludingStopped() { * @see #resolveActivityInfo */ public ComponentName resolveActivity(@NonNull PackageManager pm) { + if (GmsCompat.isEnabled()) { + if (Settings.ACTION_SETTINGS_EMBED_DEEP_LINK_ACTIVITY.equals(getAction())) { + if (!GmsCompat.hasPermission(Manifest.permission.LAUNCH_MULTI_PANE_SETTINGS_DEEP_LINK)) { + return null; + } + } + } + if (mComponent != null) { return mComponent; } diff --git a/core/java/android/content/IntentFilter.java b/core/java/android/content/IntentFilter.java index e187d86399e3d..5912e34af3e2d 100644 --- a/core/java/android/content/IntentFilter.java +++ b/core/java/android/content/IntentFilter.java @@ -816,6 +816,15 @@ public final void addAction(String action) { mActions.add(action.intern()); } + /** @hide */ + public final boolean replaceAction(String action, String replacement) { + if (mActions.remove(action)) { + mActions.add(replacement); + return true; + } + return false; + } + /** * Return the number of actions in the filter. */ diff --git a/core/java/android/content/pm/AppPermissionUtils.java b/core/java/android/content/pm/AppPermissionUtils.java new file mode 100644 index 0000000000000..6d3a18f6359eb --- /dev/null +++ b/core/java/android/content/pm/AppPermissionUtils.java @@ -0,0 +1,115 @@ +/* + * Copyright (C) 2022 GrapheneOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content.pm; + +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.app.compat.gms.GmsCompat; + +import com.android.internal.app.ContactScopes; +import com.android.internal.app.StorageScopesAppHooks; +import com.android.internal.gmscompat.GmsHooks; + +/** @hide */ +@SystemApi +public class AppPermissionUtils { + + // If the list of spoofed permissions changes at runtime, make sure to invalidate the permission + // check cache, it's keyed on the PermissionManager.CACHE_KEY_PACKAGE_INFO_CACHE system property. + // Updates of GosPackageState invalidate this cache automatically. + // + // android.permission.PermissionManager#checkPermissionUncached + /** @hide */ + public static boolean shouldSpoofSelfCheck(String permName) { + if (StorageScopesAppHooks.shouldSpoofSelfPermissionCheck(permName)) { + return true; + } + + if (SrtPermissions.shouldSpoofSelfCheck(permName)) { + return true; + } + + if (ContactScopes.shouldSpoofSelfPermissionCheck(permName)) { + return true; + } + + if (GmsCompat.isEnabled()) { + if (GmsHooks.config().shouldSpoofSelfPermissionCheck(permName)) { + return true; + } + } + + return false; + } + + // android.app.AppOpsManager#checkOpNoThrow + // android.app.AppOpsManager#noteOpNoThrow + // android.app.AppOpsManager#noteProxyOpNoThrow + // android.app.AppOpsManager#unsafeCheckOpRawNoThrow + /** @hide */ + public static boolean shouldSpoofSelfAppOpCheck(int op) { + if (StorageScopesAppHooks.shouldSpoofSelfAppOpCheck(op)) { + return true; + } + + if (ContactScopes.shouldSpoofSelfAppOpCheck(op)) { + return true; + } + + return false; + } + + public static boolean shouldSkipPermissionRequestDialog(@NonNull GosPackageState ps, @NonNull String perm) { + // Don't check whether the app actually declared this permission: + // app can request a permission that isn't declared in its AndroidManifest and if that + // permission is split into multiple permissions (based on app's targetSdk), and at least + // one of of those split permissions is present in manifest, then permission prompt would be + // shown anyway. + return getSpoofablePermissionDflag(ps, perm, true) != 0; + } + + // Controls spoofing of Activity#onRequestPermissionsResult() callback + public static boolean shouldSpoofPermissionRequestResult(@NonNull GosPackageState ps, @NonNull String perm) { + int dflag = getSpoofablePermissionDflag(ps, perm, false); + return dflag != 0 && ps.hasDerivedFlag(dflag); + } + + private static int getSpoofablePermissionDflag(GosPackageState ps, String perm, boolean forRequestDialog) { + if (ps.hasFlag(GosPackageStateFlag.STORAGE_SCOPES_ENABLED)) { + int permDflag = StorageScopesAppHooks.getSpoofablePermissionDflag(perm); + if (permDflag != 0) { + if (!forRequestDialog) { + if (StorageScopesAppHooks.shouldSkipPermissionCheckSpoof(ps.derivedFlags, permDflag)) { + return 0; + } + } + return permDflag; + } + } + + if (ps.hasFlag(GosPackageStateFlag.CONTACT_SCOPES_ENABLED)) { + int permDflag = ContactScopes.getSpoofablePermissionDflag(perm); + if (permDflag != 0) { + return permDflag; + } + } + + return 0; + } + + private AppPermissionUtils() {} +} diff --git a/core/java/android/content/pm/ApplicationInfo.java b/core/java/android/content/pm/ApplicationInfo.java index 878a910c87c96..57af3320ff795 100644 --- a/core/java/android/content/pm/ApplicationInfo.java +++ b/core/java/android/content/pm/ApplicationInfo.java @@ -32,6 +32,7 @@ import android.content.Context; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources; +import android.ext.AppInfoExt; import android.graphics.drawable.Drawable; import android.os.Build; import android.os.Environment; @@ -2137,6 +2138,7 @@ public ApplicationInfo() { public ApplicationInfo(ApplicationInfo orig) { super(orig); + ext = orig.ext; taskAffinity = orig.taskAffinity; permission = orig.permission; mKnownActivityEmbeddingCerts = orig.mKnownActivityEmbeddingCerts; @@ -2236,6 +2238,7 @@ public void writeToParcel(Parcel dest, int parcelableFlags) { } final int preWriteSize = dest.dataSize(); super.writeToParcel(dest, parcelableFlags); + ext.writeToParcel(dest, parcelableFlags); dest.writeString8(taskAffinity); dest.writeString8(permission); dest.writeString8(processName); @@ -2367,6 +2370,7 @@ public ApplicationInfo[] newArray(int size) { @SuppressWarnings("unchecked") private ApplicationInfo(Parcel source) { super(source); + ext = AppInfoExt.CREATOR.createFromParcel(source); taskAffinity = source.readString8(); permission = source.readString8(); processName = source.readString8(); @@ -3204,4 +3208,45 @@ public boolean shouldBackupAgentRunInPccProcess() { public int getBackupAgentUid() { return shouldBackupAgentRunInPccProcess() ? pccUid : uid; } + + private AppInfoExt ext = AppInfoExt.DEFAULT; + + /** @hide */ + public void setExt(AppInfoExt ext) { + this.ext = ext; + } + + /** @hide */ + @SystemApi + public @NonNull AppInfoExt ext() { + return ext; + } + + private static volatile Boolean hasPlayStoreSourceStamp; + + /** @hide */ + public boolean hasPlayStoreSourceStamp() { + Boolean cache = hasPlayStoreSourceStamp; + if (cache != null) { + return cache.booleanValue(); + } + + var apkPaths = new ArrayList(); + apkPaths.add(Objects.requireNonNull(sourceDir)); + String[] splits = splitSourceDirs; + if (splits != null) { + for (String splitPath : splits) { + apkPaths.add(Objects.requireNonNull(splitPath)); + } + } + byte[] playStoreSourceStampCertDigest = java.util.HexFormat.of().parseHex( + "3257d599a49d2c961a471ca9843f59d341a405884583fc087df4237b733bbd6d"); + boolean result = android.util.apk.SourceStampVerifier + .verify(apkPaths, /* requiredSourceStamp */ playStoreSourceStampCertDigest) + .isVerified(); + hasPlayStoreSourceStamp = Boolean.valueOf(result); + android.util.Log.d("PlayStoreSourceStampCheck", + "result for " + packageName + ": " + result); + return result; + } } diff --git a/core/java/android/content/pm/CrossProfileApps.java b/core/java/android/content/pm/CrossProfileApps.java index 354943d927c72..2d73e41a9ecc3 100644 --- a/core/java/android/content/pm/CrossProfileApps.java +++ b/core/java/android/content/pm/CrossProfileApps.java @@ -35,6 +35,7 @@ import android.app.ActivityOptions; import android.app.AppOpsManager.Mode; import android.app.admin.DevicePolicyManager; +import android.app.compat.gms.GmsCompat; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -315,6 +316,10 @@ public void startActivity(@NonNull ComponentName component, @NonNull UserHandle * @see UserManager#getUserProfiles() */ public @NonNull List getTargetUserProfiles() { + if (GmsCompat.isEnabled()) { + return java.util.Collections.emptyList(); + } + try { return mService.getTargetUserProfiles(mContext.getPackageName()); } catch (RemoteException ex) { @@ -484,6 +489,10 @@ private String getDefaultProfileSwitchingLabel(boolean isManagedProfile, String * @return true if the calling package can request to interact across profiles. */ public boolean canRequestInteractAcrossProfiles() { + if (GmsCompat.isEnabled()) { + return false; + } + try { return mService.canRequestInteractAcrossProfiles(mContext.getPackageName()); } catch (RemoteException ex) { @@ -515,6 +524,10 @@ public boolean canRequestInteractAcrossProfiles() { * calling UID. */ public boolean canInteractAcrossProfiles() { + if (GmsCompat.isEnabled()) { + return false; + } + try { return mService.canInteractAcrossProfiles(mContext.getPackageName()); } catch (RemoteException ex) { diff --git a/core/java/android/content/pm/GosPackageState.aidl b/core/java/android/content/pm/GosPackageState.aidl new file mode 100644 index 0000000000000..c7a48e85d0783 --- /dev/null +++ b/core/java/android/content/pm/GosPackageState.aidl @@ -0,0 +1,3 @@ +package android.content.pm; + +parcelable GosPackageState; diff --git a/core/java/android/content/pm/GosPackageState.java b/core/java/android/content/pm/GosPackageState.java new file mode 100644 index 0000000000000..91daf0b273226 --- /dev/null +++ b/core/java/android/content/pm/GosPackageState.java @@ -0,0 +1,344 @@ +package android.content.pm; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.annotation.SystemApi; +import android.annotation.UserIdInt; +import android.app.ActivityThread; +import android.app.PropertyInvalidatedCache; +import android.content.Context; +import android.ext.DerivedPackageFlag; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.RemoteException; +import android.os.UserHandle; +import android.permission.PermissionManager; + +import java.util.Arrays; +import java.util.Objects; + +/** + * GrapheneOS-specific persistent package state, stored in per-user PackageUserState. + *

+ * GosPackageState has a special handling for sharedUid packages. All packages in a given sharedUid + * share the same GosPackageState. This was done because in some cases (e.g. when an app accesses + * MediaProvider via FUSE) there's no way to retrieve the package name, only UID is available. + * Manually merging GosPackageStates of sharedUid members would be too complex. + * + * @hide + */ +@SystemApi +public final class GosPackageState implements Parcelable { + /** @hide */ public final long flagStorage1; + // flags that have package-specific meaning + /** @hide */ public final long packageFlagStorage; + @Nullable + public final byte[] storageScopes; + @Nullable + public final byte[] contactScopes; + /** + * 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 + * performing separate IPC to fetch them. + *

+ * Note that calculation of derived flags is skipped unless a GosPackageState flag is set that + * depends on derived flags, @see {@link com.android.server.pm.GosPackageStatePmHooks#maybeDeriveFlags} + *

+ * If package is part of sharedUid, then its derivedFlags are calculated across all + * sharedUid member packages. See GosPackageState javadoc for reasoning. + * @hide + */ + @DerivedPackageFlag.Enum public int derivedFlags; + + /** @hide */ public static final GosPackageState DEFAULT = createEmpty(); + + /** + * A sentinel value that is returned when the package is not installed (e.g. when it was racily + * uninstalled) and when the caller doesn't have access to the actual GosPackageState. + * + * @hide + */ + public static final GosPackageState NONE = createEmpty(); + + /** @hide */ + public GosPackageState(long flagStorage1, long packageFlagStorage, + @Nullable byte[] storageScopes, @Nullable byte[] contactScopes) { + this.flagStorage1 = flagStorage1; + this.packageFlagStorage = packageFlagStorage; + this.storageScopes = storageScopes; + this.contactScopes = contactScopes; + } + + private static GosPackageState createEmpty() { + return new GosPackageState(0L, 0L, null, null); + } + + private static final int TYPE_NONE = 0; + private static final int TYPE_DEFAULT = 1; + private static final int TYPE_REGULAR = 2; + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + int type = TYPE_REGULAR; + if (this == DEFAULT) { + type = TYPE_DEFAULT; + } else if (this == NONE) { + type = TYPE_NONE; + } + dest.writeInt(type); + if (type != TYPE_REGULAR) { + return; + } + dest.writeLong(this.flagStorage1); + dest.writeLong(this.packageFlagStorage); + dest.writeByteArray(storageScopes); + dest.writeByteArray(contactScopes); + dest.writeInt(derivedFlags); + } + + @NonNull + public static final Creator CREATOR = new Creator<>() { + @Override + public GosPackageState createFromParcel(Parcel in) { + switch (in.readInt()) { + case TYPE_DEFAULT: return DEFAULT; + case TYPE_NONE: return NONE; + }; + var res = new GosPackageState(in.readLong(), in.readLong(), + in.createByteArray(), in.createByteArray()); + res.derivedFlags = in.readInt(); + return res; + } + + @Override + public GosPackageState[] newArray(int size) { + return new GosPackageState[size]; + } + }; + + @Override + public int hashCode() { + return Long.hashCode(flagStorage1) + Arrays.hashCode(storageScopes) + Arrays.hashCode(contactScopes) + Long.hashCode(packageFlagStorage); + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof GosPackageState o)) { + return false; + } + if (this == o) { + return true; + } + if (flagStorage1 != o.flagStorage1) { + return false; + } + if (!Arrays.equals(storageScopes, o.storageScopes)) { + return false; + } + if (!Arrays.equals(contactScopes, o.contactScopes)) { + return false; + } + if (packageFlagStorage != o.packageFlagStorage) { + return false; + } + return true; + } + + @Override + public int describeContents() { + return 0; + } + + public boolean hasFlag(@GosPackageStateFlag.Enum int flag) { + return (this.flagStorage1 & (1L << flag)) != 0; + } + + public boolean hasPackageFlag(int packageFlag) { + return (this.packageFlagStorage & (1L << packageFlag)) != 0; + } + + public boolean hasDerivedFlag(@DerivedPackageFlag.Enum int flag) { + return (derivedFlags & flag) != 0; + } + + public boolean hasDerivedFlags(@DerivedPackageFlag.Enum int flags) { + return (derivedFlags & flags) == flags; + } + + /** @see #NONE */ + public boolean isNone() { + return this == NONE; + } + + @NonNull + public static GosPackageState getForSelf(@NonNull Context context) { + return get(context.getPackageName(), context.getUserId()); + } + + @NonNull + @SuppressLint("UserHandleName") + public static GosPackageState get(@NonNull String packageName, @NonNull UserHandle user) { + return get(packageName, user.getIdentifier()); + } + + @NonNull + public static GosPackageState get(@NonNull String packageName, @UserIdInt int userId) { + return Objects.requireNonNull(sCache.query(new CacheQuery(packageName, userId))); + } + + private record CacheQuery(String packageName, int userId) {} + + // invalidated by PackageManager#invalidatePackageInfoCache() (e.g. when + // PackageManagerService#setGosPackageState succeeds) + private static volatile PropertyInvalidatedCache sCache = + new PropertyInvalidatedCache<>(256, PermissionManager.CACHE_KEY_PACKAGE_INFO_CACHE, + "getGosPackageState") { + @Override + public GosPackageState recompute(CacheQuery query) { + try { + return ActivityThread.getPackageManager().getGosPackageState(query.packageName, query.userId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + }; + + @NonNull + @SuppressLint("UserHandleName") + public Editor createEditor(@NonNull String packageName, @NonNull UserHandle user) { + return createEditor(packageName, user.getIdentifier()); + } + + @NonNull + public Editor createEditor(@NonNull String packageName, @UserIdInt int userId) { + return new Editor(this, packageName, userId); + } + + @NonNull + @SuppressLint("UserHandleName") + public static Editor edit(@NonNull String packageName, @NonNull UserHandle user) { + return edit(packageName, user.getIdentifier()); + } + + @NonNull + public static Editor edit(@NonNull String packageName, @UserIdInt int userId) { + return GosPackageState.get(packageName, userId).createEditor(packageName, userId); + } + + /** @hide */ public static final int EDITOR_FLAG_KILL_UID_AFTER_APPLY = 1; + /** @hide */ public static final int EDITOR_FLAG_NOTIFY_UID_AFTER_APPLY = 1 << 1; + + public static class Editor { + private final String packageName; + private final int userId; + private long flagStorage1; + private long packageFlagStorage; + private byte[] storageScopes; + private byte[] contactScopes; + private int editorFlags; + + /** @hide */ + public Editor(GosPackageState s, String packageName, int userId) { + this.packageName = packageName; + this.userId = userId; + this.flagStorage1 = s.flagStorage1; + this.packageFlagStorage = s.packageFlagStorage; + this.storageScopes = s.storageScopes; + this.contactScopes = s.contactScopes; + } + + @NonNull + public Editor setFlagState(@GosPackageStateFlag.Enum int flag, boolean state) { + if (state) { + addFlag(flag); + } else { + clearFlag(flag); + } + return this; + } + + @NonNull + public Editor addFlag(@GosPackageStateFlag.Enum int flag) { + this.flagStorage1 |= (1L << flag); + return this; + } + + @NonNull + public Editor clearFlag(@GosPackageStateFlag.Enum int flag) { + this.flagStorage1 &= ~(1L << flag); + return this; + } + + @NonNull + public Editor addPackageFlag(int flag) { + this.packageFlagStorage |= (1L << flag); + return this; + } + + @NonNull + public Editor clearPackageFlag(int flag) { + this.packageFlagStorage &= ~(1L << flag); + return this; + } + + @NonNull + public Editor setPackageFlagState(int flag, boolean state) { + if (state) { + addPackageFlag(flag); + } else { + clearPackageFlag(flag); + } + return this; + } + + @NonNull + public Editor setStorageScopes(@Nullable byte[] storageScopes) { + this.storageScopes = storageScopes; + return this; + } + + @NonNull + public Editor setContactScopes(@Nullable byte[] contactScopes) { + this.contactScopes = contactScopes; + return this; + } + + @NonNull + public Editor killUidAfterApply() { + return setKillUidAfterApply(true); + } + + @NonNull + public Editor setKillUidAfterApply(boolean v) { + if (v) { + this.editorFlags |= EDITOR_FLAG_KILL_UID_AFTER_APPLY; + } else { + this.editorFlags &= ~EDITOR_FLAG_KILL_UID_AFTER_APPLY; + } + return this; + } + + @NonNull + public Editor setNotifyUidAfterApply(boolean v) { + if (v) { + this.editorFlags |= EDITOR_FLAG_NOTIFY_UID_AFTER_APPLY; + } else { + this.editorFlags &= ~EDITOR_FLAG_NOTIFY_UID_AFTER_APPLY; + } + return this; + } + + // Returns true if the update was successfully applied and is scheduled to be written back + // to storage. Actual writeback is performed asynchronously. + public boolean apply() { + try { + return ActivityThread.getPackageManager().setGosPackageState(packageName, userId, + new GosPackageState(flagStorage1, packageFlagStorage, storageScopes, contactScopes), + 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 new file mode 100644 index 0000000000000..79c05d16711b3 --- /dev/null +++ b/core/java/android/content/pm/GosPackageStateFlag.java @@ -0,0 +1,74 @@ +package android.content.pm; + +import android.annotation.IntDef; +import android.annotation.SystemApi; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** @hide */ +@SystemApi +public interface GosPackageStateFlag { + /* SysApi */ int STORAGE_SCOPES_ENABLED = 0; + /** @hide */ int ALLOW_ACCESS_TO_OBB_DIRECTORY = 1; + // flags 2, 3, 4 were used previously, do not reuse them + /* SysApi */ int CONTACT_SCOPES_ENABLED = 5; + /** @hide */ int BLOCK_NATIVE_DEBUGGING_NON_DEFAULT = 6; + /** @hide */ int BLOCK_NATIVE_DEBUGGING = 7; + /** @hide */ int BLOCK_NATIVE_DEBUGGING_SUPPRESS_NOTIF = 8; + /** @hide */ int RESTRICT_MEMORY_DYN_CODE_LOADING_NON_DEFAULT = 9; + /** @hide */ int RESTRICT_MEMORY_DYN_CODE_LOADING = 10; + /** @hide */ int RESTRICT_MEMORY_DYN_CODE_LOADING_SUPPRESS_NOTIF = 11; + /** @hide */ int RESTRICT_STORAGE_DYN_CODE_LOADING_NON_DEFAULT = 12; + /** @hide */ int RESTRICT_STORAGE_DYN_CODE_LOADING = 13; + /** @hide */ int RESTRICT_STORAGE_DYN_CODE_LOADING_SUPPRESS_NOTIF = 14; + /** @hide */ int RESTRICT_WEBVIEW_DYN_CODE_LOADING_NON_DEFAULT = 15; + /** @hide */ int RESTRICT_WEBVIEW_DYN_CODE_LOADING = 16; + /** @hide */ int USE_HARDENED_MALLOC_NON_DEFAULT = 17; + /** @hide */ int USE_HARDENED_MALLOC = 18; + /** @hide */ int USE_EXTENDED_VA_SPACE_NON_DEFAULT = 19; + /** @hide */ int USE_EXTENDED_VA_SPACE = 20; + /** @hide */ int FORCE_MEMTAG_NON_DEFAULT = 21; + /** @hide */ int FORCE_MEMTAG = 22; + /** @hide */ int FORCE_MEMTAG_SUPPRESS_NOTIF = 23; + /** @hide */ int ENABLE_EXPLOIT_PROTECTION_COMPAT_MODE = 24; + // flag 25 was used previously, do not reuse it + /** @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; + /** @hide */ int USE_EXEC_SPAWNING_NON_DEFAULT = 29; + /** @hide */ int USE_EXEC_SPAWNING = 30; + + /** @hide */ + @IntDef(value = { + STORAGE_SCOPES_ENABLED, + ALLOW_ACCESS_TO_OBB_DIRECTORY, + CONTACT_SCOPES_ENABLED, + BLOCK_NATIVE_DEBUGGING_NON_DEFAULT, + BLOCK_NATIVE_DEBUGGING, + BLOCK_NATIVE_DEBUGGING_SUPPRESS_NOTIF, + RESTRICT_MEMORY_DYN_CODE_LOADING_NON_DEFAULT, + RESTRICT_MEMORY_DYN_CODE_LOADING, + RESTRICT_MEMORY_DYN_CODE_LOADING_SUPPRESS_NOTIF, + RESTRICT_STORAGE_DYN_CODE_LOADING_NON_DEFAULT, + RESTRICT_STORAGE_DYN_CODE_LOADING, + RESTRICT_STORAGE_DYN_CODE_LOADING_SUPPRESS_NOTIF, + RESTRICT_WEBVIEW_DYN_CODE_LOADING_NON_DEFAULT, + RESTRICT_WEBVIEW_DYN_CODE_LOADING, + USE_HARDENED_MALLOC_NON_DEFAULT, + USE_HARDENED_MALLOC, + USE_EXTENDED_VA_SPACE_NON_DEFAULT, + USE_EXTENDED_VA_SPACE, + FORCE_MEMTAG_NON_DEFAULT, + FORCE_MEMTAG, + FORCE_MEMTAG_SUPPRESS_NOTIF, + ENABLE_EXPLOIT_PROTECTION_COMPAT_MODE, + PLAY_INTEGRITY_API_USED_AT_LEAST_ONCE, + SUPPRESS_PLAY_INTEGRITY_API_NOTIF, + BLOCK_PLAY_INTEGRITY_API, + USE_EXEC_SPAWNING_NON_DEFAULT, + USE_EXEC_SPAWNING, + }) + @Retention(RetentionPolicy.SOURCE) + @interface Enum {} +} diff --git a/core/java/android/content/pm/IPackageInstaller.aidl b/core/java/android/content/pm/IPackageInstaller.aidl index 3cfad302420d3..247e51be383f6 100644 --- a/core/java/android/content/pm/IPackageInstaller.aidl +++ b/core/java/android/content/pm/IPackageInstaller.aidl @@ -67,6 +67,9 @@ interface IPackageInstaller { @EnforcePermission("INSTALL_PACKAGES") void setPermissionsResult(int sessionId, boolean accepted); + @EnforcePermission("INSTALL_GRANT_RUNTIME_PERMISSIONS") + void updatePermissionStates(int sessionId, in String[] permissionNames, in int[] states); + void bypassNextStagedInstallerCheck(boolean value); void bypassNextAllowedApexUpdateCheck(boolean value); diff --git a/core/java/android/content/pm/IPackageInstallerSession.aidl b/core/java/android/content/pm/IPackageInstallerSession.aidl index bf3bffcc65688..bc568aa1c932a 100644 --- a/core/java/android/content/pm/IPackageInstallerSession.aidl +++ b/core/java/android/content/pm/IPackageInstallerSession.aidl @@ -61,6 +61,7 @@ interface IPackageInstallerSession { int[] getChildSessionIds(); void addChildSessionId(in int sessionId); void removeChildSessionId(in int sessionId); + int getId(); int getParentSessionId(); boolean isStaged(); diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl index e2a940953d821..81ef17444ea4b 100644 --- a/core/java/android/content/pm/IPackageManager.aidl +++ b/core/java/android/content/pm/IPackageManager.aidl @@ -866,4 +866,17 @@ interface IPackageManager { List getAllApexDirectories(); int getAppUidForPrivateComputeCoreUid(int pccUid); + + android.content.pm.GosPackageState getGosPackageState(String packageName, int userId); + + boolean setGosPackageState(String packageName, int userId, in android.content.pm.GosPackageState updatedPs, int editorFlags); + + PackageInfo findPackage(String packageName, long minVersion, in Bundle validSignaturesSha256); + + boolean updateListOfBusyPackages(boolean add, in List packageNames, IBinder callerBinder); + + void updateSeInfo(String packageName); + + void sendBootCompletedBroadcastToPackage(String packageName, boolean includeStopped, + int userId); } diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java index 8926ad126720b..ab0b047816f82 100644 --- a/core/java/android/content/pm/PackageInstaller.java +++ b/core/java/android/content/pm/PackageInstaller.java @@ -48,6 +48,7 @@ import android.app.ActivityThread; import android.app.AppGlobals; import android.app.PendingIntent; +import android.app.compat.gms.GmsCompat; import android.compat.annotation.UnsupportedAppUsage; import android.content.BroadcastReceiver; import android.content.ComponentName; @@ -79,6 +80,7 @@ import android.os.PersistableBundle; import android.os.RemoteCallback; import android.os.RemoteException; +import android.os.SystemClock; import android.os.SystemProperties; import android.os.UserHandle; import android.system.ErrnoException; @@ -87,8 +89,10 @@ import android.util.ArrayMap; import android.util.ArraySet; import android.util.ExceptionUtils; +import android.util.Log; import com.android.internal.content.InstallLocationUtils; +import com.android.internal.gmscompat.PlayStoreHooks; import com.android.internal.util.ArrayUtils; import com.android.internal.util.DataClass; import com.android.internal.util.IndentingPrintWriter; @@ -1048,6 +1052,10 @@ public PackageInstaller(IPackageInstaller installer, * session is finalized. IDs are not reused during a given boot. */ public int createSession(@NonNull SessionParams params) throws IOException { + if (GmsCompat.isPlayStore()) { + PlayStoreHooks.adjustSessionParams(params); + } + try { return mInstaller.createSession(params, mInstallerPackageName, mAttributionTag, mUserId); @@ -1123,6 +1131,16 @@ public void updateSessionAppLabel(int sessionId, @Nullable CharSequence appLabel * the session is invalid. */ public void abandonSession(int sessionId) { + if (GmsCompat.isPlayStore()) { + SessionInfo si = getSessionInfo(sessionId); + if (si != null && si.isCommitted()) { + // Play Store doesn't expect committed sessions to be waiting for confirmation from + // the user and tries to destroy them after ~10 minutes + Log.d("GmsCompat", "skipped PackageInstaller.abandonSession(), sessionId " + sessionId); + return; + } + } + try { mInstaller.abandonSession(sessionId); } catch (RemoteException e) { @@ -1409,6 +1427,22 @@ public void installPackageArchived(@NonNull ArchivedPackageInfo archivedPackageI Objects.requireNonNull(archivedPackageInfo, "archivedPackageInfo cannot be null"); Objects.requireNonNull(sessionParams, "sessionParams cannot be null"); Objects.requireNonNull(statusReceiver, "statusReceiver cannot be null"); + + if (GmsCompat.isPlayStore()) { + var fillIn = new Intent(); + fillIn.putExtra(PackageInstaller.EXTRA_PACKAGE_NAME, archivedPackageInfo.getPackageName()); + // there's no unprivileged alternative to this API + fillIn.putExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE); + com.android.internal.os.BackgroundThread.getHandler().post(() -> { + try { + statusReceiver.sendIntent(GmsCompat.appContext(), 0, fillIn, null, null); + } catch (IntentSender.SendIntentException e) { + Log.d("GmsCompat", "", e); + } + }); + return; + } + try { mInstaller.installPackageArchived( archivedPackageInfo.getParcel(), @@ -1432,6 +1466,16 @@ public void setPermissionsResult(int sessionId, boolean accepted) { } } + /** @hide */ + @RequiresPermission(Manifest.permission.INSTALL_GRANT_RUNTIME_PERMISSIONS) + public void updatePermissionStates(int sessionId, String[] permissionNames, int[] states) { + try { + mInstaller.updatePermissionStates(sessionId, permissionNames, states); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + /** * Check if install constraints are satisfied for the given packages. * @@ -2341,6 +2385,10 @@ public void onChecksumsReady(List checksums) * @see #requestUserPreapproval */ public void commit(@NonNull IntentSender statusReceiver) { + if (GmsCompat.isPlayStore()) { + statusReceiver = PlayStoreHooks.wrapCommitStatusReceiver(this, statusReceiver); + } + try { mSession.commit(statusReceiver, false); } catch (RemoteException e) { @@ -2436,6 +2484,20 @@ public void close() { * would be destroyed and the created {@link Session} information will be discarded.

*/ public void abandon() { + if (GmsCompat.isPlayStore()) { + final int id; + try { + id = mSession.getId(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + PackageInstaller packageInstaller = GmsCompat.appContext().getPackageManager() + .getPackageInstaller(); + // see comment in abandonSession() + packageInstaller.abandonSession(id); + return; + } + try { mSession.abandon(); } catch (RemoteException e) { @@ -3175,7 +3237,14 @@ public static class SessionParams implements Parcelable { @Nullable public PersistableBundle extensionParams; - private final ArrayMap mPermissionStates; + private volatile ArrayMap mPermissionStates; + /** + * @hide + * + * Used by gmscompat to disallow updates to unknown versions of GmsCore and Play Store. + */ + @SuppressLint("MutableBareField") + public long maxAllowedVersion = Long.MAX_VALUE; /** @hide */ public static final int MAX_URI_LENGTH = 2048; @@ -3192,6 +3261,10 @@ public static class SessionParams implements Parcelable { public SessionParams(int mode) { this.mode = mode; mPermissionStates = new ArrayMap<>(); + if (GmsCompat.isPlayStore()) { + // called here instead of in createSession() to give Play Store a chance to override + setRequireUserAction(USER_ACTION_NOT_REQUIRED); + } } /** @hide */ @@ -3235,6 +3308,7 @@ public SessionParams(Parcel source) { dexoptCompilerFilter = source.readString(); forceVerification = source.readBoolean(); isAutoInstallDependenciesEnabled = source.readBoolean(); + maxAllowedVersion = source.readLong(); extensionParams = source.readPersistableBundle(); } @@ -3274,6 +3348,7 @@ public SessionParams copy() { ret.dexoptCompilerFilter = dexoptCompilerFilter; ret.forceVerification = forceVerification; ret.isAutoInstallDependenciesEnabled = isAutoInstallDependenciesEnabled; + ret.maxAllowedVersion = maxAllowedVersion; ret.extensionParams = extensionParams; return ret; } @@ -3448,12 +3523,19 @@ public void setGrantedRuntimePermissions(String[] permissions) { @NonNull public SessionParams setPermissionState(@NonNull String permissionName, @PermissionState int state) { + setPermissionState(mPermissionStates, permissionName, state); + return this; + } + + /** @hide */ + public static void setPermissionState(ArrayMap states, + @NonNull String permissionName, @PermissionState int state) { if (TextUtils.isEmpty(permissionName)) { throw new IllegalArgumentException("Provided permissionName cannot be " + (permissionName == null ? "null" : "empty")); } if (state != PERMISSION_STATE_DEFAULT - && !validatePermissionStates(Set.of(permissionName))) { + && !validatePermissionStates(states, Set.of(permissionName))) { throw new IllegalArgumentException( "Permissions states exceeds size limits total size limit of " + MAX_PERMISSION_STATES_SIZE + " in length"); @@ -3461,22 +3543,20 @@ public SessionParams setPermissionState(@NonNull String permissionName, switch (state) { case PERMISSION_STATE_DEFAULT: - mPermissionStates.remove(permissionName); + states.remove(permissionName); break; case PERMISSION_STATE_GRANTED: case PERMISSION_STATE_DENIED: - mPermissionStates.put(permissionName, state); + states.put(permissionName, state); break; default: throw new IllegalArgumentException("Unexpected permission state int: " + state); } - - return this; } - private boolean validatePermissionStates(Collection permissionNames) { + private static boolean validatePermissionStates(ArrayMap permissionStates, Collection permissionNames) { int totalLength = 0; - for (String permission : mPermissionStates.keySet()) { + for (String permission : permissionStates.keySet()) { totalLength += permission.length(); } for (String permission : permissionNames) { @@ -3490,7 +3570,7 @@ public void setPermissionStates(Collection grantPermissions, Collection denyPermissions) { Set newPermissions = new HashSet<>(grantPermissions); newPermissions.addAll(denyPermissions); - if (!validatePermissionStates(newPermissions)) { + if (!validatePermissionStates(mPermissionStates, newPermissions)) { throw new IllegalArgumentException( "Permissions states exceeds size limits total size limit of " + MAX_PERMISSION_STATES_SIZE + " in length"); @@ -3999,6 +4079,11 @@ public ArrayMap getPermissionStates() { return mPermissionStates; } + /** @hide */ + public void replacePermissionStates(ArrayMap states) { + mPermissionStates = states; + } + /** @hide */ @Nullable public String[] getLegacyGrantedRuntimePermissions() { @@ -4146,6 +4231,7 @@ public void writeToParcel(Parcel dest, int flags) { dest.writeString(dexoptCompilerFilter); dest.writeBoolean(forceVerification); dest.writeBoolean(isAutoInstallDependenciesEnabled); + dest.writeLong(maxAllowedVersion); dest.writePersistableBundle(extensionParams); } @@ -6041,4 +6127,9 @@ long getRequiredStorageBytes() { return mUserActionIntent; } } + + /** @hide **/ + public IPackageInstaller getIPackageInstaller() { + return mInstaller; + } } diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index 5a9142dea8c2e..4360a8a45d117 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -2001,6 +2001,7 @@ default void onPermissionsChanged(int uid, @NonNull String persistentDeviceId) { @IntDef(flag = true, value = { DONT_KILL_APP, SYNCHRONOUS, + SKIP_IF_MISSING, }) @Retention(RetentionPolicy.SOURCE) public @interface EnabledFlags {} @@ -2022,6 +2023,9 @@ default void onPermissionsChanged(int uid, @NonNull String persistentDeviceId) { */ public static final int SYNCHRONOUS = 0x00000002; + /** @hide */ + public static final int SKIP_IF_MISSING = 0x4000_0000; + /** @hide */ @IntDef(flag = true, value = { FLAG_SUSPEND_QUARANTINED, @@ -12472,6 +12476,19 @@ public static int maybeGetSdkFeatureIndex(String featureName) { return com.android.internal.pm.SystemFeaturesMetadata.maybeGetSdkFeatureIndex(featureName); } + /** @hide */ + @RequiresPermission(Manifest.permission.GRANT_RUNTIME_PERMISSIONS) + @SystemApi + public void sendBootCompletedBroadcastToPackage(@NonNull String packageName, boolean includeStopped, + int userId) { + try { + ActivityThread.getPackageManager().sendBootCompletedBroadcastToPackage( + packageName, includeStopped, userId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + /** * Maps a Private Compute Core (PCC) UID to its corresponding application UID. * diff --git a/core/java/android/content/pm/SpecialRuntimePermAppUtils.java b/core/java/android/content/pm/SpecialRuntimePermAppUtils.java new file mode 100644 index 0000000000000..85ca727f0deff --- /dev/null +++ b/core/java/android/content/pm/SpecialRuntimePermAppUtils.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2022 GrapheneOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content.pm; + +import android.annotation.SystemApi; + +/** @hide */ +@SystemApi +public class SpecialRuntimePermAppUtils { + + private static boolean isInternetCompatEnabled; + + /** @hide */ + public static void enableInternetCompat() { + isInternetCompatEnabled = true; + } + + public static boolean isInternetCompatEnabled() { + return isInternetCompatEnabled; + } + + private SpecialRuntimePermAppUtils() {} +} diff --git a/core/java/android/content/pm/SrtPermissions.java b/core/java/android/content/pm/SrtPermissions.java new file mode 100644 index 0000000000000..73338d14b7dc8 --- /dev/null +++ b/core/java/android/content/pm/SrtPermissions.java @@ -0,0 +1,31 @@ +package android.content.pm; + +import android.Manifest; + +/** @hide */ +public class SrtPermissions { // "special runtime permissions" + public static final int FLAG_INTERNET_COMPAT_ENABLED = 1; + + private static int flags; + + public static int getFlags() { + return flags; + } + + public static void setFlags(int value) { + flags = value; + + if ((value & FLAG_INTERNET_COMPAT_ENABLED) != 0) { + SpecialRuntimePermAppUtils.enableInternetCompat(); + } + } + + public static boolean shouldSpoofSelfCheck(String permName) { + switch (permName) { + case Manifest.permission.INTERNET: + return SpecialRuntimePermAppUtils.isInternetCompatEnabled(); + default: + return false; + } + } +} diff --git a/core/java/android/content/pm/UserInfo.java b/core/java/android/content/pm/UserInfo.java index 9444edd721063..8b26f521ee137 100644 --- a/core/java/android/content/pm/UserInfo.java +++ b/core/java/android/content/pm/UserInfo.java @@ -549,13 +549,11 @@ public boolean canHaveProfile() { */ public boolean canHaveProfile(String userType) { if (!canHaveProfile()) { + if (UserManager.isUserTypePrivateProfile(userType) && UserManager.USER_TYPE_FULL_SECONDARY.equals(this.userType)) { + return true; + } return false; } - if (UserManager.isUserTypePrivateProfile(userType)) { - // Even if we eventually allow other users to have profiles too, only MainUsers are - // eligible to have a Private Space, for some reason. - return isMainUnlogged(); - } return true; } diff --git a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java index 26252a9906764..69244cea2aea0 100644 --- a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java +++ b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java @@ -773,8 +773,19 @@ private static ParseResult parseApkLite(ParseInput input, String codePa } } } else { - targetVer = minVer; - targetCode = minCode; + int targetSdkIndex = -1; + for (int i = 0, m = parser.getAttributeCount(); i < m; ++i) { + if (parser.getAttributeNameResource(i) == com.android.internal.R.attr.targetSdkVersion) { + targetSdkIndex = i; + break; + } + } + if (targetSdkIndex >= 0) { + targetVer = parser.getAttributeIntValue(targetSdkIndex, targetVer); + } else { + targetVer = minVer; + targetCode = minCode; + } } boolean allowUnknownCodenames = false; diff --git a/core/java/android/content/res/ApkAssets.java b/core/java/android/content/res/ApkAssets.java index bce15c36019bb..9a7e1d805745a 100644 --- a/core/java/android/content/res/ApkAssets.java +++ b/core/java/android/content/res/ApkAssets.java @@ -27,6 +27,7 @@ import android.util.Log; import com.android.internal.annotations.GuardedBy; +import com.android.internal.gmscompat.dynamite.GmsDynamiteClientHooks; import com.android.internal.pm.pkg.component.AconfigFlags; import dalvik.annotation.optimization.CriticalNative; @@ -167,7 +168,7 @@ public final class ApkAssets { */ public static @NonNull ApkAssets loadFromPath(@NonNull String path, @PropertyFlags int flags) throws IOException { - return new ApkAssets(FORMAT_APK, path, flags, null /* assets */); + return loadFromPath(path, flags, null); } /** @@ -181,6 +182,13 @@ public final class ApkAssets { */ public static @NonNull ApkAssets loadFromPath(@NonNull String path, @PropertyFlags int flags, @Nullable AssetsProvider assets) throws IOException { + if (GmsDynamiteClientHooks.enabled()) { + ApkAssets apkAssets = GmsDynamiteClientHooks.loadAssetsFromPath(path, flags, assets); + if (apkAssets != null) { + return apkAssets; + } + } + return new ApkAssets(FORMAT_APK, path, flags, assets); } diff --git a/core/java/android/content/res/AssetManager.java b/core/java/android/content/res/AssetManager.java index e18ba22389052..60cd8c1f7effb 100644 --- a/core/java/android/content/res/AssetManager.java +++ b/core/java/android/content/res/AssetManager.java @@ -28,6 +28,7 @@ import android.annotation.StringRes; import android.annotation.StyleRes; import android.annotation.TestApi; +import android.app.ActivityManager; import android.app.ResourcesManager; import android.compat.annotation.UnsupportedAppUsage; import android.content.pm.ActivityInfo; @@ -268,6 +269,9 @@ private AssetManager(boolean sentinel) { } } + /** @hide */ + public static volatile String[] systemIdmapPaths_; + /** * This must be called from Zygote so that system assets are shared by all applications. * @hide @@ -285,9 +289,29 @@ public static void createSystemAssetsInZygoteLocked(boolean reinitialize, apkAssets.add(ApkAssets.loadFromPath(frameworkPath, ApkAssets.PROPERTY_SYSTEM)); // TODO(Ravenwood): overlay support? - final String[] systemIdmapPaths = - RavenwoodHelperBridge.getInstance().isRunningOnRavenwood() ? new String[0] : - OverlayConfig.getZygoteInstance().createImmutableFrameworkIdmapsInZygote(); + String[] systemIdmapPaths; + if (RavenwoodHelperBridge.getInstance().isRunningOnRavenwood()) { + systemIdmapPaths = new String[0]; + } else { + // createImmutableFrameworkIdmapsInZygote() should be called only in zygote, it fails + // in regular processes and is unnecessary there. + // When it's called in zygote, overlay state is cached in /data/resource-cache/*@idmap + // files. These files are readable by regular app processes. + // + // When exec-based spawning in used, in-memory cache of assets is lost, and the spawned + // process is unable to recreate it, since it's not allowed to create idmaps. + // + // To resolve this issue, idmap paths are passed from system_server via AppBindArgs. + // system_server always uses zygote-based spawning. + systemIdmapPaths = systemIdmapPaths_; + if (systemIdmapPaths != null) { + Log.d(TAG, "reusing systemIdmapPaths"); + } else { + systemIdmapPaths = OverlayConfig.getZygoteInstance().createImmutableFrameworkIdmapsInZygote(); + systemIdmapPaths_ = systemIdmapPaths; + } + } + for (String idmapPath : systemIdmapPaths) { apkAssets.add(ApkAssets.loadOverlayFromPath(idmapPath, ApkAssets.PROPERTY_SYSTEM)); } diff --git a/core/java/android/content/res/Configuration.java b/core/java/android/content/res/Configuration.java index 9aa1b18648f0b..2f08be2bc985d 100644 --- a/core/java/android/content/res/Configuration.java +++ b/core/java/android/content/res/Configuration.java @@ -49,6 +49,7 @@ import android.annotation.TestApi; import android.app.GrammaticalInflectionManager; import android.app.WindowConfiguration; +import android.app.compat.gms.GmsCompat; import android.compat.annotation.UnsupportedAppUsage; import android.content.pm.ActivityInfo; import android.content.pm.ActivityInfo.Config; @@ -67,6 +68,7 @@ import android.util.proto.WireTypeMismatchException; import android.view.View; +import com.android.internal.gmscompat.PlayStoreHooks; import com.android.internal.util.XmlUtils; import org.xmlpull.v1.XmlPullParser; @@ -2360,7 +2362,14 @@ public void setGrammaticalGender(@GrammaticalGender int grammaticalGender) { */ public @NonNull LocaleList getLocales() { fixUpLocaleList(); - return mLocaleList; + LocaleList res = mLocaleList; + if (GmsCompat.isPlayStore()) { + LocaleList override = PlayStoreHooks.overrideApplicationLocales(res, null); + if (override != null) { + res = override; + } + } + return res; } /** diff --git a/core/java/android/content/res/Resources.java b/core/java/android/content/res/Resources.java index ca1380cea40e1..e2dde1c6d2640 100644 --- a/core/java/android/content/res/Resources.java +++ b/core/java/android/content/res/Resources.java @@ -76,6 +76,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.ext.EuiccGoogleHooks; import com.android.internal.util.ArrayUtils; import com.android.internal.util.GrowingArrayUtils; import com.android.internal.util.Preconditions; @@ -474,6 +475,15 @@ public ConfigurationBoundResourceCache getStateListAnimatorCa @NonNull public CharSequence getText(@StringRes int id) throws NotFoundException { CharSequence res = mResourcesImpl.getAssets().getResourceText(id); if (res != null) { + if (android.app.AppGlobals.getInitialPackageId() == android.ext.PackageId.G_EUICC_LPA) { + if (!EuiccGoogleHooks.isResourceFilteringSuppressed()) { + String s = res.toString(); + if (s.contains("Google")) { + Log.d("GEuiccLpa", "replaced getText() with empty string: " + s); + return ""; + } + } + } return res; } throw new NotFoundException("String resource ID #0x" diff --git a/core/java/android/credentials/CredentialOption.java b/core/java/android/credentials/CredentialOption.java index df948f17d6e01..2a8c5ac4feb13 100644 --- a/core/java/android/credentials/CredentialOption.java +++ b/core/java/android/credentials/CredentialOption.java @@ -22,14 +22,20 @@ import android.annotation.NonNull; import android.annotation.SuppressLint; +import android.app.compat.gms.GmsCompat; import android.content.ComponentName; +import android.content.Context; +import android.ext.PackageId; import android.os.Bundle; import android.os.Parcel; import android.os.Parcelable; +import android.os.RemoteException; import android.util.ArraySet; +import android.util.Log; import androidx.annotation.RequiresPermission; +import com.android.internal.gmscompat.GmsCompatApp; import com.android.internal.util.AnnotationValidations; import com.android.internal.util.Preconditions; @@ -284,6 +290,23 @@ public Builder(@NonNull String type, @NonNull Bundle credentialRetrievalData, @SuppressLint("MissingGetterMatchingBuilder") @NonNull public Builder setIsSystemProviderRequired(boolean isSystemProviderRequired) { + String typeGoogleId = "com.google.android.libraries.identity.googleid.TYPE_GOOGLE_ID_TOKEN_CREDENTIAL"; + if (isSystemProviderRequired && mType.equals(typeGoogleId)) { + Context appContext = GmsCompat.appContext(); + String gmscorePkg = PackageId.GMS_CORE_NAME; + if (appContext != null && GmsCompat.isEnabledFor(gmscorePkg, appContext.getUserId())) { + Log.d("GmsCompat", "ignored CredentialOption.setIsSystemProviderRequired(true) for " + + "TYPE_GOOGLE_ID_TOKEN_CREDENTIAL", new Throwable()); + mIsSystemProviderRequired = false; + + try { + GmsCompatApp.iClientOfGmsCore2Gca().onGoogleIdCredentialOptionInit(); + } catch (RemoteException e) { + throw GmsCompatApp.callFailed(e); + } + return this; + } + } mIsSystemProviderRequired = isSystemProviderRequired; return this; } diff --git a/core/java/android/database/sqlite/SQLiteOpenHelper.java b/core/java/android/database/sqlite/SQLiteOpenHelper.java index 75fe07f71bf2a..b1e2d73733603 100644 --- a/core/java/android/database/sqlite/SQLiteOpenHelper.java +++ b/core/java/android/database/sqlite/SQLiteOpenHelper.java @@ -19,6 +19,7 @@ import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.compat.gms.GmsCompat; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.database.DatabaseErrorHandler; @@ -27,6 +28,8 @@ import android.os.FileUtils; import android.util.Log; +import com.android.internal.gmscompat.GmsHooks; + import java.io.File; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; @@ -184,6 +187,10 @@ private SQLiteOpenHelper(@Nullable Context context, @Nullable String name, int v lock = sDbLock.computeIfAbsent(mName, (String k) -> new Object()); } mLock = lock; + + if (GmsCompat.isEnabled()) { + GmsHooks.onSQLiteOpenHelperConstructed(this, context); + } } /** diff --git a/core/java/android/ext/AppInfoExt.java b/core/java/android/ext/AppInfoExt.java new file mode 100644 index 0000000000000..4604ff0a7704d --- /dev/null +++ b/core/java/android/ext/AppInfoExt.java @@ -0,0 +1,85 @@ +package android.ext; + +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.app.AppGlobals; +import android.os.Parcel; +import android.os.Parcelable; + +/** @hide */ +@SystemApi +public final class AppInfoExt implements Parcelable { + /** @hide */ + public static final AppInfoExt DEFAULT = new AppInfoExt(PackageId.UNKNOWN, 0, 0L); + + private final int packageId; + private final int flags; + + /** @hide */ + public static final long HAS_COMPAT_CHANGES = 1L << 63; + private final long compatChanges; + + public AppInfoExt(int packageId, int flags, long compatChanges) { + this.packageId = packageId; + this.flags = flags; + this.compatChanges = compatChanges; + } + + /** + * One of {@link android.ext.PackageId} int constants. + */ + public int getPackageId() { + return packageId; + } + + public static int getInitialPackageId() { + return AppGlobals.getInitialPackageId(); + } + + public boolean hasFlag(@AppInfoExtFlag.Enum int flag) { + return (flags & (1 << flag)) != 0; + } + + public boolean hasCompatConfig() { + return (compatChanges & HAS_COMPAT_CHANGES) != 0; + } + + public boolean hasCompatChange(int flag) { + long mask = (1L << flag) | HAS_COMPAT_CHANGES; + return (compatChanges & mask) == mask; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int parcelFlags) { + boolean def = this == DEFAULT; + dest.writeBoolean(def); + if (def) { + return; + } + + dest.writeInt(packageId); + dest.writeInt(flags); + dest.writeLong(compatChanges); + } + + @NonNull + public static final Creator CREATOR = new Creator<>() { + @Override + public AppInfoExt createFromParcel(@NonNull Parcel p) { + if (p.readBoolean()) { + return DEFAULT; + } + return new AppInfoExt(p.readInt(), p.readInt(), p.readLong()); + } + + @Override + public AppInfoExt[] newArray(int size) { + return new AppInfoExt[size]; + } + }; +} diff --git a/core/java/android/ext/AppInfoExtFlag.java b/core/java/android/ext/AppInfoExtFlag.java new file mode 100644 index 0000000000000..917f247697582 --- /dev/null +++ b/core/java/android/ext/AppInfoExtFlag.java @@ -0,0 +1,20 @@ +package android.ext; + +import android.annotation.IntDef; +import android.annotation.SystemApi; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** @hide */ +@SystemApi +public interface AppInfoExtFlag { + /* SysApi */ int HAS_GMSCORE_CLIENT_LIBRARY = 0; + + /** @hide */ + @IntDef(value = { + HAS_GMSCORE_CLIENT_LIBRARY, + }) + @Retention(RetentionPolicy.SOURCE) + @interface Enum {} +} diff --git a/core/java/android/ext/BrowserUtils.java b/core/java/android/ext/BrowserUtils.java new file mode 100644 index 0000000000000..41bf530388f9a --- /dev/null +++ b/core/java/android/ext/BrowserUtils.java @@ -0,0 +1,26 @@ +package android.ext; + +import android.content.Context; + +import com.android.internal.R; +import com.android.internal.util.ArrayUtils; + +/** @hide */ +public class BrowserUtils { + + public static boolean isSystemBrowser(Context ctx, String pkg) { + // O(n), array is expected to have 1 or 2 (original-package) entries + return ArrayUtils.contains(getSystemBrowserPkgs(ctx), pkg); + } + + private static volatile String[] systemBrowserPkgs; + + public static String[] getSystemBrowserPkgs(Context ctx) { + String[] arr = systemBrowserPkgs; + if (arr == null) { + arr = ctx.getResources().getStringArray(R.array.system_browser_package_names); + systemBrowserPkgs = arr; + } + return arr; + } +} diff --git a/core/java/android/ext/ConnectivityUtil.java b/core/java/android/ext/ConnectivityUtil.java new file mode 100644 index 0000000000000..75777dcb00988 --- /dev/null +++ b/core/java/android/ext/ConnectivityUtil.java @@ -0,0 +1,46 @@ +package android.ext; + +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.os.UserHandle; +import android.provider.Settings; + +/** @hide */ +@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) +public class ConnectivityUtil { + private ConnectivityUtil() {} + /** + * Return true if this uid is a core system component, or if the uid is known to PackageManager + * and at least one of the packages that use it is a system app. + */ + public static boolean isSystem(@NonNull Context context, int uid) { + if (UserHandle.isCore(uid)) { + return true; + } + + PackageManager pm = context.getPackageManager(); + String[] packageNames = pm.getPackagesForUid(uid); + if (packageNames == null) { + return false; + } + int userId = UserHandle.getUserId(uid); + for (String packageName : packageNames) { + try { + ApplicationInfo appInfo = pm.getApplicationInfoAsUser(packageName, 0, userId); + if (appInfo.isSystemApp()) { + return true; + } + } catch (PackageManager.NameNotFoundException ignored) { } + } + return false; + } + + public static boolean isRegularAppWithLockdownVpnEnabled(@NonNull Context context, int uid) { + final int lockdownVpnEnabled = Settings.Secure.getIntForUser(context.getContentResolver(), + Settings.Secure.ALWAYS_ON_VPN_LOCKDOWN, 0, UserHandle.getUserId(uid)); + return lockdownVpnEnabled == 1 && !isSystem(context, uid); + } +} diff --git a/core/java/android/ext/DerivedPackageFlag.java b/core/java/android/ext/DerivedPackageFlag.java new file mode 100644 index 0000000000000..5258acf924327 --- /dev/null +++ b/core/java/android/ext/DerivedPackageFlag.java @@ -0,0 +1,53 @@ +package android.ext; + +import android.annotation.IntDef; +import android.annotation.SystemApi; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** @hide */ +@SystemApi +public interface DerivedPackageFlag { + // to distinguish between the case when no dflags are set and the case when dflags weren't calculated yet + int DFLAGS_SET = 1; + int EXPECTS_ALL_FILES_ACCESS = 1 << 1; + int EXPECTS_ACCESS_TO_MEDIA_FILES_ONLY = 1 << 2; + int EXPECTS_STORAGE_WRITE_ACCESS = 1 << 3; + int HAS_READ_EXTERNAL_STORAGE_DECLARATION = 1 << 4; + int HAS_WRITE_EXTERNAL_STORAGE_DECLARATION = 1 << 5; + int HAS_MANAGE_EXTERNAL_STORAGE_DECLARATION = 1 << 6; + int HAS_MANAGE_MEDIA_DECLARATION = 1 << 7; + int HAS_ACCESS_MEDIA_LOCATION_DECLARATION = 1 << 8; + int HAS_READ_MEDIA_AUDIO_DECLARATION = 1 << 9; + int HAS_READ_MEDIA_IMAGES_DECLARATION = 1 << 10; + int HAS_READ_MEDIA_VIDEO_DECLARATION = 1 << 11; + int HAS_READ_MEDIA_VISUAL_USER_SELECTED_DECLARATION = 1 << 12; + int EXPECTS_LEGACY_EXTERNAL_STORAGE = 1 << 13; + int HAS_READ_CONTACTS_DECLARATION = 1 << 20; + int HAS_WRITE_CONTACTS_DECLARATION = 1 << 21; + int HAS_GET_ACCOUNTS_DECLARATION = 1 << 22; + + /** @hide */ + @IntDef(flag = true, value = { + DFLAGS_SET, + EXPECTS_ALL_FILES_ACCESS, + EXPECTS_ACCESS_TO_MEDIA_FILES_ONLY, + EXPECTS_STORAGE_WRITE_ACCESS, + HAS_READ_EXTERNAL_STORAGE_DECLARATION, + HAS_WRITE_EXTERNAL_STORAGE_DECLARATION, + HAS_MANAGE_EXTERNAL_STORAGE_DECLARATION, + HAS_MANAGE_MEDIA_DECLARATION, + HAS_ACCESS_MEDIA_LOCATION_DECLARATION, + HAS_READ_MEDIA_AUDIO_DECLARATION, + HAS_READ_MEDIA_IMAGES_DECLARATION, + HAS_READ_MEDIA_VIDEO_DECLARATION, + HAS_READ_MEDIA_VISUAL_USER_SELECTED_DECLARATION, + EXPECTS_LEGACY_EXTERNAL_STORAGE, + HAS_READ_CONTACTS_DECLARATION, + HAS_WRITE_CONTACTS_DECLARATION, + HAS_GET_ACCOUNTS_DECLARATION, + }) + @Retention(RetentionPolicy.SOURCE) + @interface Enum {} +} diff --git a/core/java/android/ext/KnownSystemPackage.java b/core/java/android/ext/KnownSystemPackage.java new file mode 100644 index 0000000000000..bb88a92edad51 --- /dev/null +++ b/core/java/android/ext/KnownSystemPackage.java @@ -0,0 +1,26 @@ +package android.ext; + +import android.annotation.IntDef; +import android.annotation.SystemApi; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** @hide */ +@SystemApi +public interface KnownSystemPackage { + int SETTINGS = 0; + int SHELL = 1; + int SYSTEM_UI = 2; + int SETUP_WIZARD = 3; + + /** @hide */ + @IntDef(value = { + SETTINGS, + SHELL, + SYSTEM_UI, + SETUP_WIZARD, + }) + @Retention(RetentionPolicy.SOURCE) + @interface Enum {} +} diff --git a/core/java/android/ext/KnownSystemPackages.java b/core/java/android/ext/KnownSystemPackages.java new file mode 100644 index 0000000000000..839f04c0773bc --- /dev/null +++ b/core/java/android/ext/KnownSystemPackages.java @@ -0,0 +1,55 @@ +package android.ext; + +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.content.Context; +import android.content.res.Resources; + +import com.android.internal.R; + +/** @hide */ +@SystemApi +public final class KnownSystemPackages { + private static volatile KnownSystemPackages INSTANCE; + + @NonNull + public static KnownSystemPackages get(@NonNull Context ctx) { + var cache = INSTANCE; + if (cache != null) { + return cache; + } + return INSTANCE = new KnownSystemPackages(ctx); + } + + @NonNull public final String contactsProvider; + @NonNull public final String launcher; + @NonNull public final String mediaProvider; + @NonNull public final String permissionController; + @NonNull public final String settings; + @NonNull public final String setupWizard; + @NonNull public final String shell; + @NonNull public final String systemUi; + + private KnownSystemPackages(Context ctx) { + Resources res = ctx.getResources(); + contactsProvider = "com.android.providers.contacts"; + launcher = "com.android.launcher3"; + mediaProvider = "com.android.providers.media.module"; + permissionController = "com.android.permissioncontroller"; + settings = "com.android.settings"; + setupWizard = "app.grapheneos.setupwizard"; + shell = "com.android.shell"; + systemUi = res.getString(R.string.config_systemUi); + } + + @NonNull + public String getById(@KnownSystemPackage.Enum int id) { + return switch (id) { + case KnownSystemPackage.SETTINGS -> settings; + case KnownSystemPackage.SHELL -> shell; + case KnownSystemPackage.SYSTEM_UI -> systemUi; + case KnownSystemPackage.SETUP_WIZARD -> setupWizard; + default -> throw new IllegalArgumentException(); + }; + } +} diff --git a/core/java/android/ext/LogViewerApp.java b/core/java/android/ext/LogViewerApp.java new file mode 100644 index 0000000000000..0eb6239bc279b --- /dev/null +++ b/core/java/android/ext/LogViewerApp.java @@ -0,0 +1,67 @@ +package android.ext; + +import android.content.Intent; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.UUID; +import java.util.zip.GZIPOutputStream; + +import static java.nio.charset.StandardCharsets.UTF_8; + +/** @hide */ +public class LogViewerApp { + private static final String PACKAGE_NAME = "app.grapheneos.logviewer"; + + public static final String ACTION_ERROR_REPORT = PACKAGE_NAME + ".ERROR_REPORT"; + public static final String ACTION_LOGCAT = PACKAGE_NAME + ".LOGCAT"; + public static final String ACTION_PKG_LOGCAT = PACKAGE_NAME + ".PKG_LOGCAT"; + + public static final String EXTRA_ERROR_TYPE = "type"; + public static final String EXTRA_GZIPPED_MESSAGE = "gzipped_msg"; + public static final String EXTRA_SOURCE_APP_INFO = "source_app_info"; + public static final String EXTRA_SHOW_REPORT_BUTTON = "show_report_button"; + public static final String EXTRA_TEXT_TOMBSTONE_FILE_PATH = "text_tombstone_file_path"; + // Tombstone file path can be reused by a subsequent tombstone file, last modified timestamp is + // used to detect that case. + public static final String EXTRA_TEXT_TOMBSTONE_LAST_MODIFIED_TIME = "text_tombstone_last_modified"; + public static final String EXTRA_PREFER_TEXT_TOMBSTONE = "prefer_text_tombstone"; + + public static String getPackageName() { + return PACKAGE_NAME; + } + + public static Intent createBaseErrorReportIntent(String msg) { + var i = new Intent(ACTION_ERROR_REPORT); + i.putExtra(EXTRA_GZIPPED_MESSAGE, gzipString(msg)); + // this is needed for correct usage via PendingIntent + i.setIdentifier(UUID.randomUUID().toString()); + i.setPackage(PACKAGE_NAME); + return i; + } + + public static Intent getPackageLogcatIntent(String pkgName) { + var i = new Intent(ACTION_PKG_LOGCAT); + i.putExtra(Intent.EXTRA_PACKAGE_NAME, pkgName); + i.setPackage(PACKAGE_NAME); + return i; + } + + public static Intent getLogcatIntent() { + var i = new Intent(ACTION_LOGCAT); + i.setPackage(PACKAGE_NAME); + return i; + } + + private static byte[] gzipString(String msg) { + var bos = new ByteArrayOutputStream(msg.length() / 4); + + try (var s = new GZIPOutputStream(bos)) { + s.write(msg.getBytes(UTF_8)); + } catch (IOException e) { + throw new IllegalStateException(e); + } + + return bos.toByteArray(); + } +} diff --git a/core/java/android/ext/PackageId.java b/core/java/android/ext/PackageId.java new file mode 100644 index 0000000000000..2784ba86b84e0 --- /dev/null +++ b/core/java/android/ext/PackageId.java @@ -0,0 +1,59 @@ +package android.ext; + +import android.annotation.SystemApi; + +/** @hide */ +@SystemApi +// Int values that are assigned to packages in this interface can be retrieved at runtime from +// ApplicationInfo.ext().getPackageId() or from AndroidPackage.ext().getPackageId() (in system_server). +// +// PackageIds are assigned to parsed APKs only after they are verified, either by a certificate check +// or by a check that the APK is stored on an immutable OS partition. +public interface PackageId { + int UNKNOWN = 0; + + String GSF_NAME = "com.google.android.gsf"; + // no longer needed: int GSF = 1 + + String GMS_CORE_NAME = "com.google.android.gms"; + int GMS_CORE = 2; + + String PLAY_STORE_NAME = "com.android.vending"; + int PLAY_STORE = 3; + + String G_SEARCH_APP_NAME = "com.google.android.googlequicksearchbox"; + int G_SEARCH_APP = 4; + + String EUICC_SUPPORT_PIXEL_NAME = "com.google.euiccpixel"; + int EUICC_SUPPORT_PIXEL = 5; + + String G_EUICC_LPA_NAME = "com.google.android.euicc"; + int G_EUICC_LPA = 6; + + String G_CARRIER_SETTINGS_NAME = "com.google.android.carrier"; + int G_CARRIER_SETTINGS = 7; + + String G_CAMERA_NAME = "com.google.android.GoogleCamera"; + int G_CAMERA = 8; + + String PIXEL_CAMERA_SERVICES_NAME = "com.google.android.apps.camera.services"; + int PIXEL_CAMERA_SERVICES = 9; + + String ANDROID_AUTO_NAME = "com.google.android.projection.gearhead"; + int ANDROID_AUTO = 10; + + // "Google Fi" + String TYCHO_NAME = "com.google.android.apps.tycho"; + int TYCHO = 11; + + /** @hide */ String G_TEXT_TO_SPEECH_NAME = "com.google.android.tts"; + /** @hide */ int G_TEXT_TO_SPEECH = 12; + + String PIXEL_HEALTH_NAME = "com.google.android.apps.pixel.health"; + int PIXEL_HEALTH = 13; + + /** @hide */ String BUGLE_NAME = "com.google.android.apps.messaging"; + /** @hide */ int BUGLE = 14; + + /** @hide */ String SYSTEM_KEYBOARD = "com.android.inputmethod.latin"; +} diff --git a/core/java/android/ext/SettingsIntents.java b/core/java/android/ext/SettingsIntents.java new file mode 100644 index 0000000000000..956b1144b0e4d --- /dev/null +++ b/core/java/android/ext/SettingsIntents.java @@ -0,0 +1,24 @@ +package android.ext; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; + +/** @hide */ +public class SettingsIntents { + + public static final String APP_NATIVE_DEBUGGING = "android.settings.OPEN_APP_NATIVE_DEBUGGING_SETTINGS"; + public static final String APP_MEMTAG = "android.settings.OPEN_APP_MEMTAG_SETTINGS"; + public static final String APP_HARDENED_MALLOC = "android.settings.OPEN_APP_HARDENED_MALLOC_SETTINGS"; + public static final String APP_MEMORY_DYN_CODE_LOADING = "android.settings.OPEN_APP_MEMORY_DYN_CODE_LOADING_SETTINGS"; + public static final String APP_STORAGE_DYN_CODE_LOADING = "android.settings.OPEN_APP_STORAGE_DYN_CODE_LOADING_SETTINGS"; + public static final String APP_MANAGE_PLAY_INTEGRITY_API = "android.settings.OPEN_APP_MANAGE_PLAY_INTEGRITY_API_SETTINGS"; + + public static Intent getAppIntent(Context ctx, String action, String pkgName) { + var i = new Intent(action); + i.setData(Uri.fromParts("package", pkgName, null)); + i.setPackage(KnownSystemPackages.get(ctx).settings); + i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + return i; + } +} diff --git a/core/java/android/ext/cscopes/ContactScope.java b/core/java/android/ext/cscopes/ContactScope.java new file mode 100644 index 0000000000000..8565a233c3e06 --- /dev/null +++ b/core/java/android/ext/cscopes/ContactScope.java @@ -0,0 +1,71 @@ +package android.ext.cscopes; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.net.Uri; +import android.os.Parcel; +import android.os.Parcelable; + +/** @hide */ +@SystemApi +public final class ContactScope implements Parcelable { + public static final int TYPE_GROUP = 0; + public static final int TYPE_CONTACT = 1; + public static final int TYPE_NUMBER = 2; + public static final int TYPE_EMAIL = 3; + public static final int TYPE_COUNT = 4; + + public final int type; + public final long id; + + @Nullable + public final String title; + @Nullable + public final String summary; + @Nullable + public final Uri detailsUri; + + public ContactScope(int type, long id, @Nullable String title, @Nullable String summary, @Nullable Uri detailsUri) { + this.type = type; + this.id = id; + this.title = title; + this.summary = summary; + this.detailsUri = detailsUri; + } + + ContactScope(Parcel in) { + type = in.readInt(); + id = in.readLong(); + title = in.readString(); + summary = in.readString(); + detailsUri = in.readParcelable(Uri.class.getClassLoader(), Uri.class); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(type); + dest.writeLong(id); + dest.writeString(title); + dest.writeString(summary); + dest.writeParcelable(detailsUri, 0); + } + + @NonNull + public static final Creator CREATOR = new Creator<>() { + @Override + public ContactScope createFromParcel(Parcel in) { + return new ContactScope(in); + } + + @Override + public ContactScope[] newArray(int size) { + return new ContactScope[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } +} diff --git a/core/java/android/ext/cscopes/ContactScopesApi.java b/core/java/android/ext/cscopes/ContactScopesApi.java new file mode 100644 index 0000000000000..044ccfae94d93 --- /dev/null +++ b/core/java/android/ext/cscopes/ContactScopesApi.java @@ -0,0 +1,38 @@ +package android.ext.cscopes; + +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.content.ComponentName; +import android.content.Intent; +import android.provider.ContactsContract; + +/** @hide */ +@SystemApi +public class ContactScopesApi { + + public static final String ACTION_NOTIFY_CONTENT_OBSERVERS = + "android.ext.cscopes.action.NOTIFY_CONTENT_OBSERVERS"; + + public static final String SCOPED_CONTACTS_PROVIDER_AUTHORITY = ContactsContract.AUTHORITY + ".scoped"; + + // ScopedContactsProvider method IDs + public static final String METHOD_GET_IDS_FROM_URIS = "get_ids_from_uris"; + public static final String METHOD_GET_VIEW_MODEL = "get_view_model"; + public static final String METHOD_GET_GROUPS = "get_groups"; + + // keys for Bundles passed from/to ScopedContactsProvider methods + public static final String KEY_URIS = "uris"; + public static final String KEY_IDS = "ids"; + public static final String KEY_RESULT = "result"; + + @NonNull + public static Intent createConfigActivityIntent(@NonNull String targetPkg) { + var i = new Intent(); + String pkg = "com.android.permissioncontroller"; + i.setComponent(ComponentName.createRelative(pkg, ".cscopes.ContactScopesActivity")); + i.putExtra(Intent.EXTRA_PACKAGE_NAME, targetPkg); + return i; + } + + private ContactScopesApi() {} +} diff --git a/core/java/android/ext/cscopes/ContactScopesStorage.java b/core/java/android/ext/cscopes/ContactScopesStorage.java new file mode 100644 index 0000000000000..267aed4fdde85 --- /dev/null +++ b/core/java/android/ext/cscopes/ContactScopesStorage.java @@ -0,0 +1,153 @@ +package android.ext.cscopes; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.annotation.SystemApi; +import android.content.pm.GosPackageState; +import android.util.Log; + +import com.android.internal.util.ArrayUtils; + +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 ContactScopesStorage { + private static final String TAG = "ContactScope"; + + private final long[][] idsByType; + + @SuppressLint("MinMaxConstant") + public static final int MAX_COUNT_PER_PACKAGE = 100; + + private ContactScopesStorage() { + this(new long[ContactScope.TYPE_COUNT][0]); + } + + private ContactScopesStorage(long[][] idsByType) { + this.idsByType = idsByType; + } + + @NonNull + public long[] getIds(int type) { + return idsByType[type]; + } + + public boolean add(int type, long id) { + if (getCount() >= MAX_COUNT_PER_PACKAGE) { + return false; + } + + long[] cur = idsByType[type]; + long[] upd = ArrayUtils.appendLong(cur, id); + if (cur == upd) { + return false; + } + idsByType[type] = upd; + return true; + } + + public boolean remove(int type, long id) { + long[] cur = idsByType[type]; + long[] upd = ArrayUtils.removeLong(cur, id); + if (cur == upd) { + return false; + } + idsByType[type] = upd; + return true; + } + + public int getCount() { + int res = 0; + for (long[] arr : idsByType) { + res += arr.length; + } + return res; + } + + private static final int VERSION = 0; + + @Nullable + public byte[] serialize() { + int count = getCount(); + if (count == 0) { + return null; + } + + if (count > MAX_COUNT_PER_PACKAGE) { + throw new IllegalStateException(); + } + + var bos = new ByteArrayOutputStream(20 + (count * 8)); + + var s = new DataOutputStream(bos); + + try { + s.writeByte(VERSION); + + for (int type = ContactScope.TYPE_GROUP; type < ContactScope.TYPE_COUNT; ++type) { + long[] ids = this.idsByType[type]; + int idsLen = ids.length; + if (idsLen == 0) { + continue; + } + s.writeByte(type); + s.writeInt(idsLen); + for (int i = 0; i < idsLen; ++i) { + s.writeLong(ids[i]); + } + } + + return bos.toByteArray(); + } catch (IOException e) { + throw new IllegalStateException(e); + } + } + + public static boolean isEmpty(@NonNull GosPackageState gosPackageState) { + return gosPackageState.contactScopes == null; + } + + @NonNull + public static ContactScopesStorage deserialize(@NonNull GosPackageState gosPackageState) { + byte[] ser = gosPackageState.contactScopes; + if (ser == null) { + return new ContactScopesStorage(); + } + + var s = new DataInputStream(new ByteArrayInputStream(ser)); + + try { + final int version = s.readByte(); + if (version != VERSION) { + Log.e(TAG, "unexpected version " + version); + return new ContactScopesStorage(); + } + + long[][] idsByType = new long[ContactScope.TYPE_COUNT][0]; + + do { + int type = s.readByte(); + int cnt = s.readInt(); + + long[] ids = new long[cnt]; + + for (int i = 0; i < cnt; ++i) { + ids[i] = s.readLong(); + } + + idsByType[type] = ids; + } while (s.available() != 0); + + return new ContactScopesStorage(idsByType); + } catch (IOException e) { + Log.e(TAG, "", e); + return new ContactScopesStorage(); + } + } +} diff --git a/core/java/android/ext/cscopes/ContactsGroup.java b/core/java/android/ext/cscopes/ContactsGroup.java new file mode 100644 index 0000000000000..7445fc22e614e --- /dev/null +++ b/core/java/android/ext/cscopes/ContactsGroup.java @@ -0,0 +1,54 @@ +package android.ext.cscopes; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.os.Parcel; +import android.os.Parcelable; + +/** @hide */ +@SystemApi +public final class ContactsGroup implements Parcelable { + public final long id; + @Nullable + public final String title; + @Nullable + public final String summary; + + public ContactsGroup(long id, @Nullable String title, @Nullable String summary) { + this.id = id; + this.title = title; + this.summary = summary; + } + + ContactsGroup(@NonNull Parcel in) { + id = in.readLong(); + title = in.readString(); + summary = in.readString(); + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeLong(id); + dest.writeString(title); + dest.writeString(summary); + } + + @Override + public int describeContents() { + return 0; + } + + @NonNull + public static final Creator CREATOR = new Creator<>() { + @Override + public ContactsGroup createFromParcel(Parcel in) { + return new ContactsGroup(in); + } + + @Override + public ContactsGroup[] newArray(int size) { + return new ContactsGroup[size]; + } + }; +} diff --git a/core/java/android/ext/dcl/DynCodeLoading.java b/core/java/android/ext/dcl/DynCodeLoading.java new file mode 100644 index 0000000000000..975f11f24a2e5 --- /dev/null +++ b/core/java/android/ext/dcl/DynCodeLoading.java @@ -0,0 +1,172 @@ +package android.ext.dcl; + +import android.annotation.NonNull; +import android.annotation.SuppressLint; +import android.annotation.SystemApi; +import android.app.ActivityManager; +import android.app.AppGlobals; +import android.app.Application; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.GosPackageState; +import android.ext.settings.app.AswRestrictMemoryDynCodeLoading; +import android.ext.settings.app.AswRestrictStorageDynCodeLoading; +import android.ext.settings.app.AswRestrictWebViewDynCodeLoading; +import android.os.RemoteException; +import android.system.Os; +import android.util.Log; + +import androidx.annotation.Keep; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Objects; + +import dalvik.system.DexFile; + +/** @hide */ +@SystemApi +public class DynCodeLoading { + private static final String TAG = DynCodeLoading.class.getSimpleName(); + + /** @hide */ + public static final int RESTRICT_MEMORY_DCL = 1; + /** @hide */ + public static final int RESTRICT_STORAGE_DCL = 1 << 1; + /** @hide */ + public static final int RESTRICT_WEBVIEW_DCL = 1 << 2; + + /** @hide */ + public static int getAppBindFlags(Context ctx, int userId, ApplicationInfo appInfo, + GosPackageState gosPs) { + int res = 0; + + if (AswRestrictMemoryDynCodeLoading.I.get(ctx, userId, appInfo, gosPs)) { + res |= RESTRICT_MEMORY_DCL; + } + + if (AswRestrictStorageDynCodeLoading.I.get(ctx, userId, appInfo, gosPs)) { + res |= RESTRICT_STORAGE_DCL; + } + + if (AswRestrictWebViewDynCodeLoading.I.get(ctx, userId, appInfo, gosPs)) { + res |= RESTRICT_WEBVIEW_DCL; + } + + return res; + } + + private static int appBindFlags; + + /** @hide */ + public static int getAppBindFlags() { + return appBindFlags; + } + + /** @hide */ + public static void handleAppBindFlags(int flags) { + appBindFlags = flags; + if ((flags & (RESTRICT_MEMORY_DCL | RESTRICT_STORAGE_DCL)) != 0) { + // RESTRICT_WEBVIEW_DCL is intentionally not included here since it means + // "restrict DCL for isolated WebView processes" and shouldn't apply inside other app's + // processes. WebView processes are being moved to the new native-only (no ART) isolated + // processes in Android 17, which removes the need to block Java DCL there. + DexFile.enableDynCodeLoadingChecks(flags); + } + var sb = new StringBuilder("AppBindFlags: "); + if (flags == 0) { + sb.append("0"); + } else { + if ((flags & RESTRICT_MEMORY_DCL) != 0) { + sb.append("RESTRICT_MEMORY_DCL, "); + } + if ((flags & RESTRICT_STORAGE_DCL) != 0) { + sb.append("RESTRICT_STORAGE_DCL, "); + } + if ((flags & RESTRICT_WEBVIEW_DCL) != 0) { + sb.append("RESTRICT_WEBVIEW_DCL, "); + } + } + Log.d(TAG, sb.toString()); + } + + private static void showNotif(int type, String path, String denialType, Exception e) { + String pkgName = AppGlobals.getInitialPackage(); + + var reportLines = new ArrayList(); + reportLines.add("process: " + Application.getProcessName()); + reportLines.add("thread: " + Thread.currentThread().getName()); + reportLines.add(""); + String stackTrace = Log.getStackTraceString(e); + reportLines.addAll(Arrays.asList(stackTrace.split("\n"))); + + try { + ActivityManager.getService().showDynCodeLoadingNotification(type, pkgName, path, + reportLines, denialType); + } catch (RemoteException re) { + Log.d(TAG, "", re); + } + } + + @Keep // called from native ART code + public static void checkInMemoryDexFileOpen(int flags) { + var se = new SecurityException(); + showNotif(RESTRICT_MEMORY_DCL, null, "InMemoryDexFile", se); + throw se; + } + + @Keep + @SuppressLint("GenericException") + public static void checkDexFileOpen(int flags, @NonNull String rawPath) throws Exception { + String gmscompatFdPrefix = "/gmscompat_fd_"; + if (rawPath.startsWith(gmscompatFdPrefix)) { + int fd = Integer.parseInt(rawPath.substring(gmscompatFdPrefix.length())); + rawPath = Os.readlink("/proc/self/fd/" + fd); + } + + String path; + try { + path = new File(rawPath).getCanonicalPath(); + } catch (IOException e) { + path = null; + } + + boolean allow = false; + if (path != null) { + String[] pathParts = path.split("/"); + if (!pathParts[0].equals("")) { + throw new IllegalStateException(path); + } + + switch (pathParts[1]) { + case "apex": + case "product": + case "system": + case "system_ext": + case "vendor": + allow = true; + break; + case "data": + allow = path.startsWith("/data/app/") && path.endsWith(".apk"); + break; + } + } + + + if (allow) { + Log.d(TAG, "allowed DexFileOpen from " + rawPath); + } else { + String msg = "DCL via storage, path: " + rawPath; + if (!Objects.equals(rawPath, path)) { + msg += ", canonicalPath: " + path; + } + var e = new SecurityException(msg); + showNotif(RESTRICT_STORAGE_DCL, rawPath, "DexFileOpen", e); + throw e; + } + } + + private DynCodeLoading() {} +} diff --git a/core/java/android/ext/power/BatteryChargeLimit.java b/core/java/android/ext/power/BatteryChargeLimit.java new file mode 100644 index 0000000000000..652c2f41c65f2 --- /dev/null +++ b/core/java/android/ext/power/BatteryChargeLimit.java @@ -0,0 +1,30 @@ +package android.ext.power; + +import android.content.Context; +import android.ext.settings.BoolSetting; +import android.ext.settings.Setting; +import android.os.Build; +import android.provider.Settings; + +/** @hide */ +public class BatteryChargeLimit { + public static final int CHARGE_LEVEL = 80; + + private static final BoolSetting SETTING = new BoolSetting(Setting.Scope.GLOBAL, + Settings.Global.BATTERY_CHARGE_LIMIT, false); + + public static BoolSetting getSetting() { + return SETTING; + } + + public static boolean isChargeLimitEnabled(Context context) { + if (!isGoogleDevice()) { + return false; + } + return SETTING.get(context); + } + + public static boolean isGoogleDevice() { + return "Google".equals(Build.MANUFACTURER); + } +} diff --git a/core/java/android/ext/settings/AltTouchscreenMode.java b/core/java/android/ext/settings/AltTouchscreenMode.java new file mode 100644 index 0000000000000..9f79c5e38ea28 --- /dev/null +++ b/core/java/android/ext/settings/AltTouchscreenMode.java @@ -0,0 +1,18 @@ +package android.ext.settings; + +import android.content.Context; + +import com.android.internal.R; + +/** @hide */ +public class AltTouchscreenMode { + public static final String STATE_OF_DEFAULT_DISPLAY_PROP = "sys.state_of_default_display"; + + public static BoolSysProperty getSetting() { + return new BoolSysProperty("persist.sys.alt_touch_mode", false); + } + + public static boolean isAvailable(Context ctx) { + return ctx.getResources().getBoolean(R.bool.config_has_alternative_touchscreen_mode); + } +} diff --git a/core/java/android/ext/settings/BoolSetting.java b/core/java/android/ext/settings/BoolSetting.java new file mode 100644 index 0000000000000..fc6f808a7e73c --- /dev/null +++ b/core/java/android/ext/settings/BoolSetting.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2022 GrapheneOS + * SPDX-License-Identifier: Apache-2.0 + */ + +package android.ext.settings; + +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.content.Context; + +import java.util.function.Function; + +/** @hide */ +@SystemApi +public class BoolSetting extends Setting { + private boolean defaultValue; + private volatile Function defaultValueSupplier; + + public BoolSetting(@NonNull Scope scope, @NonNull String key, boolean defaultValue) { + super(scope, key); + this.defaultValue = defaultValue; + } + + public BoolSetting(@NonNull Scope scope, @NonNull String key, @NonNull Function defaultValue) { + super(scope, key); + defaultValueSupplier = defaultValue; + } + + public final boolean get(@NonNull Context ctx) { + return get(ctx, ctx.getUserId()); + } + + // use only if this is a per-user setting and the context is not a per-user one + public final boolean get(@NonNull Context ctx, int userId) { + String valueStr = getRaw(ctx, userId); + + if (valueStr == null) { + return getDefaultValue(ctx); + } + + if (valueStr.equals("true")) { + return true; + } + + if (valueStr.equals("false")) { + return false; + } + + try { + int valueInt = Integer.parseInt(valueStr); + if (valueInt == 1) { + return true; + } else if (valueInt == 0) { + return false; + } + } catch (NumberFormatException e) { + e.printStackTrace(); + } + + return getDefaultValue(ctx); + } + + public final boolean put(@NonNull Context ctx, boolean val) { + return putRaw(ctx, val ? "1" : "0"); + } + + private boolean getDefaultValue(Context ctx) { + Function supplier = defaultValueSupplier; + if (supplier != null) { + defaultValue = supplier.apply(ctx).booleanValue(); + defaultValueSupplier = null; + } + return defaultValue; + } +} diff --git a/core/java/android/ext/settings/BoolSysProperty.java b/core/java/android/ext/settings/BoolSysProperty.java new file mode 100644 index 0000000000000..4ea251cb1ffb3 --- /dev/null +++ b/core/java/android/ext/settings/BoolSysProperty.java @@ -0,0 +1,32 @@ +package android.ext.settings; + +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.content.Context; +import android.os.UserHandle; + +import java.util.function.BooleanSupplier; +import java.util.function.Function; + +/** @hide */ +@SystemApi +public class BoolSysProperty extends BoolSetting { + + public BoolSysProperty(@NonNull String key, boolean defaultValue) { + super(Scope.SYSTEM_PROPERTY, key, defaultValue); + } + + public BoolSysProperty(@NonNull String key, @NonNull Function defaultValue) { + super(Scope.SYSTEM_PROPERTY, key, defaultValue); + } + + public boolean get() { + //noinspection DataFlowIssue + return super.get(null, UserHandle.USER_SYSTEM); + } + + public boolean put(boolean val) { + //noinspection DataFlowIssue + return super.put(null, val); + } +} diff --git a/core/java/android/ext/settings/CertTransparencyDownloaderSetting.java b/core/java/android/ext/settings/CertTransparencyDownloaderSetting.java new file mode 100644 index 0000000000000..8c7a6e9b52e68 --- /dev/null +++ b/core/java/android/ext/settings/CertTransparencyDownloaderSetting.java @@ -0,0 +1,22 @@ +package android.ext.settings; + +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.provider.Settings; + +/** @hide */ +@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) +public class CertTransparencyDownloaderSetting { + /** Use GrapheneOS server */ + public static final int VAL_GRAPHENEOS = 0; + /** Use standard (Google) server */ + public static final int VAL_STANDARD = 1; + /** Fail all CertificateTransparencyDownloader downloads */ + public static final int VAL_OFF = 2; + + @NonNull + public static final IntSetting SETTING = new IntSetting(Setting.Scope.GLOBAL, + Settings.Global.CERT_TRANSPARENCY_DOWNLOADER, VAL_GRAPHENEOS); + + private CertTransparencyDownloaderSetting() {} +} diff --git a/core/java/android/ext/settings/ConnChecksSetting.java b/core/java/android/ext/settings/ConnChecksSetting.java new file mode 100644 index 0000000000000..b6247a0acb371 --- /dev/null +++ b/core/java/android/ext/settings/ConnChecksSetting.java @@ -0,0 +1,45 @@ +package android.ext.settings; + +import android.annotation.SystemApi; +import android.compat.annotation.UnsupportedAppUsage; +import android.os.SystemProperties; + +/** @hide */ +@SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) +public class ConnChecksSetting { + + public static final int VAL_GRAPHENEOS = 0; + public static final int VAL_STANDARD = 1; + public static final int VAL_DISABLED = 2; + public static final int VAL_DEFAULT = VAL_GRAPHENEOS; + + /** + * Note that the underlying system property is read directly in packages/modules/DnsResolver + * + * @hide + * */ + public static final IntSysProperty SYS_PROP = new IntSysProperty( + "persist.sys.connectivity_checks", + ConnChecksSetting.VAL_DEFAULT, + ConnChecksSetting.VAL_GRAPHENEOS, ConnChecksSetting.VAL_STANDARD, ConnChecksSetting.VAL_DISABLED + ); + + @UnsupportedAppUsage + public static int get() { + return SYS_PROP.get(); + } + + public static boolean put(int val) { + return SYS_PROP.put(val); + } + + /** + * Use only during migration, to decide whether to perform it. + * @hide + */ + public static boolean isSet() { + return !SystemProperties.get(SYS_PROP.getKey()).isEmpty(); + } + + private ConnChecksSetting() {} +} diff --git a/core/java/android/ext/settings/CrossProfileClipboardAccessSettings.java b/core/java/android/ext/settings/CrossProfileClipboardAccessSettings.java new file mode 100644 index 0000000000000..5e70abf069cb0 --- /dev/null +++ b/core/java/android/ext/settings/CrossProfileClipboardAccessSettings.java @@ -0,0 +1,32 @@ +package android.ext.settings; + +import android.annotation.NonNull; +import android.content.Context; +import android.provider.Settings; + +/** @hide */ +public class CrossProfileClipboardAccessSettings { + + public static final int FOLLOW_DEFAULT = 0; + public static final int ALLOW_IMPORT_DEFAULTS_ONLY = 1; + public static final int ALLOW_EXPORT_DEFAULTS_ONLY = 2; + public static final int BLOCK = 3; + + public static final IntSetting CROSS_PROFILE_CLIPBOARD_ACCESS_SETTINGS = new IntSetting( + Setting.Scope.PER_USER, Settings.Secure.CROSS_PROFILE_CLIPBOARD_ACCESS, + // default + FOLLOW_DEFAULT, + // valid values + FOLLOW_DEFAULT, ALLOW_IMPORT_DEFAULTS_ONLY, ALLOW_EXPORT_DEFAULTS_ONLY, BLOCK + ); + + public static boolean isExportAccessAllowed(@NonNull Context ctx, int userId) { + int curSetting = CROSS_PROFILE_CLIPBOARD_ACCESS_SETTINGS.get(ctx, userId); + return curSetting == FOLLOW_DEFAULT || curSetting == ALLOW_EXPORT_DEFAULTS_ONLY; + } + + public static boolean isImportAccessAllowed(@NonNull Context ctx, int userId) { + int curSetting = CROSS_PROFILE_CLIPBOARD_ACCESS_SETTINGS.get(ctx, userId); + return curSetting == FOLLOW_DEFAULT || curSetting == ALLOW_IMPORT_DEFAULTS_ONLY; + } +} diff --git a/core/java/android/ext/settings/ExtSettings.java b/core/java/android/ext/settings/ExtSettings.java new file mode 100644 index 0000000000000..821f3d2384ce2 --- /dev/null +++ b/core/java/android/ext/settings/ExtSettings.java @@ -0,0 +1,116 @@ +package android.ext.settings; + +import android.annotation.BoolRes; +import android.annotation.IntegerRes; +import android.annotation.StringRes; +import android.content.Context; +import android.provider.Settings; + +import com.android.internal.R; + +import java.util.concurrent.TimeUnit; +import java.util.function.Function; +import java.util.function.ToIntFunction; + +/** + * Note that android.provider.Settings setting names should be defined in the corresponding classes, + * since the readability of settings is determined by using Java reflection on members of that class. + * + * @see android.provider.Settings#getPublicSettingsForClass + * @hide + */ +public class ExtSettings { + + public static final BoolSysProperty EXEC_SPAWNING = new BoolSysProperty( + "persist.security.exec_spawn", true); + + public static final BoolSetting ALLOW_KEYGUARD_CAMERA = new BoolSetting( + Setting.Scope.SYSTEM_PROPERTY, "persist.keyguard.camera", true); + + public static final BoolSetting AUTO_GRANT_OTHER_SENSORS_PERMISSION = new BoolSetting( + Setting.Scope.PER_USER, Settings.Secure.AUTO_GRANT_OTHER_SENSORS_PERMISSION, true); + + public static final IntSetting AUTO_REBOOT_TIMEOUT = new IntSetting( + Setting.Scope.GLOBAL, Settings.Global.AUTO_REBOOT_TIMEOUT, + // default value: 18 hours + (int) TimeUnit.HOURS.toMillis(18)); + + public static final BoolSetting SCREENSHOT_TIMESTAMP_EXIF = new BoolSetting( + Setting.Scope.PER_USER, Settings.Secure.SCREENSHOT_TIMESTAMP_EXIF, false); + + public static final BoolSetting SCRAMBLE_LOCKSCREEN_PIN_LAYOUT_PRIMARY = new BoolSetting( + Setting.Scope.PER_USER, Settings.Secure.SCRAMBLE_PIN_LAYOUT_PRIMARY, false); + public static final BoolSetting SCRAMBLE_LOCKSCREEN_PIN_LAYOUT_SECONDARY = new BoolSetting( + Setting.Scope.PER_USER, Settings.Secure.SCRAMBLE_PIN_LAYOUT_SECONDARY, false); + + public static final BoolSetting SCRAMBLE_SIM_PIN_LAYOUT = new BoolSetting( + Setting.Scope.PER_USER, Settings.Secure.SCRAMBLE_SIM_PIN_LAYOUT, + // inherit lockscreen PIN setting by default + SCRAMBLE_LOCKSCREEN_PIN_LAYOUT_PRIMARY::get); + + public static final BoolSysProperty ALLOW_GOOGLE_APPS_SPECIAL_ACCESS_TO_ACCELERATORS = new BoolSysProperty( + // also accessed in native code, in frameworks/native/cmds/servicemanager/Access.cpp + "persist.sys.allow_google_apps_special_access_to_accelerators", true); + + // The amount of time in milliseconds before a disconnected Wi-Fi adapter is turned off + public static final IntSetting WIFI_AUTO_OFF = new IntSetting( + Setting.Scope.GLOBAL, Settings.Global.WIFI_AUTO_OFF, 0 /* off by default */); + + // The amount of time in milliseconds before a disconnected Bluetooth adapter is turned off + public static final IntSetting BLUETOOTH_AUTO_OFF = new IntSetting( + Setting.Scope.GLOBAL, Settings.Global.BLUETOOTH_AUTO_OFF, 0 /* off by default */); + + // The amount of time in milliseconds before a Nfc adapter is turned off + public static final IntSetting NFC_AUTO_OFF = new IntSetting( + Setting.Scope.GLOBAL, Settings.Global.NFC_AUTO_OFF, 0 /* off by default */); + + public static final BoolSysProperty ALLOW_NATIVE_DEBUG_BY_DEFAULT = new BoolSysProperty( + "persist.native_debug", defaultBool(R.bool.setting_default_allow_native_debugging)); + + // AppCompatConfig specifies which hardening features are compatible/incompatible with a + // specific app. + // This setting controls whether incompatible hardening features would be disabled by default + // for that app. In both cases, user will still be able to enable/disable them manually. + // + // Note that hardening features that are marked as compatible are enabled unconditionally by + // default, regardless of this setting. + public static final BoolSetting ALLOW_DISABLING_HARDENING_VIA_APP_COMPAT_CONFIG = new BoolSetting( + Setting.Scope.GLOBAL, Settings.Global.ALLOW_DISABLING_HARDENING_VIA_APP_COMPAT_CONFIG, + defaultBool(R.bool.setting_default_allow_disabling_hardening_via_app_compat_config)); + + public static final BoolSetting RESTRICT_MEMORY_DYN_CODE_LOADING_BY_DEFAULT = new BoolSetting( + Setting.Scope.GLOBAL, Settings.Global.RESTRICT_MEMORY_DYN_CODE_LOADING_BY_DEFAULT, + defaultBool(R.bool.setting_default_restrict_memory_dyn_code_loading)); + + public static final BoolSetting RESTRICT_STORAGE_DYN_CODE_LOADING_BY_DEFAULT = new BoolSetting( + Setting.Scope.GLOBAL, Settings.Global.RESTRICT_STORAGE_DYN_CODE_LOADING_BY_DEFAULT, + defaultBool(R.bool.setting_default_restrict_storage_dyn_code_loading)); + + public static final BoolSetting RESTRICT_WEBVIEW_DYN_CODE_LOADING_BY_DEFAULT = new BoolSetting( + Setting.Scope.GLOBAL, Settings.Global.RESTRICT_WEBVIEW_DYN_CODE_LOADING_BY_DEFAULT, + defaultBool(R.bool.setting_default_restrict_webview_dyn_code_loading)); + + public static final BoolSetting FORCE_APP_MEMTAG_BY_DEFAULT = new BoolSetting( + Setting.Scope.GLOBAL, Settings.Global.FORCE_APP_MEMTAG_BY_DEFAULT, + defaultBool(R.bool.setting_default_force_app_memtag)); + + public static final BoolSetting SHOW_SYSTEM_PROCESS_CRASH_NOTIFICATIONS = new BoolSetting( + Setting.Scope.GLOBAL, Settings.Global.SHOW_SYSTEM_PROCESS_CRASH_NOTIFICATIONS, false); + + public static final BoolSetting DISALLOW_DELAYED_LOCKING_ON_USER_STOP = new BoolSetting( + Setting.Scope.PER_USER, Settings.Secure.DISALLOW_DELAYED_LOCKING_ON_USER_STOP, false); + + private ExtSettings() {} + + public static Function defaultBool(@BoolRes int res) { + return ctx -> ctx.getResources().getBoolean(res); + } + + public static ToIntFunction defaultInt(@IntegerRes int res) { + return ctx -> ctx.getResources().getInteger(res); + } + + public static Function defaultString(@StringRes int res) { + return ctx -> ctx.getString(res); + } +} diff --git a/core/java/android/ext/settings/GeocoderSettings.java b/core/java/android/ext/settings/GeocoderSettings.java new file mode 100644 index 0000000000000..969a38970e956 --- /dev/null +++ b/core/java/android/ext/settings/GeocoderSettings.java @@ -0,0 +1,17 @@ +package android.ext.settings; + +import android.provider.Settings; + +/** @hide */ +public class GeocoderSettings { + + public static final int GEOCODER_DISABLED = 0; + public static final int GEOCODER_SERVER_OPENSTREETMAP = 1; + public static final int GEOCODER_SERVER_GRAPHENEOS = 2; + + public static final IntSetting GEOCODER_SETTING = new IntSetting( + Setting.Scope.GLOBAL, Settings.Global.GEOCODER, + GEOCODER_DISABLED, // default + GEOCODER_SERVER_GRAPHENEOS, GEOCODER_SERVER_OPENSTREETMAP, GEOCODER_DISABLED // valid values + ); +} diff --git a/core/java/android/ext/settings/GnssSettings.java b/core/java/android/ext/settings/GnssSettings.java new file mode 100644 index 0000000000000..c380bc36f3165 --- /dev/null +++ b/core/java/android/ext/settings/GnssSettings.java @@ -0,0 +1,37 @@ +package android.ext.settings; + +import android.content.Context; +import android.provider.Settings; + +/** @hide */ +public class GnssSettings { + + public static final int SUPL_SERVER_STANDARD = 0; + public static final int SUPL_DISABLED = 1; + public static final int SUPL_SERVER_GRAPHENEOS_PROXY = 2; + + public static final IntSetting SUPL_SETTING = new IntSetting( + Setting.Scope.GLOBAL, Settings.Global.GNSS_SUPL, + SUPL_SERVER_GRAPHENEOS_PROXY, // default + SUPL_SERVER_STANDARD, SUPL_DISABLED, SUPL_SERVER_GRAPHENEOS_PROXY // valid values + ); + + // keep in sync with bionic/libc/bionic/gnss_psds_setting.c + public static final int PSDS_SERVER_GRAPHENEOS = 0; + public static final int PSDS_SERVER_STANDARD = 1; + public static final int PSDS_DISABLED = 2; + + public static final IntSetting STANDARD_PSDS_SETTING = new IntSetting( + Setting.Scope.GLOBAL, Settings.Global.GNSS_PSDS_STANDARD, + PSDS_SERVER_GRAPHENEOS, // default + PSDS_SERVER_GRAPHENEOS, PSDS_SERVER_STANDARD, PSDS_DISABLED // valid values + ); + + public static IntSetting getPsdsSetting(Context ctx) { + return STANDARD_PSDS_SETTING; + } + + public static boolean isStandardPsds(Context ctx) { + return getPsdsSetting(ctx) == STANDARD_PSDS_SETTING; + } +} diff --git a/core/java/android/ext/settings/IntSetting.java b/core/java/android/ext/settings/IntSetting.java new file mode 100644 index 0000000000000..bffc35ae75cc3 --- /dev/null +++ b/core/java/android/ext/settings/IntSetting.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2022 GrapheneOS + * SPDX-License-Identifier: Apache-2.0 + */ + +package android.ext.settings; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.content.Context; + +import java.util.function.ToIntFunction; + +/** @hide */ +@SystemApi +public class IntSetting extends Setting { + private int defaultValue; + private volatile ToIntFunction defaultValueSupplier; + + @Nullable private final int[] validValues; + + private IntSetting(@NonNull Scope scope, @NonNull String key, @Nullable int[] validValues) { + super(scope, key); + this.validValues = validValues; + } + + public IntSetting(@NonNull Scope scope, @NonNull String key, int defaultValue) { + this(scope, key, (int[]) null); + setDefaultValue(defaultValue); + } + + public IntSetting(@NonNull Scope scope, @NonNull String key, int defaultValue, @NonNull int... validValues) { + this(scope, key, validValues); + setDefaultValue(defaultValue); + } + + public IntSetting(@NonNull Scope scope, @NonNull String key, @NonNull ToIntFunction defaultValue) { + this(scope, key, (int[]) null); + defaultValueSupplier = defaultValue; + } + + public IntSetting(@NonNull Scope scope, @NonNull String key, @NonNull ToIntFunction defaultValue, + @NonNull int... validValues) { + this(scope, key, validValues); + defaultValueSupplier = defaultValue; + } + + public boolean validateValue(int val) { + if (validValues == null) { + return true; + } + // don't do sort() + bsearch() of validValues array, it's expected to have a small number of entries + for (int validValue : validValues) { + if (val == validValue) { + return true; + } + } + return false; + } + + public final int get(@NonNull Context ctx) { + return get(ctx, ctx.getUserId()); + } + + // use only if this is a per-user setting and the context is not a per-user one + public final int get(@NonNull Context ctx, int userId) { + String valueStr = getRaw(ctx, userId); + + if (valueStr == null) { + return getDefaultValue(ctx); + } + + int value; + try { + value = Integer.parseInt(valueStr); + } catch (NumberFormatException e) { + e.printStackTrace(); + return getDefaultValue(ctx); + } + + if (!validateValue(value)) { + return getDefaultValue(ctx); + } + + return value; + } + + public final boolean put(@NonNull Context ctx, int val) { + if (!validateValue(val)) { + throw new IllegalArgumentException(Integer.toString(val)); + } + return putRaw(ctx, Integer.toString(val)); + } + + private void setDefaultValue(int val) { + if (!validateValue(val)) { + throw new IllegalStateException("invalid default value " + val); + } + defaultValue = val; + } + + private int getDefaultValue(Context ctx) { + ToIntFunction supplier = defaultValueSupplier; + if (supplier != null) { + setDefaultValue(supplier.applyAsInt(ctx)); + defaultValueSupplier = null; + } + return defaultValue; + } +} diff --git a/core/java/android/ext/settings/IntSysProperty.java b/core/java/android/ext/settings/IntSysProperty.java new file mode 100644 index 0000000000000..c8842ebb54557 --- /dev/null +++ b/core/java/android/ext/settings/IntSysProperty.java @@ -0,0 +1,39 @@ +package android.ext.settings; + +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.content.Context; +import android.os.UserHandle; + +import java.util.function.ToIntFunction; + +/** @hide */ +@SystemApi +public class IntSysProperty extends IntSetting { + + public IntSysProperty(@NonNull String key, int defaultValue) { + super(Scope.SYSTEM_PROPERTY, key, defaultValue); + } + + public IntSysProperty(@NonNull String key, int defaultValue, @NonNull int... validValues) { + super(Scope.SYSTEM_PROPERTY, key, defaultValue, validValues); + } + + public IntSysProperty(@NonNull String key, @NonNull ToIntFunction defaultValue) { + super(Scope.SYSTEM_PROPERTY, key, defaultValue); + } + + public IntSysProperty(@NonNull String key, @NonNull ToIntFunction defaultValue, @NonNull int... validValues) { + super(Scope.SYSTEM_PROPERTY, key, defaultValue, validValues); + } + + public int get() { + //noinspection DataFlowIssue + return super.get(null, UserHandle.USER_SYSTEM); + } + + public boolean put(int val) { + //noinspection DataFlowIssue + return super.put(null, val); + } +} diff --git a/core/java/android/ext/settings/NetworkLocationSettings.java b/core/java/android/ext/settings/NetworkLocationSettings.java new file mode 100644 index 0000000000000..80445c44d16ed --- /dev/null +++ b/core/java/android/ext/settings/NetworkLocationSettings.java @@ -0,0 +1,18 @@ +package android.ext.settings; + +import android.provider.Settings; + +/** @hide */ +public class NetworkLocationSettings { + + public static final int NETWORK_LOCATION_DISABLED = 0; + public static final int NETWORK_LOCATION_APPLE = 1; + public static final int NETWORK_LOCATION_GRAPHENEOS_APPLE_PROXY = 2; + public static final int NETWORK_LOCATION_APPLE_CHINA = 3; + + public static final IntSetting NETWORK_LOCATION_SETTING = new IntSetting( + Setting.Scope.GLOBAL, Settings.Global.NETWORK_LOCATION, + NETWORK_LOCATION_DISABLED, // default + NETWORK_LOCATION_APPLE_CHINA, NETWORK_LOCATION_GRAPHENEOS_APPLE_PROXY, NETWORK_LOCATION_APPLE, NETWORK_LOCATION_DISABLED // valid values + ); +} \ No newline at end of file diff --git a/core/java/android/ext/settings/RemoteKeyProvisioningSettings.java b/core/java/android/ext/settings/RemoteKeyProvisioningSettings.java new file mode 100644 index 0000000000000..69959118c414e --- /dev/null +++ b/core/java/android/ext/settings/RemoteKeyProvisioningSettings.java @@ -0,0 +1,37 @@ +package android.ext.settings; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.content.Context; +import android.provider.Settings; + +import static android.annotation.SystemApi.Client.MODULE_LIBRARIES; + +/** @hide */ +@SystemApi(client = MODULE_LIBRARIES) +public class RemoteKeyProvisioningSettings { + + public static final int GRAPHENEOS_PROXY = 0; + public static final int STANDARD_SERVER = 1; + + private static final String GRAPHENEOS_PROXY_URL = "https://remoteprovisioning.grapheneos.org/v1"; + + /** @hide */ + public static final IntSetting SERVER_SETTING = new IntSetting( + Setting.Scope.GLOBAL, Settings.Global.REMOTE_KEY_PROVISIONING_SERVER, + GRAPHENEOS_PROXY, // default + STANDARD_SERVER, GRAPHENEOS_PROXY // valid values + ); + + @Nullable + public static String getServerUrlOverride(@NonNull Context ctx) { + if (SERVER_SETTING.get(ctx) == GRAPHENEOS_PROXY) { + return GRAPHENEOS_PROXY_URL; + } + + return null; + } + + private RemoteKeyProvisioningSettings() {} +} diff --git a/core/java/android/ext/settings/Setting.java b/core/java/android/ext/settings/Setting.java new file mode 100644 index 0000000000000..236df65a6cf45 --- /dev/null +++ b/core/java/android/ext/settings/Setting.java @@ -0,0 +1,179 @@ +/* + * Copyright (C) 2022 GrapheneOS + * SPDX-License-Identifier: Apache-2.0 + */ + +package android.ext.settings; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.annotation.SystemApi; +import android.content.Context; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; +import android.os.SystemProperties; +import android.provider.Settings; +import android.util.Log; + +import java.util.function.Consumer; + +/** @hide */ +@SystemApi +public abstract class Setting { + + @SuppressLint("Enum") + public enum Scope { + SYSTEM_PROPERTY, // android.os.SystemProperties, doesn't support state observers + GLOBAL, // android.provider.Settings.Global + PER_USER, // android.provider.Settings.Secure + } + + private final Scope scope; + private final String key; + + /** @hide */ + protected Setting(Scope scope, String key) { + this.scope = scope; + this.key = key; + } + + @NonNull + public final String getKey() { + return key; + } + + @NonNull + public final Scope getScope() { + return scope; + } + + /** @hide */ + @Nullable + protected final String getRaw(Context ctx, int userId) { + try { + switch (scope) { + case SYSTEM_PROPERTY: { + String s = SystemProperties.get(key); + if (s.isEmpty()) { + return null; + } + return s; + } + case GLOBAL: + return Settings.Global.getString(ctx.getContentResolver(), key); + case PER_USER: + return Settings.Secure.getStringForUser(ctx.getContentResolver(), key, userId); + } + } catch (Throwable e) { + Log.e("ExtSettings", "key: " + key, e); + if (Settings.isInSystemServer()) { + // should never happen under normal circumstances, but if it does, + // don't crash the system_server + return null; + } + + throw e; + } + + // "switch (scope)" above should be exhaustive + throw new IllegalStateException(); + } + + /** @hide */ + protected final boolean putRaw(Context ctx, String val) { + switch (scope) { + case SYSTEM_PROPERTY: { + try { + SystemProperties.set(key, val); + return true; + } catch (RuntimeException e) { + e.printStackTrace(); + if (e instanceof IllegalArgumentException) { + // see doc + throw e; + } + return false; + } + } + case GLOBAL: + return Settings.Global.putString(ctx.getContentResolver(), key, val); + case PER_USER: + return Settings.Secure.putString(ctx.getContentResolver(), key, val); + default: + throw new IllegalStateException(); + } + } + + public final boolean canObserveState() { + return scope != Scope.SYSTEM_PROPERTY; + } + + // pass the return value to unregisterObserver() to remove the observer + @NonNull + public final Object registerObserver(@NonNull Context ctx, @NonNull Handler handler, @NonNull Consumer callback) { + return registerObserver(ctx, ctx.getUserId(), handler, callback); + } + + @NonNull + public final Object registerObserver(@NonNull Context ctx, int userId, @NonNull Handler handler, @NonNull Consumer callback) { + if (scope == Scope.SYSTEM_PROPERTY) { + // SystemProperties.addChangeCallback() doesn't work unless the change is actually + // reported elsewhere in the same process with SystemProperties.callChangeCallbacks() + // or with its native equivalent (report_sysprop_change()). + // Leave the code in place in case this changes in the future. + if (false) { + Runnable observer = new Runnable() { + private volatile String prev = SystemProperties.get(getKey()); + + @Override + public void run() { + String value = SystemProperties.get(getKey()); + // change callback is dispatched whenever any change to system props occurs + if (!prev.equals(value)) { + prev = value; + handler.post(() -> callback.accept((SelfType) Setting.this)); + } + } + }; + SystemProperties.addChangeCallback(observer); + return observer; + } + throw new UnsupportedOperationException("observing sysprop state is not supported"); + } + + Uri uri; + switch (scope) { + case GLOBAL: + uri = Settings.Global.getUriFor(key); + break; + case PER_USER: + uri = Settings.Secure.getUriFor(key); + break; + default: + throw new IllegalStateException(); + } + + ContentObserver observer = new ContentObserver(handler) { + @Override + public void onChange(boolean selfChange) { + callback.accept((SelfType) Setting.this); + } + }; + ctx.getContentResolver().registerContentObserver(uri, false, observer, userId); + + return observer; + } + + public final void unregisterObserver(@NonNull Context ctx, @NonNull Object observer) { + if (scope == Scope.SYSTEM_PROPERTY) { + if (false) { // see comment in registerObserverInner + SystemProperties.removeChangeCallback((Runnable) observer); + } + throw new UnsupportedOperationException("observing sysprop state is not supported"); + } else { + ctx.getContentResolver().unregisterContentObserver((ContentObserver) observer); + } + } +} diff --git a/core/java/android/ext/settings/StringSetting.java b/core/java/android/ext/settings/StringSetting.java new file mode 100644 index 0000000000000..46fdf5a4e21a6 --- /dev/null +++ b/core/java/android/ext/settings/StringSetting.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2022 GrapheneOS + * SPDX-License-Identifier: Apache-2.0 + */ + +package android.ext.settings; + +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.content.Context; + +import java.util.function.Function; + +/** @hide */ +@SystemApi +public class StringSetting extends Setting { + private String defaultValue; + private volatile Function defaultValueSupplier; + + public StringSetting(@NonNull Scope scope, @NonNull String key, @NonNull String defaultValue) { + super(scope, key); + setDefaultValue(defaultValue); + } + + public StringSetting(@NonNull Scope scope, @NonNull String key, @NonNull Function defaultValue) { + super(scope, key); + this.defaultValueSupplier = defaultValue; + } + + public boolean validateValue(@NonNull String val) { + return true; + } + + @NonNull + public final String get(@NonNull Context ctx) { + return get(ctx, ctx.getUserId()); + } + + @NonNull + // use only if this is a per-user setting and the context does not specify the userId + public final String get(@NonNull Context ctx, int userId) { + String s = getRaw(ctx, userId); + if (s == null || !validateValue(s)) { + return getDefaultValue(ctx); + } + return s; + } + + public final boolean put(@NonNull Context ctx, @NonNull String val) { + if (!validateValue(val)) { + throw new IllegalStateException("invalid value " + val); + } + return putRaw(ctx, val); + } + + private void setDefaultValue(@NonNull String val) { + if (!validateValue(val)) { + throw new IllegalStateException("invalid default value " + val); + } + defaultValue = val; + } + + @NonNull + private String getDefaultValue(Context ctx) { + Function supplier = defaultValueSupplier; + if (supplier != null) { + setDefaultValue(supplier.apply(ctx)); + defaultValueSupplier = null; + } + return defaultValue; + } +} diff --git a/core/java/android/ext/settings/StringSysProperty.java b/core/java/android/ext/settings/StringSysProperty.java new file mode 100644 index 0000000000000..c641426ca0c33 --- /dev/null +++ b/core/java/android/ext/settings/StringSysProperty.java @@ -0,0 +1,32 @@ +package android.ext.settings; + +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.content.Context; +import android.os.UserHandle; + +import java.util.function.Function; + +/** @hide */ +@SystemApi +public class StringSysProperty extends StringSetting { + + public StringSysProperty(@NonNull String key, @NonNull String defaultValue) { + super(Scope.SYSTEM_PROPERTY, key, defaultValue); + } + + public StringSysProperty(@NonNull String key, @NonNull Function defaultValue) { + super(Scope.SYSTEM_PROPERTY, key, defaultValue); + } + + @NonNull + public String get() { + //noinspection DataFlowIssue + return super.get(null, UserHandle.USER_SYSTEM); + } + + public boolean put(@NonNull String val) { + //noinspection DataFlowIssue + return super.put(null, val); + } +} diff --git a/core/java/android/ext/settings/UsbPortSecurity.java b/core/java/android/ext/settings/UsbPortSecurity.java new file mode 100644 index 0000000000000..d4a55db600381 --- /dev/null +++ b/core/java/android/ext/settings/UsbPortSecurity.java @@ -0,0 +1,20 @@ +package android.ext.settings; + +import android.os.Build; + +/** @hide */ +public class UsbPortSecurity { + public static final int MODE_ALL_PORTS_DISABLED = 0; + public static final int MODE_CHARGING_ONLY = 1; + // doesn't apply to connections that were made before locking + public static final int MODE_CHARGING_ONLY_WHEN_LOCKED = 2; + // doesn't apply to connections that were made before locking or first unlock + public static final int MODE_CHARGING_ONLY_WHEN_LOCKED_AFU = 3; + public static final int MODE_ALL_PORTS_ENABLED = 4; + + // keep in sync with USB HAL implementations that check this sysprop during init + public static final IntSysProperty MODE_SETTING = new IntSysProperty( + "persist.security.usb_mode", + // USB adb access is needed for debugging early boot failures + Build.IS_DEBUGGABLE ? MODE_ALL_PORTS_ENABLED : MODE_CHARGING_ONLY_WHEN_LOCKED); +} diff --git a/core/java/android/ext/settings/WidevineProvisioningSettings.java b/core/java/android/ext/settings/WidevineProvisioningSettings.java new file mode 100644 index 0000000000000..6d5ba2eaa69a1 --- /dev/null +++ b/core/java/android/ext/settings/WidevineProvisioningSettings.java @@ -0,0 +1,37 @@ +package android.ext.settings; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.content.Context; +import android.provider.Settings; + +import static android.annotation.SystemApi.Client.MODULE_LIBRARIES; + +/** @hide */ +@SystemApi(client = MODULE_LIBRARIES) +public class WidevineProvisioningSettings { + /** @hide */ + public static final int WV_GRAPHENEOS_PROXY = 0; + /** @hide */ + public static final int WV_STANDARD_SERVER = 1; + + private static final String WV_GRAPHENEOS_PROXY_HOSTNAME = "widevineprovisioning.grapheneos.org"; + + /** @hide */ + public static final IntSetting SERVER_SETTING = new IntSetting( + Setting.Scope.GLOBAL, Settings.Global.WIDEVINE_PROVISIONING_SERVER, + WV_GRAPHENEOS_PROXY, // default + WV_STANDARD_SERVER, WV_GRAPHENEOS_PROXY // valid values + ); + + @Nullable + public static String getServerHostnameOverride(@NonNull Context ctx) { + if (SERVER_SETTING.get(ctx) == WV_GRAPHENEOS_PROXY) { + return WV_GRAPHENEOS_PROXY_HOSTNAME; + } + return null; + } + + private WidevineProvisioningSettings() {} +} diff --git a/core/java/android/ext/settings/app/AppSwitch.java b/core/java/android/ext/settings/app/AppSwitch.java new file mode 100644 index 0000000000000..989c268bf4c43 --- /dev/null +++ b/core/java/android/ext/settings/app/AppSwitch.java @@ -0,0 +1,175 @@ +package android.ext.settings.app; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.GosPackageState; +import android.content.pm.GosPackageStateFlag; +import android.ext.AppInfoExt; +import android.ext.settings.ExtSettings; + +/** @hide */ +public abstract class AppSwitch { + // optional GosPackageState flag that indicates that non-default value is set, ignored if 0 + @GosPackageStateFlag.Enum int gosPsFlagNonDefault; + // GosPackageState flag that indicates whether the switch is on or off, ignored if + // non-default flag is not set + @GosPackageStateFlag.Enum int gosPsFlag; + // invert meaning of gosPsFlag, i.e. true is off, false is on + boolean gosPsFlagInverted; + // optional GosPackageState flag to suppress switch-related notification (e.g. after + // "Don't show again" notification action) + @GosPackageStateFlag.Enum int gosPsFlagSuppressNotif; + + int compatChangeToDisableHardening = -1; + + // immutability reasons + public static final int IR_UNKNOWN = 0; + public static final int IR_IS_SYSTEM_APP = 1; + public static final int IR_NO_NATIVE_CODE = 2; + public static final int IR_NON_64_BIT_NATIVE_CODE = 3; + public static final int IR_OPTED_IN_VIA_MANIFEST = 4; + public static final int IR_IS_DEBUGGABLE_APP = 5; + public static final int IR_EXPLOIT_PROTECTION_COMPAT_MODE = 6; + public static final int IR_REQUIRED_BY_HARDENED_MALLOC = 7; + public static final int IR_REQUIRED_BY_ZYGOTE_SPAWNING = 8; + + // default value reasons + public static final int DVR_UNKNOWN = 0; + public static final int DVR_DEFAULT_SETTING = 1; + public static final int DVR_APP_COMPAT_CONFIG_HARDENING_OPT_IN = 2; + public static final int DVR_APP_COMPAT_CONFIG_HARDENING_OPT_OUT = 3; + public static final int DVR_APP_IS_CLIENT_OF_GMSCORE = 4; + + public static class StateInfo { + // use it only if StateInfo is not needed, it's not thread-safe to read from this variable + static final StateInfo PLACEHOLDER = new StateInfo(); + + boolean isImmutable; + int immutabilityReason = IR_UNKNOWN; + + boolean isUsingDefaultValue; + int defaultValueReason = DVR_UNKNOWN; + + public boolean isImmutable() { + return isImmutable; + } + + public int getImmutabilityReason() { + return immutabilityReason; + } + + public boolean isUsingDefaultValue() { + return isUsingDefaultValue; + } + + public int getDefaultValueReason() { + return defaultValueReason; + } + } + + public final boolean isImmutable(Context ctx, int userId, ApplicationInfo appInfo, + GosPackageState ps) { + return getImmutableValue(ctx, userId, appInfo, ps) != null; + } + + public final Boolean getImmutableValue(Context ctx, int userId, ApplicationInfo appInfo, + GosPackageState ps) { + return getImmutableValue(ctx, userId, appInfo, ps, StateInfo.PLACEHOLDER); + } + + // returns null if value is currently mutable + public Boolean getImmutableValue(Context ctx, int userId, ApplicationInfo appInfo, + GosPackageState ps, StateInfo si) { + return null; + } + + public final boolean getDefaultValue(Context ctx, int userId, ApplicationInfo appInfo, + GosPackageState ps) { + return getDefaultValue(ctx, userId, appInfo, ps, StateInfo.PLACEHOLDER); + } + + public final boolean getDefaultValue(Context ctx, int userId, ApplicationInfo appInfo, + GosPackageState ps, StateInfo si) { + int compatChangeForOff = this.compatChangeToDisableHardening; + if (compatChangeForOff >= 0) { + AppInfoExt aie = appInfo.ext(); + if (aie.hasCompatConfig()) { + boolean res = !aie.hasCompatChange(compatChangeForOff); + if (res || ExtSettings.ALLOW_DISABLING_HARDENING_VIA_APP_COMPAT_CONFIG.get(ctx, userId)) { + si.defaultValueReason = res ? + DVR_APP_COMPAT_CONFIG_HARDENING_OPT_IN : DVR_APP_COMPAT_CONFIG_HARDENING_OPT_OUT; + return res; + } + } + } + + return getDefaultValueInner(ctx, userId, appInfo, ps, si); + } + + protected abstract boolean getDefaultValueInner(Context ctx, int userId, ApplicationInfo appInfo, + GosPackageState ps, StateInfo si); + + + public final boolean get(Context ctx, int userId, ApplicationInfo appInfo, + GosPackageState ps) { + return get(ctx, userId, appInfo, ps, StateInfo.PLACEHOLDER); + } + + public final boolean get(Context ctx, int userId, ApplicationInfo appInfo, + GosPackageState ps, StateInfo si) { + Boolean immValue = getImmutableValue(ctx, userId, appInfo, ps, si); + + boolean res; + if (immValue != null) { + si.isImmutable = true; + res = immValue.booleanValue(); + } else if (isUsingDefaultValue(ps)) { + si.isUsingDefaultValue = true; + res = getDefaultValue(ctx, userId, appInfo, ps, si); + } else { + res = ps.hasFlag(gosPsFlag); + if (gosPsFlagInverted) { + res = !res; + } + } + + return res; + } + + public final void set(GosPackageState.Editor ed, boolean on) { + if (gosPsFlagNonDefault != 0) { + ed.addFlag(gosPsFlagNonDefault); + } + + if (gosPsFlagInverted) { + ed.setFlagState(gosPsFlag, !on); + } else { + ed.setFlagState(gosPsFlag, on); + } + } + + private boolean isUsingDefaultValue(GosPackageState ps) { + return gosPsFlagNonDefault != 0 && !ps.hasFlag(gosPsFlagNonDefault); + } + + public final void setUseDefaultValue(GosPackageState.Editor ed) { + ed.clearFlag(gosPsFlagNonDefault); + ed.clearFlag(gosPsFlag); + } + + public final boolean hasNotification() { + return gosPsFlagSuppressNotif != 0; + } + + public final boolean isNotificationEnabled(GosPackageState ps) { + int flag = gosPsFlagSuppressNotif; + if (flag == 0) { + return false; + } + return !ps.hasFlag(flag); + } + + public final void setNotificationEnabled(GosPackageState.Editor ed, boolean enabled) { + ed.setFlagState(gosPsFlagSuppressNotif, !enabled); + } +} diff --git a/core/java/android/ext/settings/app/AswBlockPlayIntegrityApi.java b/core/java/android/ext/settings/app/AswBlockPlayIntegrityApi.java new file mode 100644 index 0000000000000..bf20b252416b7 --- /dev/null +++ b/core/java/android/ext/settings/app/AswBlockPlayIntegrityApi.java @@ -0,0 +1,21 @@ +package android.ext.settings.app; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.GosPackageState; +import android.content.pm.GosPackageStateFlag; + +/** @hide */ +public class AswBlockPlayIntegrityApi extends AppSwitch { + public static final AswBlockPlayIntegrityApi I = new AswBlockPlayIntegrityApi(); + + private AswBlockPlayIntegrityApi() { + gosPsFlag = GosPackageStateFlag.BLOCK_PLAY_INTEGRITY_API; + gosPsFlagSuppressNotif = GosPackageStateFlag.SUPPRESS_PLAY_INTEGRITY_API_NOTIF; + } + + @Override + protected boolean getDefaultValueInner(Context ctx, int userId, ApplicationInfo appInfo, GosPackageState ps, StateInfo si) { + return false; + } +} diff --git a/core/java/android/ext/settings/app/AswDenyNativeDebug.java b/core/java/android/ext/settings/app/AswDenyNativeDebug.java new file mode 100644 index 0000000000000..0e2b1fce51988 --- /dev/null +++ b/core/java/android/ext/settings/app/AswDenyNativeDebug.java @@ -0,0 +1,49 @@ +package android.ext.settings.app; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.GosPackageState; +import android.content.pm.GosPackageStateFlag; +import android.ext.settings.ExtSettings; + +import com.android.internal.os.SELinuxFlags; +import com.android.server.os.nano.AppCompatProtos; + +/** @hide */ +public class AswDenyNativeDebug extends AppSwitch { + public static final AswDenyNativeDebug I = new AswDenyNativeDebug(); + + private AswDenyNativeDebug() { + gosPsFlagNonDefault = GosPackageStateFlag.BLOCK_NATIVE_DEBUGGING_NON_DEFAULT; + gosPsFlag = GosPackageStateFlag.BLOCK_NATIVE_DEBUGGING; + gosPsFlagSuppressNotif = GosPackageStateFlag.BLOCK_NATIVE_DEBUGGING_SUPPRESS_NOTIF; + compatChangeToDisableHardening = AppCompatProtos.ALLOW_NATIVE_DEBUGGING; + } + + @Override + public Boolean getImmutableValue(Context ctx, int userId, ApplicationInfo appInfo, + GosPackageState ps, StateInfo si) { + if (appInfo.isSystemApp() && !SELinuxFlags.isSystemAppSepolicyWeakeningAllowed()) { + si.immutabilityReason = IR_IS_SYSTEM_APP; + return true; + } + + if (ps.hasFlag(GosPackageStateFlag.ENABLE_EXPLOIT_PROTECTION_COMPAT_MODE)) { + si.immutabilityReason = IR_EXPLOIT_PROTECTION_COMPAT_MODE; + return false; + } + + return null; + } + + @Override + protected boolean getDefaultValueInner(Context ctx, int userId, ApplicationInfo appInfo, + GosPackageState ps, StateInfo si) { + if (appInfo.isSystemApp()) { + return true; + } + + si.defaultValueReason = DVR_DEFAULT_SETTING; + return !ExtSettings.ALLOW_NATIVE_DEBUG_BY_DEFAULT.get(ctx); + } +} diff --git a/core/java/android/ext/settings/app/AswRestrictMemoryDynCodeLoading.java b/core/java/android/ext/settings/app/AswRestrictMemoryDynCodeLoading.java new file mode 100644 index 0000000000000..9ee310df68397 --- /dev/null +++ b/core/java/android/ext/settings/app/AswRestrictMemoryDynCodeLoading.java @@ -0,0 +1,70 @@ +package android.ext.settings.app; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.GosPackageState; +import android.content.pm.GosPackageStateFlag; +import android.ext.settings.ExtSettings; +import android.util.ArraySet; + +import com.android.internal.R; +import com.android.internal.os.SELinuxFlags; +import com.android.server.os.nano.AppCompatProtos; + +/** @hide */ +public class AswRestrictMemoryDynCodeLoading extends AppSwitch { + public static final AswRestrictMemoryDynCodeLoading I = new AswRestrictMemoryDynCodeLoading(); + + private AswRestrictMemoryDynCodeLoading() { + gosPsFlagNonDefault = GosPackageStateFlag.RESTRICT_MEMORY_DYN_CODE_LOADING_NON_DEFAULT; + gosPsFlag = GosPackageStateFlag.RESTRICT_MEMORY_DYN_CODE_LOADING; + gosPsFlagSuppressNotif = GosPackageStateFlag.RESTRICT_MEMORY_DYN_CODE_LOADING_SUPPRESS_NOTIF; + compatChangeToDisableHardening = AppCompatProtos.ALLOW_MEMORY_DYN_CODE_LOADING; + } + + private static volatile ArraySet allowedSystemPkgs; + + private static boolean shouldAllowByDefaultToSystemPkg(Context ctx, String pkg) { + var set = allowedSystemPkgs; + if (set == null) { + set = new ArraySet<>(ctx.getResources() + .getStringArray(R.array.system_pkgs_allowed_memory_dyn_code_loading_by_default)); + allowedSystemPkgs = set; + } + return set.contains(pkg); + } + + @Override + public Boolean getImmutableValue(Context ctx, int userId, ApplicationInfo appInfo, + GosPackageState ps, StateInfo si) { + if (appInfo.isSystemApp()) { + if (shouldAllowByDefaultToSystemPkg(ctx, appInfo.packageName)) { + // allow manual restriction + return null; + } + if (SELinuxFlags.isSystemAppSepolicyWeakeningAllowed()) { + return null; + } + si.immutabilityReason = IR_IS_SYSTEM_APP; + return true; + } + + if (ps.hasFlag(GosPackageStateFlag.ENABLE_EXPLOIT_PROTECTION_COMPAT_MODE)) { + si.immutabilityReason = IR_EXPLOIT_PROTECTION_COMPAT_MODE; + return false; + } + + return null; + } + + @Override + protected boolean getDefaultValueInner(Context ctx, int userId, ApplicationInfo appInfo, + GosPackageState ps, StateInfo si) { + if (appInfo.isSystemApp()) { + return !shouldAllowByDefaultToSystemPkg(ctx, appInfo.packageName); + } else { + si.defaultValueReason = DVR_DEFAULT_SETTING; + return ExtSettings.RESTRICT_MEMORY_DYN_CODE_LOADING_BY_DEFAULT.get(ctx, userId); + } + } +} diff --git a/core/java/android/ext/settings/app/AswRestrictStorageDynCodeLoading.java b/core/java/android/ext/settings/app/AswRestrictStorageDynCodeLoading.java new file mode 100644 index 0000000000000..656c07ecb937e --- /dev/null +++ b/core/java/android/ext/settings/app/AswRestrictStorageDynCodeLoading.java @@ -0,0 +1,91 @@ +package android.ext.settings.app; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.GosPackageState; +import android.content.pm.GosPackageStateFlag; +import android.content.pm.PackageManager; +import android.ext.AppInfoExtFlag; +import android.ext.PackageId; +import android.ext.settings.ExtSettings; +import android.util.ArraySet; + +import com.android.internal.R; +import com.android.internal.os.SELinuxFlags; +import com.android.server.os.nano.AppCompatProtos; + +/** @hide */ +public class AswRestrictStorageDynCodeLoading extends AppSwitch { + public static final AswRestrictStorageDynCodeLoading I = new AswRestrictStorageDynCodeLoading(); + + private AswRestrictStorageDynCodeLoading() { + gosPsFlagNonDefault = GosPackageStateFlag.RESTRICT_STORAGE_DYN_CODE_LOADING_NON_DEFAULT; + gosPsFlag = GosPackageStateFlag.RESTRICT_STORAGE_DYN_CODE_LOADING; + gosPsFlagSuppressNotif = GosPackageStateFlag.RESTRICT_STORAGE_DYN_CODE_LOADING_SUPPRESS_NOTIF; + compatChangeToDisableHardening = AppCompatProtos.ALLOW_STORAGE_DYN_CODE_LOADING; + } + + private static volatile ArraySet allowedSystemPkgs; + + private static boolean shouldAllowByDefaultToSystemPkg(Context ctx, String pkg) { + var set = allowedSystemPkgs; + if (set == null) { + set = new ArraySet<>(ctx.getResources() + .getStringArray(R.array.system_pkgs_allowed_storage_dyn_code_loading_by_default)); + allowedSystemPkgs = set; + } + return set.contains(pkg); + } + + @Override + public Boolean getImmutableValue(Context ctx, int userId, ApplicationInfo appInfo, + GosPackageState ps, StateInfo si) { + if (appInfo.isSystemApp()) { + if (shouldAllowByDefaultToSystemPkg(ctx, appInfo.packageName)) { + // allow manual restriction + return null; + } + if (SELinuxFlags.isSystemAppSepolicyWeakeningAllowed()) { + return null; + } + si.immutabilityReason = IR_IS_SYSTEM_APP; + return true; + } + + if (ps.hasFlag(GosPackageStateFlag.ENABLE_EXPLOIT_PROTECTION_COMPAT_MODE)) { + si.immutabilityReason = IR_EXPLOIT_PROTECTION_COMPAT_MODE; + return false; + } + + return null; + } + + public static boolean isGmsCoreInstalled(Context ctx, int userId) { + PackageManager pm = ctx.getPackageManager(); + try { + ApplicationInfo info = pm.getApplicationInfoAsUser(PackageId.GMS_CORE_NAME, 0, userId); + return info.ext().getPackageId() == PackageId.GMS_CORE; + } catch (PackageManager.NameNotFoundException ignored) { + return false; + } + } + + @Override + protected boolean getDefaultValueInner(Context ctx, int userId, ApplicationInfo appInfo, + GosPackageState ps, StateInfo si) { + if (appInfo.isSystemApp()) { + return !shouldAllowByDefaultToSystemPkg(ctx, appInfo.packageName); + } else { + if (appInfo.ext().hasFlag(AppInfoExtFlag.HAS_GMSCORE_CLIENT_LIBRARY)) { + if (isGmsCoreInstalled(ctx, userId)) { + si.defaultValueReason = DVR_APP_IS_CLIENT_OF_GMSCORE; + // Dynamite modules are loaded from writable data storage of GmsCore + return false; + } + } + + si.defaultValueReason = DVR_DEFAULT_SETTING; + return ExtSettings.RESTRICT_STORAGE_DYN_CODE_LOADING_BY_DEFAULT.get(ctx, userId); + } + } +} diff --git a/core/java/android/ext/settings/app/AswRestrictWebViewDynCodeLoading.java b/core/java/android/ext/settings/app/AswRestrictWebViewDynCodeLoading.java new file mode 100644 index 0000000000000..8ef413de96b64 --- /dev/null +++ b/core/java/android/ext/settings/app/AswRestrictWebViewDynCodeLoading.java @@ -0,0 +1,61 @@ +package android.ext.settings.app; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.GosPackageState; +import android.content.pm.GosPackageStateFlag; +import android.ext.settings.ExtSettings; +import android.util.ArraySet; + +import com.android.internal.R; +import com.android.internal.os.SELinuxFlags; + +/** @hide */ +public class AswRestrictWebViewDynCodeLoading extends AppSwitch { + public static final AswRestrictWebViewDynCodeLoading I = new AswRestrictWebViewDynCodeLoading(); + + private AswRestrictWebViewDynCodeLoading() { + gosPsFlagNonDefault = GosPackageStateFlag.RESTRICT_WEBVIEW_DYN_CODE_LOADING_NON_DEFAULT; + gosPsFlag = GosPackageStateFlag.RESTRICT_WEBVIEW_DYN_CODE_LOADING; + } + + private static volatile ArraySet allowedSystemPkgs; + + private static boolean shouldAllowByDefaultToSystemPackage(Context ctx, String pkg) { + ArraySet set = allowedSystemPkgs; + if (set == null) { + set = new ArraySet<>(ctx.getResources() + .getStringArray(R.array.system_pkgs_allowed_webview_dyn_code_loading_by_default)); + allowedSystemPkgs = set; + } + return set.contains(pkg); + } + + @Override + public Boolean getImmutableValue(Context ctx, int userId, ApplicationInfo appInfo, + GosPackageState ps, StateInfo si) { + if (appInfo.isSystemApp()) { + if (shouldAllowByDefaultToSystemPackage(ctx, appInfo.packageName)) { + // allow manual restriction + return null; + } else { + if (SELinuxFlags.isSystemAppSepolicyWeakeningAllowed()) { + return null; + } + return true; + } + } + + return null; + } + + @Override + protected boolean getDefaultValueInner(Context ctx, int userId, ApplicationInfo appInfo, + GosPackageState ps, StateInfo si) { + if (appInfo.isSystemApp()) { + return !shouldAllowByDefaultToSystemPackage(ctx, appInfo.packageName); + } else { + return ExtSettings.RESTRICT_WEBVIEW_DYN_CODE_LOADING_BY_DEFAULT.get(ctx, userId); + } + } +} diff --git a/core/java/android/ext/settings/app/AswUseExecSpawning.java b/core/java/android/ext/settings/app/AswUseExecSpawning.java new file mode 100644 index 0000000000000..7935fd2536952 --- /dev/null +++ b/core/java/android/ext/settings/app/AswUseExecSpawning.java @@ -0,0 +1,37 @@ +package android.ext.settings.app; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.GosPackageState; +import android.content.pm.GosPackageStateFlag; +import android.ext.settings.ExtSettings; + +import com.android.server.os.nano.AppCompatProtos; + +/** @hide */ +public class AswUseExecSpawning extends AppSwitch { + public static final AswUseExecSpawning I = new AswUseExecSpawning(); + + private AswUseExecSpawning() { + gosPsFlag = GosPackageStateFlag.USE_EXEC_SPAWNING; + gosPsFlagNonDefault = GosPackageStateFlag.USE_EXEC_SPAWNING_NON_DEFAULT; + compatChangeToDisableHardening = AppCompatProtos.USE_ZYGOTE_SPAWNING; + } + + @Override + public Boolean getImmutableValue(Context ctx, int userId, ApplicationInfo appInfo, + GosPackageState ps, StateInfo si) { + if (ps.hasFlag(GosPackageStateFlag.ENABLE_EXPLOIT_PROTECTION_COMPAT_MODE)) { + si.immutabilityReason = IR_EXPLOIT_PROTECTION_COMPAT_MODE; + return Boolean.FALSE; + } + + return null; + } + + @Override + protected boolean getDefaultValueInner(Context ctx, int userId, ApplicationInfo appInfo, + GosPackageState ps, StateInfo si) { + return ExtSettings.EXEC_SPAWNING.get(); + } +} diff --git a/core/java/android/ext/settings/app/AswUseExtendedVaSpace.java b/core/java/android/ext/settings/app/AswUseExtendedVaSpace.java new file mode 100644 index 0000000000000..3b1e07327a5e8 --- /dev/null +++ b/core/java/android/ext/settings/app/AswUseExtendedVaSpace.java @@ -0,0 +1,80 @@ +package android.ext.settings.app; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.GosPackageState; +import android.content.pm.GosPackageStateFlag; +import android.os.Build; + +import com.android.server.os.nano.AppCompatProtos; + +import java.util.Objects; + +import dalvik.system.VMRuntime; + +/** @hide */ +public class AswUseExtendedVaSpace extends AppSwitch { + public static final AswUseExtendedVaSpace I = new AswUseExtendedVaSpace(); + + private AswUseExtendedVaSpace() { + gosPsFlag = GosPackageStateFlag.USE_EXTENDED_VA_SPACE; + gosPsFlagNonDefault = GosPackageStateFlag.USE_EXTENDED_VA_SPACE_NON_DEFAULT; + compatChangeToDisableHardening = AppCompatProtos.DISABLE_EXTENDED_VA_SPACE; + } + + @Override + public Boolean getImmutableValue(Context ctx, int userId, ApplicationInfo appInfo, + GosPackageState ps, StateInfo si) { + if (AswUseHardenedMalloc.I.get(ctx, userId, appInfo, ps)) { + si.immutabilityReason = IR_REQUIRED_BY_HARDENED_MALLOC; + return Boolean.TRUE; + } + + String primaryAbi = appInfo.primaryCpuAbi; + if (primaryAbi != null) { + String isa = Objects.requireNonNull(VMRuntime.getInstructionSet(primaryAbi)); + if (!VMRuntime.is64BitInstructionSet(isa)) { + si.immutabilityReason = IR_NON_64_BIT_NATIVE_CODE; + return Boolean.FALSE; + } + if (!isAvailable(isa)) { + return Boolean.TRUE; + } + } + + if (!isAvailable()) { + return Boolean.TRUE; + } + + if (!AswUseExecSpawning.I.get(ctx, userId, appInfo, ps)) { + // When zygote spawning is used, extended VA space can't be used without also using + // hardened_malloc. This is a consequence of having 2 zygotes: + // - primary zygote with hardened_malloc and extended VA space + // - compat zygote with scudo and, on arm64, 39-bit VA space + si.immutabilityReason = IR_REQUIRED_BY_ZYGOTE_SPAWNING; + return Boolean.FALSE; + } + + if (ps.hasFlag(GosPackageStateFlag.ENABLE_EXPLOIT_PROTECTION_COMPAT_MODE)) { + si.immutabilityReason = IR_EXPLOIT_PROTECTION_COMPAT_MODE; + return Boolean.FALSE; + } + + return null; + } + + public static boolean isAvailable() { + return isAvailable(VMRuntime.getInstructionSet(Build.SUPPORTED_ABIS[0])); + } + + public static boolean isAvailable(String isa) { + // disabling extended VA space is supported only on arm64 + return isa.equals("arm64"); + } + + @Override + protected boolean getDefaultValueInner(Context ctx, int userId, ApplicationInfo appInfo, + GosPackageState ps, StateInfo si) { + return true; + } +} diff --git a/core/java/android/ext/settings/app/AswUseHardenedMalloc.java b/core/java/android/ext/settings/app/AswUseHardenedMalloc.java new file mode 100644 index 0000000000000..f219cf31b5694 --- /dev/null +++ b/core/java/android/ext/settings/app/AswUseHardenedMalloc.java @@ -0,0 +1,55 @@ +package android.ext.settings.app; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.GosPackageState; +import android.content.pm.GosPackageStateFlag; + +import com.android.server.os.nano.AppCompatProtos; + +import dalvik.system.VMRuntime; + +/** @hide */ +public class AswUseHardenedMalloc extends AppSwitch { + public static final AswUseHardenedMalloc I = new AswUseHardenedMalloc(); + + private AswUseHardenedMalloc() { + gosPsFlag = GosPackageStateFlag.USE_HARDENED_MALLOC; + gosPsFlagNonDefault = GosPackageStateFlag.USE_HARDENED_MALLOC_NON_DEFAULT; + compatChangeToDisableHardening = AppCompatProtos.DISABLE_HARDENED_MALLOC; + } + + @Override + public Boolean getImmutableValue(Context ctx, int userId, ApplicationInfo appInfo, + GosPackageState ps, StateInfo si) { + String primaryAbi = appInfo.primaryCpuAbi; + if (primaryAbi == null) { + si.immutabilityReason = IR_NO_NATIVE_CODE; + return true; + } + + if (!VMRuntime.is64BitAbi(primaryAbi)) { + // hardened_malloc is 64-bit only + si.immutabilityReason = IR_NON_64_BIT_NATIVE_CODE; + return false; + } + + if (appInfo.isSystemApp()) { + si.immutabilityReason = IR_IS_SYSTEM_APP; + return true; + } + + if (ps.hasFlag(GosPackageStateFlag.ENABLE_EXPLOIT_PROTECTION_COMPAT_MODE)) { + si.immutabilityReason = IR_EXPLOIT_PROTECTION_COMPAT_MODE; + return false; + } + + return null; + } + + @Override + protected boolean getDefaultValueInner(Context ctx, int userId, ApplicationInfo appInfo, + GosPackageState ps, StateInfo si) { + return true; + } +} diff --git a/core/java/android/ext/settings/app/AswUseMemoryTagging.java b/core/java/android/ext/settings/app/AswUseMemoryTagging.java new file mode 100644 index 0000000000000..1201529385423 --- /dev/null +++ b/core/java/android/ext/settings/app/AswUseMemoryTagging.java @@ -0,0 +1,68 @@ +package android.ext.settings.app; + +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.GosPackageState; +import android.content.pm.GosPackageStateFlag; +import android.ext.PackageId; +import android.ext.settings.ExtSettings; + +import com.android.server.os.nano.AppCompatProtos; + +import dalvik.system.VMRuntime; + +/** @hide */ +public class AswUseMemoryTagging extends AppSwitch { + public static final AswUseMemoryTagging I = new AswUseMemoryTagging(); + + private AswUseMemoryTagging() { + gosPsFlag = GosPackageStateFlag.FORCE_MEMTAG; + gosPsFlagNonDefault = GosPackageStateFlag.FORCE_MEMTAG_NON_DEFAULT; + gosPsFlagSuppressNotif = GosPackageStateFlag.FORCE_MEMTAG_SUPPRESS_NOTIF; + compatChangeToDisableHardening = AppCompatProtos.DISABLE_MEMORY_TAGGING; + } + + @Override + public Boolean getImmutableValue(Context ctx, int userId, ApplicationInfo appInfo, + GosPackageState ps, StateInfo si) { + final String primaryAbi = appInfo.primaryCpuAbi; + if (primaryAbi == null) { + si.immutabilityReason = IR_NO_NATIVE_CODE; + return true; + } + + if (!VMRuntime.is64BitAbi(primaryAbi)) { + si.immutabilityReason = IR_NON_64_BIT_NATIVE_CODE; + return false; + } + + if (appInfo.isSystemApp()) { + switch (appInfo.packageName) { + case PackageId.PIXEL_CAMERA_SERVICES_NAME: + return false; + } + si.immutabilityReason = IR_IS_SYSTEM_APP; + return true; + } + + int mm = appInfo.getMemtagMode(); + if (mm == ApplicationInfo.MEMTAG_ASYNC || mm == ApplicationInfo.MEMTAG_SYNC) { + si.immutabilityReason = IR_OPTED_IN_VIA_MANIFEST; + return true; + } + + if (ps.hasFlag(GosPackageStateFlag.ENABLE_EXPLOIT_PROTECTION_COMPAT_MODE)) { + si.immutabilityReason = IR_EXPLOIT_PROTECTION_COMPAT_MODE; + return false; + } + + return null; + } + + @Override + protected boolean getDefaultValueInner(Context ctx, int userId, ApplicationInfo appInfo, + GosPackageState ps, StateInfo si) { + si.defaultValueReason = DVR_DEFAULT_SETTING; + return ExtSettings.FORCE_APP_MEMTAG_BY_DEFAULT.get(ctx, userId); + } +} diff --git a/core/java/android/hardware/biometrics/BiometricPrompt.java b/core/java/android/hardware/biometrics/BiometricPrompt.java index 6aa980a4d397c..e556bb62aed4e 100644 --- a/core/java/android/hardware/biometrics/BiometricPrompt.java +++ b/core/java/android/hardware/biometrics/BiometricPrompt.java @@ -36,6 +36,7 @@ import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.TestApi; +import android.app.compat.gms.GmsCompat; import android.content.ComponentName; import android.content.Context; import android.content.DialogInterface; @@ -257,6 +258,12 @@ public BiometricPrompt.Builder setLogoRes(@DrawableRes int logoRes) { "Exclusively one of logo resource or logo bitmap can be set"); } if (logoRes != 0) { + if (GmsCompat.isEnabled()) { + if (!GmsCompat.hasPermission(SET_BIOMETRIC_DIALOG_ADVANCED)) { + return this; + } + } + mPromptInfo.setLogo(logoRes, convertDrawableToBitmap(mContext.getDrawable(logoRes))); } @@ -281,6 +288,11 @@ public BiometricPrompt.Builder setLogoBitmap(@NonNull Bitmap logoBitmap) { throw new IllegalStateException( "Exclusively one of logo resource or logo bitmap can be set"); } + if (GmsCompat.isEnabled()) { + if (!GmsCompat.hasPermission(SET_BIOMETRIC_DIALOG_ADVANCED)) { + return this; + } + } mPromptInfo.setLogo(0, logoBitmap); return this; } @@ -308,6 +320,11 @@ public BiometricPrompt.Builder setLogoDescription(@NonNull String logoDescriptio "Logo description passed in exceeds" + MAX_LOGO_DESCRIPTION_CHARACTER_NUMBER + " character number and may be truncated."); } + if (GmsCompat.isEnabled()) { + if (!GmsCompat.hasPermission(SET_BIOMETRIC_DIALOG_ADVANCED)) { + return this; + } + } mPromptInfo.setLogoDescription(logoDescription); return this; } diff --git a/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java b/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java index 9f8d4656fc55c..2a14c6bd48d92 100644 --- a/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java +++ b/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java @@ -25,6 +25,7 @@ import android.content.ContentResolver; import android.content.Context; import android.content.Intent; +import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.content.ServiceConnection; import android.graphics.ImageFormat; @@ -289,6 +290,16 @@ public static CameraExtensionManagerGlobal get() { return GLOBAL_CAMERA_MANAGER; } + private static boolean validateVendorCameraExtensionsPackage(Context ctx, String pkgName) { + try { + ApplicationInfo ai = ctx.getPackageManager().getApplicationInfo(pkgName, 0); + return ai.isSystemApp(); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + return false; + } + } + private void releaseProxyConnectionLocked(Context context, int extension) { Context ctx = context.getApplicationContext(); if (mConnectionManager.getConnection(extension) != null) { @@ -316,7 +327,8 @@ private void connectToProxyLocked(Context context, int extension, boolean useFal vendorProxyService = efvProxyService; } } - if (!vendorProxyPackage.isEmpty() && !vendorProxyService.isEmpty()) { + if (!vendorProxyPackage.isEmpty() && !vendorProxyService.isEmpty() + && validateVendorCameraExtensionsPackage(ctx, vendorProxyPackage)) { Log.v(TAG, "Choosing the vendor camera extensions proxy package: " + vendorProxyPackage); diff --git a/core/java/android/hardware/display/DisplayManagerGlobal.java b/core/java/android/hardware/display/DisplayManagerGlobal.java index b931cd4a46d67..46d4ccbcb8407 100644 --- a/core/java/android/hardware/display/DisplayManagerGlobal.java +++ b/core/java/android/hardware/display/DisplayManagerGlobal.java @@ -273,6 +273,12 @@ public static DisplayManagerGlobal getInstance() { } } + public static void clearCachedInstance() { + synchronized (DisplayManagerGlobal.class) { + sInstance = null; + } + } + /** * Get information about a particular logical display. * diff --git a/core/java/android/hardware/fingerprint/FingerprintManager.java b/core/java/android/hardware/fingerprint/FingerprintManager.java index 8007eb3e865c9..198e24e50cc30 100644 --- a/core/java/android/hardware/fingerprint/FingerprintManager.java +++ b/core/java/android/hardware/fingerprint/FingerprintManager.java @@ -33,7 +33,9 @@ import static com.android.internal.util.FrameworkStatsLog.AUTH_DEPRECATED_APIUSED__DEPRECATED_API__API_FINGERPRINT_MANAGER_AUTHENTICATE; import static com.android.internal.util.FrameworkStatsLog.AUTH_DEPRECATED_APIUSED__DEPRECATED_API__API_FINGERPRINT_MANAGER_HAS_ENROLLED_FINGERPRINTS; import static com.android.internal.util.FrameworkStatsLog.AUTH_DEPRECATED_APIUSED__DEPRECATED_API__API_FINGERPRINT_MANAGER_IS_HARDWARE_DETECTED; +import static java.util.Objects.requireNonNull; +import android.annotation.CheckResult; import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; @@ -681,6 +683,45 @@ public void authenticate(@Nullable CryptoObject crypto, @Nullable CancellationSi } } + /** @hide */ public static final int SUCCESS = 0; + /** @hide */ public static final int ERROR_NO_PENDING_AUTH_TOKEN = 1; + /** @hide */ public static final int ERROR_UNABLE_TO_ADD_AUTH_TOKEN_TO_KEYSTORE = 2; + + /** @hide */ + @IntDef({SUCCESS, ERROR_NO_PENDING_AUTH_TOKEN, ERROR_UNABLE_TO_ADD_AUTH_TOKEN_TO_KEYSTORE}) + @Retention(RetentionPolicy.SOURCE) + public @interface AddPendingAuthTokenResult {} + + /** + * Add a pending hardware auth token to KeyStore. This should only be called after biometric + * second factor has succeeded. + * @hide + */ + @RequiresPermission(USE_BIOMETRIC_INTERNAL) + @CheckResult + public @AddPendingAuthTokenResult int addPendingAuthTokenToKeyStore(final int userId) { + IFingerprintService service = requireNonNull(mService, "mService"); + try { + return service.addPendingAuthTokenToKeyStore(userId); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Clear all pending auth tokens. + * @hide + */ + @RequiresPermission(USE_BIOMETRIC_INTERNAL) + public void clearPendingAuthTokens() { + IFingerprintService service = requireNonNull(mService, "mService"); + try { + service.clearPendingAuthTokens(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + /** * Uses the fingerprint hardware to detect for the presence of a finger, without giving details * about accept/reject/lockout. diff --git a/core/java/android/hardware/fingerprint/IFingerprintService.aidl b/core/java/android/hardware/fingerprint/IFingerprintService.aidl index 370f097b68507..2b3916c2f824a 100644 --- a/core/java/android/hardware/fingerprint/IFingerprintService.aidl +++ b/core/java/android/hardware/fingerprint/IFingerprintService.aidl @@ -61,6 +61,15 @@ interface IFingerprintService { long authenticate(IBinder token, long operationId, IFingerprintServiceReceiver receiver, in FingerprintAuthenticateOptions options); + // Add a pending hardware auth token to KeyStore. This should only be called after biometric + // second factor has succeeded. + @EnforcePermission("USE_BIOMETRIC_INTERNAL") + int addPendingAuthTokenToKeyStore(int userId); + + // Clear all pending auth tokens. + @EnforcePermission("USE_BIOMETRIC_INTERNAL") + void clearPendingAuthTokens(); + // Uses the fingerprint hardware to detect for the presence of a finger, without giving details // about accept/reject/lockout. A requestId is returned that can be used to cancel this // operation. diff --git a/core/java/android/hardware/usb/IUsbManager.aidl b/core/java/android/hardware/usb/IUsbManager.aidl index 31c4bb9a846b8..d64b59c948fa3 100644 --- a/core/java/android/hardware/usb/IUsbManager.aidl +++ b/core/java/android/hardware/usb/IUsbManager.aidl @@ -163,14 +163,14 @@ interface IUsbManager boolean isUvcGadgetSupportEnabled(); /* Sets the current USB function. */ - @EnforcePermission("MANAGE_USB") + @EnforcePermission(anyOf={"MANAGE_USB", "MANAGE_USB_ANDROID_AUTO"}) void setCurrentFunctions(long functions, int operationId); /* Compatibility version of setCurrentFunctions(long). */ void setCurrentFunction(String function, boolean usbDataUnlocked, int operationId); /* Gets the current USB functions. */ - @EnforcePermission("MANAGE_USB") + @EnforcePermission(anyOf={"MANAGE_USB", "MANAGE_USB_ANDROID_AUTO"}) long getCurrentFunctions(); /* Gets the current USB Speed. */ @@ -192,7 +192,7 @@ interface IUsbManager long getScreenUnlockedFunctions(); /* Resets the USB gadget. */ - @EnforcePermission("MANAGE_USB") + @EnforcePermission(anyOf={"MANAGE_USB", "MANAGE_USB_ANDROID_AUTO"}) void resetUsbGadget(); /* Resets the USB port. */ @@ -216,7 +216,7 @@ interface IUsbManager ParcelFileDescriptor getControlFd(long function); /* Gets the list of USB ports. */ - @EnforcePermission("MANAGE_USB") + @EnforcePermission(anyOf={"MANAGE_USB", "MANAGE_USB_ANDROID_AUTO"}) List getPorts(); /* Gets the status of the specified USB port. */ @@ -251,6 +251,7 @@ interface IUsbManager "@android.annotation.RequiresPermission(android.Manifest.permission.MANAGE_USB)") void unregisterForDisplayPortEvents(IDisplayPortAltModeInfoListener listener); + void updatePortSecuritySetting(int newValue); /* Enable/disable PCI tunnels for USB4 and Thunderbolt connections. */ @EnforcePermission("MANAGE_USB") void setPciTunnelingEnabled(boolean enable); diff --git a/core/java/android/hardware/usb/UsbManager.java b/core/java/android/hardware/usb/UsbManager.java index b13fc1aec6e4f..cfe5d3d628247 100644 --- a/core/java/android/hardware/usb/UsbManager.java +++ b/core/java/android/hardware/usb/UsbManager.java @@ -2370,6 +2370,15 @@ public void unregisterDisplayPortAltModeInfoListener( return; } + /** @hide */ + public void updatePortSecuritySetting(int newValue) { + try { + mService.updatePortSecuritySetting(newValue); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + @GuardedBy("mBc12TypeListenersLock") @RequiresPermission(Manifest.permission.MANAGE_USB) private boolean registerBc12TypeEventsIfNeededLocked() { diff --git a/core/java/android/net/EventLogTags.logtags b/core/java/android/net/EventLogTags.logtags index 32953c92d1203..94726cae55443 100644 --- a/core/java/android/net/EventLogTags.logtags +++ b/core/java/android/net/EventLogTags.logtags @@ -4,3 +4,5 @@ option java_package android.net 50080 ntp_success (server|3),(rtt|2),(offset|2) 50081 ntp_failure (server|3),(msg|3) +50082 https_time_success (url|3),(rtt|2),(offset|2) +50083 https_time_failure (url|3),(msg|3) diff --git a/core/java/android/net/HttpsTimeClient.java b/core/java/android/net/HttpsTimeClient.java new file mode 100644 index 0000000000000..fadfc94a4c769 --- /dev/null +++ b/core/java/android/net/HttpsTimeClient.java @@ -0,0 +1,421 @@ +package android.net; + +import android.annotation.Nullable; +import android.content.Context; +import android.content.res.Resources; +import android.os.SystemClock; +import android.util.Log; +import android.util.NtpTrustedTime.TimeResult; + +import com.android.internal.R; + +import java.io.InputStream; +import java.math.BigInteger; +import java.net.InetSocketAddress; +import java.net.MalformedURLException; +import java.net.URL; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.KeyManagementException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Principal; +import java.security.PublicKey; +import java.security.SignatureException; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Set; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLSocketFactory; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; +import javax.security.auth.x500.X500Principal; + +import libcore.io.IoUtils; + +import static com.android.net.module.util.ConnectivityUtils.saturatedCast; + +/** @hide */ +public class HttpsTimeClient { + private static final String TAG = HttpsTimeClient.class.getSimpleName(); + + private final Config config; + private final Network network; + + public HttpsTimeClient(Config config, Network network) { + this.config = config; + this.network = network; + } + + public static class Config { + public final List urls; + public final int timeoutMillis; + + Config(List urls, int timeoutMillis) { + this.urls = urls; + this.timeoutMillis = timeoutMillis; + } + + public static Config getDefault(Context ctx) { + Resources res = ctx.getResources(); + + String[] urlArr = res.getStringArray(R.array.config_httpsTimeUrls); + int num = urlArr.length; + List urls = new ArrayList<>(num); + for (int i = 0; i < num; ++i) { + URL url; + try { + url = new URL(urlArr[i]); + } catch (MalformedURLException e) { + throw new IllegalStateException(e); + } + urls.add(url); + } + final int timeoutMillis = res.getInteger(R.integer.config_ntpTimeout); + + return new Config(urls, timeoutMillis); + } + + @Override + public String toString() { + return "HttpsTimeConfig{urls=" + Arrays.toString(urls.toArray()) + + ", timeoutMillis=" + timeoutMillis + + '}'; + } + } + + public static class Result { + public final TimeResult timeResult; + public final URL url; + + Result(TimeResult timeResult, URL url) { + this.timeResult = timeResult; + this.url = url; + } + } + + public Result requestTime(@Nullable URL lastSuccessfulUrl) { + ArrayList urls = new ArrayList<>(config.urls); + if (urls.remove(lastSuccessfulUrl)) { + urls.add(0, lastSuccessfulUrl); + } + + for (URL url : urls) { + TimeResult timeResult = requestTimeInner(url); + if (timeResult != null) { + return new Result(timeResult, url); + } + } + return null; + } + + private TimeResult requestTimeInner(URL url) { + final Network networkForResolv = network.getPrivateDnsBypassingCopy(); + final int timeout = config.timeoutMillis; + HttpsURLConnection conn = null; + InputStream streamToClose = null; + try { + SSLSocketFactory socketFactory = createSSLSocketFactory(); + + // establish HTTPS connection in advance to improve accuracy + conn = (HttpsURLConnection) networkForResolv.openConnection(url); + conn.setSSLSocketFactory(socketFactory); + conn.setConnectTimeout(timeout); + conn.setReadTimeout(timeout); + // closing the InputStream makes the connection available for reuse + conn.getInputStream().close(); + + conn = (HttpsURLConnection) networkForResolv.openConnection(url); + conn.setSSLSocketFactory(socketFactory); + conn.setConnectTimeout(timeout); + conn.setReadTimeout(timeout); + conn.setRequestProperty("Connection", "close"); + + final long requestTime = System.currentTimeMillis(); + final long requestTicks = SystemClock.elapsedRealtime(); + streamToClose = conn.getInputStream(); + final long responseTicks = SystemClock.elapsedRealtime(); + + long serverTime; + try { + serverTime = Long.parseLong(conn.getHeaderField("X-Time")); + } catch (final NumberFormatException e) { + Log.w(TAG, "X-Time header is missing, falling back to the less precise date header", e); + serverTime = conn.getDate(); + } + + final long roundTripTime = responseTicks - requestTicks; + final long responseTime = requestTime + roundTripTime; + final long clockOffset = ((serverTime - requestTime) + (serverTime - responseTime)) / 2; + + Log.d(TAG, "roundTripTime: " + roundTripTime + " ms, " + + "clockOffset: " + clockOffset + " ms"); + + if (serverTime < android.os.Build.TIME) { + throw new GeneralSecurityException("server timestamp is before android.os.Build.TIME"); + } + + EventLogTags.writeHttpsTimeSuccess(url.toString(), roundTripTime, clockOffset); + + return new TimeResult( + responseTime + clockOffset, // unixEpochTimeMillis + responseTicks, // elapsedRealtimeMillis + saturatedCast(roundTripTime / 2), // uncertaintyMillis + InetSocketAddress.createUnresolved(url.getHost(), 443) // ntpServerSocketAddress + ); + } catch (Exception e) { + EventLogTags.writeHttpsTimeFailure(url.toString(), e.toString()); + Log.e(TAG, "request failed, url: " + url, e); + return null; + } finally { + if (conn != null) { + conn.disconnect(); + } + if (streamToClose != null) { + // it's important to close the stream after conn.disconnect(), otherwise connection + // might be reused for the next request + IoUtils.closeQuietly(streamToClose); + } + } + } + + // SSL certificate time checks might fail if the time was never synced before, or if it has + // drifted far enough after the previous sync, + // + // To prevent this issue, construct a special SSLSocketFactory that uses the OS build time + // (android.os.Build.TIME) for SSL certificate expiration checks + private static SSLSocketFactory createSSLSocketFactory() throws KeyManagementException, + KeyStoreException, NoSuchAlgorithmException { + var tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init((KeyStore) null); + + TrustManager[] trustManagers = tmf.getTrustManagers(); + for (int i = 0; i < trustManagers.length; ++i) { + if (trustManagers[i] instanceof X509TrustManager xtm) { + trustManagers[i] = new X509TrustManagerWrapper(xtm); + } + } + + SSLContext sslCtx = SSLContext.getInstance("TLS"); + sslCtx.init(null, trustManagers, null); + return sslCtx.getSocketFactory(); + } + + // see createSSLSocketFactory() + static class X509TrustManagerWrapper implements X509TrustManager { + final X509TrustManager orig; + + X509TrustManagerWrapper(X509TrustManager orig) { + this.orig = orig; + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + int num = chain.length; + var wrappedChain = new X509Certificate[num]; + for (int i = 0; i < num; ++i) { + wrappedChain[i] = new X509CertificateWrapper(chain[i]); + } + orig.checkServerTrusted(wrappedChain, authType); + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return orig.getAcceptedIssuers(); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + // unused + throw new IllegalStateException(authType); + } + } + + // see createSSLSocketFactory() + static class X509CertificateWrapper extends X509Certificate { + private final X509Certificate orig; + + X509CertificateWrapper(X509Certificate orig) { + this.orig = orig; + } + + @Override + public void checkValidity() throws CertificateExpiredException { + checkValidity(null); + } + + @Override + public void checkValidity(Date ignored) throws CertificateExpiredException { + final Date buildDate = new Date(android.os.Build.TIME); + + // don't check notBefore: HttpsTimeClient is the main time source + + if (buildDate.after(getNotAfter())) { + String msg = "notAfter: " + getNotAfter() + ", buildDate: " + buildDate; + throw new CertificateExpiredException(msg); + } + } + + @Override + public Set getCriticalExtensionOIDs() { + return orig.getCriticalExtensionOIDs(); + } + + @Override + public byte[] getExtensionValue(String oid) { + return orig.getExtensionValue(oid); + } + + @Override + public Set getNonCriticalExtensionOIDs() { + return orig.getNonCriticalExtensionOIDs(); + } + + @Override + public boolean hasUnsupportedCriticalExtension() { + return orig.hasUnsupportedCriticalExtension(); + } + + @Override + public int getBasicConstraints() { + return orig.getBasicConstraints(); + } + + @Override + public Principal getIssuerDN() { + return orig.getIssuerDN(); + } + + @Override + public boolean[] getIssuerUniqueID() { + return orig.getIssuerUniqueID(); + } + + @Override + public boolean[] getKeyUsage() { + return orig.getKeyUsage(); + } + + @Override + public Date getNotAfter() { + return orig.getNotAfter(); + } + + @Override + public Date getNotBefore() { + return orig.getNotBefore(); + } + + @Override + public BigInteger getSerialNumber() { + return orig.getSerialNumber(); + } + + @Override + public String getSigAlgName() { + return orig.getSigAlgName(); + } + + @Override + public String getSigAlgOID() { + return orig.getSigAlgOID(); + } + + @Override + public byte[] getSigAlgParams() { + return orig.getSigAlgParams(); + } + + @Override + public byte[] getSignature() { + return orig.getSignature(); + } + + @Override + public Principal getSubjectDN() { + return orig.getSubjectDN(); + } + + @Override + public boolean[] getSubjectUniqueID() { + return orig.getSubjectUniqueID(); + } + + @Override + public byte[] getTBSCertificate() throws CertificateEncodingException { + return orig.getTBSCertificate(); + } + + @Override + public int getVersion() { + return orig.getVersion(); + } + + @Override + public byte[] getEncoded() throws CertificateEncodingException { + return orig.getEncoded(); + } + + @Override + public PublicKey getPublicKey() { + return orig.getPublicKey(); + } + + @Override + public String toString() { + return orig.toString(); + } + + @Override + public void verify(PublicKey key) throws CertificateException, InvalidKeyException, + NoSuchAlgorithmException, NoSuchProviderException, SignatureException { + orig.verify(key); + } + + @Override + public void verify(PublicKey key, String sigProvider) throws CertificateException, + InvalidKeyException, NoSuchAlgorithmException, NoSuchProviderException, + SignatureException { + orig.verify(key, sigProvider); + } + + @Override + public List getExtendedKeyUsage() throws CertificateParsingException { + return orig.getExtendedKeyUsage(); + } + + @Override + public Collection> getIssuerAlternativeNames() throws CertificateParsingException { + return orig.getIssuerAlternativeNames(); + } + + @Override + public X500Principal getIssuerX500Principal() { + return orig.getIssuerX500Principal(); + } + + @Override + public Collection> getSubjectAlternativeNames() throws CertificateParsingException { + return orig.getSubjectAlternativeNames(); + } + + @Override + public X500Principal getSubjectX500Principal() { + return orig.getSubjectX500Principal(); + } + } +} diff --git a/core/java/android/os/AppZygote.java b/core/java/android/os/AppZygote.java index 423ac0a192b8c..a577b5449ab48 100644 --- a/core/java/android/os/AppZygote.java +++ b/core/java/android/os/AppZygote.java @@ -27,6 +27,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.os.Zygote; +import com.android.internal.os.ZygoteExtraArgs; import dalvik.system.VMRuntime; @@ -85,11 +86,11 @@ public AppZygote(ApplicationInfo appInfo, ProcessInfo processInfo, int zygoteUid * Returns the zygote process associated with this app zygote. * Creates the process if it's not already running. */ - public ChildZygoteProcess getProcess() { + public ChildZygoteProcess getProcess(ZygoteExtraArgs zygoteExtArgs) { synchronized (mLock) { if (mZygote != null) return mZygote; - connectToZygoteIfNeededLocked(); + connectToZygoteIfNeededLocked(zygoteExtArgs); return mZygote; } } @@ -149,7 +150,9 @@ public String getProcessName() { * @return An object that describes the result of the attempt to start the process. * @throws RuntimeException on fatal start failure */ - public final Process.ProcessStartResult startProcess(@NonNull final String processClass, + public final Process.ProcessStartResult startProcess( + @NonNull ZygoteExtraArgs zygoteExtArgs, + @NonNull final String processClass, final String niceName, int uid, @Nullable int[] gids, int runtimeFlags, int mountExternal, @@ -170,7 +173,7 @@ public final Process.ProcessStartResult startProcess(@NonNull final String proce long startSeq, @Nullable String[] zygoteArgs) { try { - return getProcess().getZygoteProcess().start(processClass, + return getProcess(zygoteExtArgs).getZygoteProcess().start(zygoteExtArgs, processClass, niceName, uid, uid, gids, runtimeFlags, mountExternal, targetSdkVersion, seInfo, abi, instructionSet, appDataDir, null, packageName, @@ -180,7 +183,7 @@ public final Process.ProcessStartResult startProcess(@NonNull final String proce false, false, false, startSeq, zygoteArgs); } catch (RuntimeException e) { - final boolean zygote_dead = getProcess().isDead(); + final boolean zygote_dead = getProcess(zygoteExtArgs).isDead(); if (!zygote_dead) { throw e; // Zygote process is alive. Do nothing. } @@ -188,7 +191,7 @@ public final Process.ProcessStartResult startProcess(@NonNull final String proce // Retry here if the previous start fails. Log.w(LOG_TAG, "retry starting process " + niceName); stopZygote(); - return getProcess().getZygoteProcess().start(processClass, + return getProcess(zygoteExtArgs).getZygoteProcess().start(zygoteExtArgs, processClass, niceName, uid, uid, gids, runtimeFlags, mountExternal, targetSdkVersion, seInfo, abi, instructionSet, appDataDir, null, packageName, @@ -212,7 +215,7 @@ private void stopZygoteLocked() { } @GuardedBy("mLock") - private void connectToZygoteIfNeededLocked() { + private void connectToZygoteIfNeededLocked(ZygoteExtraArgs zygoteExtArgs) { String abi = mAppInfo.primaryCpuAbi != null ? mAppInfo.primaryCpuAbi : Build.SUPPORTED_ABIS[0]; try { @@ -225,6 +228,7 @@ private void connectToZygoteIfNeededLocked() { mIsNativeService ? Process.NATIVE_ZYGOTE_PROCESS : Process.ZYGOTE_PROCESS; String seInfo = mIsNativeService ? "native_app_zygote" : "app_zygote"; mZygote = process.startChildZygote( + zygoteExtArgs, "com.android.internal.os.AppZygoteInit", mProcessName, mZygoteUid, @@ -247,7 +251,10 @@ private void connectToZygoteIfNeededLocked() { mZygote.getZygoteProcess().getPrimarySocketAddress()); // preload application code in the zygote Log.i(LOG_TAG, "Starting application preload."); - mZygote.getZygoteProcess().preloadApp(mAppInfo, abi); + mZygote.getZygoteProcess().preloadApp(mAppInfo, abi, + // this is a child zygote, ZygoteSelectionMode from ZygoteExtraArgs should + // not be applied to it + ZygoteSelectionMode.Regular); Log.i(LOG_TAG, "Application preload done."); } } catch (Exception e) { diff --git a/core/java/android/os/BaseBundle.java b/core/java/android/os/BaseBundle.java index 4231b6dfca54c..837cb5360da87 100644 --- a/core/java/android/os/BaseBundle.java +++ b/core/java/android/os/BaseBundle.java @@ -21,6 +21,8 @@ import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.annotation.SystemApi; import android.compat.annotation.UnsupportedAppUsage; import android.content.Intent; import android.security.Flags; @@ -1997,4 +1999,25 @@ public static void dumpStats(IndentingPrintWriter pw, BaseBundle bundle) { } pw.decreaseIndent(); } + + /** @hide */ + @SystemApi + @Nullable + public T getNumber(@NonNull String key) { + // get{Boolean,Byte,Short,Int,Long,Float,Double}() methods do not distinguish between + // absence of value and value being invalid, and do not allow reliably detecting these cases + // at all + unparcel(); + return (T) mMap.get(key); + } + + /**@hide */ + @SystemApi + @Nullable + @SuppressLint("AutoBoxing") + public Boolean getBoolean2(@NonNull String key) { + // see getNumber() + unparcel(); + return (Boolean) mMap.get(key); + } } diff --git a/core/java/android/os/Binder.java b/core/java/android/os/Binder.java index de9f57910c92a..21242a59de1d8 100644 --- a/core/java/android/os/Binder.java +++ b/core/java/android/os/Binder.java @@ -20,12 +20,14 @@ import android.annotation.Nullable; import android.annotation.SystemApi; import android.app.AppOpsManager; +import android.app.compat.gms.GmsCompat; import android.compat.annotation.UnsupportedAppUsage; import android.ravenwood.annotation.RavenwoodKeepWholeClass; import android.util.ExceptionUtils; import android.util.Log; import android.util.Slog; +import com.android.internal.gmscompat.GmsHooks; import com.android.internal.os.BinderCallHeavyHitterWatcher; import com.android.internal.os.BinderCallHeavyHitterWatcher.BinderCallHeavyHitterListener; import com.android.internal.os.BinderInternal; @@ -746,8 +748,20 @@ public Binder(@Nullable String descriptor) { public void attachInterface(@Nullable IInterface owner, @Nullable String descriptor) { mOwner = owner; mDescriptor = descriptor; + + // Interface that is used when obtaining a binder from GmsCore + mIsIGmsCallbacks = "com.google.android.gms.common.internal.IGmsCallbacks".equals(descriptor); + + 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. */ @@ -1196,6 +1210,14 @@ public final void setExtension(@Nullable IBinder extension) { private final native void setExtensionNative(@Nullable IBinder extension); + private static final String LOG_TAG_TXN = "BinderTxn"; + private static boolean LOG_TXNS = Log.isLoggable(LOG_TAG_TXN, Log.VERBOSE); + + /** @hide */ + public static void onZygotePostForkChild() { + LOG_TXNS = Log.isLoggable(LOG_TAG_TXN, Log.VERBOSE); + } + /** * Set whether or not this binder node will inherit real-time priority. * This should be called immediately when the object is created. @@ -1224,6 +1246,10 @@ public final boolean transact(int code, @NonNull Parcel data, @Nullable Parcel r int flags) throws RemoteException { if (false) Log.v("Binder", "Transact: " + code + " to " + this); + if (LOG_TXNS) { + Log.v(LOG_TAG_TXN, getInterfaceDescriptor() + ", code " + code, new Throwable()); + } + if (data != null) { data.setDataPosition(0); } @@ -1299,10 +1325,15 @@ public static void setWorkSourceProvider(BinderInternal.WorkSourceProvider workS sWorkSourceProvider = workSourceProvider; } + private volatile int mPreviousUid; + // Entry point from android_util_Binder.cpp's onTransact. @UnsupportedAppUsage private boolean execTransact(int code, long dataObj, long replyObj, int flags) { + if (LOG_TXNS) { + Log.v(LOG_TAG_TXN, getInterfaceDescriptor() + ", code " + code); + } Parcel data = Parcel.obtain(dataObj); Parcel reply = Parcel.obtain(replyObj); @@ -1319,6 +1350,16 @@ private boolean execTransact(int code, long dataObj, long replyObj, final long origWorkSource = callingUid == -1 ? -1 : ThreadLocalWorkSource.setUid(callingUid); + if (GmsCompat.isEnabled() && callingUid != -1) { + if (callingUid != mPreviousUid) { + // harmless race + mPreviousUid = callingUid; + if (Process.isApplicationUid(callingUid)) { + GmsHooks.onBinderTransaction(Binder.getCallingPid(), callingUid); + } + } + } + try { return execTransactInternal(code, data, reply, flags, callingUid); } finally { @@ -1346,7 +1387,14 @@ private boolean execTransactInternal(int code, Parcel data, Parcel reply, int fl // Log any exceptions as warnings, don't silently suppress them. // If the call was {@link IBinder#FLAG_ONEWAY} then these exceptions // disappear into the ether. + data.mCallMaybeOverrideBinder = mIsIGmsCallbacks; + boolean onBeginGmsServiceBrokerCallRet = false; 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. final BinderCallHeavyHitterWatcher heavyHitterWatcher = sHeavyHitterWatcher; @@ -1394,6 +1442,10 @@ private boolean execTransactInternal(int code, Parcel data, Parcel reply, int fl } res = true; } finally { + data.mCallMaybeOverrideBinder = false; + if (onBeginGmsServiceBrokerCallRet) { + GmsHooks.onEndGmsServiceBrokerCall(); + } if (observer != null) { // The parcel RPC headers have been called during onTransact so we can now access // the worksource UID from the parcel. @@ -1442,4 +1494,4 @@ public static synchronized void setHeavyHitterWatcherConfig(final boolean enable watcher.setConfig(false, 0, 0.0f, null); } } -} \ No newline at end of file +} diff --git a/core/java/android/os/BinderDef.aidl b/core/java/android/os/BinderDef.aidl new file mode 100644 index 0000000000000..6b55861e809a6 --- /dev/null +++ b/core/java/android/os/BinderDef.aidl @@ -0,0 +1,3 @@ +package android.os; + +parcelable BinderDef; diff --git a/core/java/android/os/BinderDef.java b/core/java/android/os/BinderDef.java new file mode 100644 index 0000000000000..f846f3ed0f73b --- /dev/null +++ b/core/java/android/os/BinderDef.java @@ -0,0 +1,102 @@ +package android.os; + +import android.annotation.Nullable; +import android.content.Context; +import android.util.ArrayMap; +import android.util.Log; + +import java.lang.reflect.Constructor; + +import dalvik.system.DexClassLoader; + +/** + * Definition of a binder in another Android package. + * + * @hide + */ +public class BinderDef implements Parcelable { + private static final String TAG = BinderDef.class.getSimpleName(); + + public final String interfaceName; // also referred to as "interface descriptor" + public final String apkPath; + public final String className; + // Sorted array of handled binder transactions codes, null means "all transactions are handled" + @Nullable public final int[] transactionCodes; + + public BinderDef(String interfaceName, String apkPath, String className, @Nullable int[] transactionCodes) { + this.interfaceName = interfaceName; + this.apkPath = apkPath; + this.className = className; + this.transactionCodes = transactionCodes; + } + + protected BinderDef(Parcel in) { + interfaceName = in.readString(); + apkPath = in.readString(); + className = in.readString(); + transactionCodes = in.createIntArray(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(interfaceName); + dest.writeString(apkPath); + dest.writeString(className); + dest.writeIntArray(transactionCodes); + } + + private volatile IBinder instance; + + @Nullable + public IBinder getInstance(Context ctx) { + { IBinder cache = instance; if (cache != null) return cache; } + + synchronized (this) { + { IBinder cache = instance; if (cache != null) return cache; } + try { + return instance = instantiate(ctx); + } catch (ReflectiveOperationException e) { + Log.e(TAG, "unable to instantiate " + className + " from " + apkPath, e); + return null; + } + } + } + + private IBinder instantiate(Context ctx) throws ReflectiveOperationException { + Class cls = getDexClassLoader().loadClass(className); + Constructor constructor = cls.getConstructor(Context.class); + + return (IBinder) constructor.newInstance(ctx); + } + + private static final ArrayMap classLoaders = new ArrayMap<>(); + + private DexClassLoader getDexClassLoader() { + final var map = classLoaders; + synchronized (map) { + DexClassLoader cl = map.get(apkPath); + if (cl == null) { + cl = new DexClassLoader(apkPath, null, null, String.class.getClassLoader()); + map.put(apkPath, cl); + } + return cl; + } + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator CREATOR = new Creator() { + @Override + public BinderDef createFromParcel(Parcel in) { + return new BinderDef(in); + } + + @Override + public BinderDef[] newArray(int size) { + return new BinderDef[size]; + } + }; +} diff --git a/core/java/android/os/BinderProxy.java b/core/java/android/os/BinderProxy.java index 1b25d63944a3e..cecc0284b5052 100644 --- a/core/java/android/os/BinderProxy.java +++ b/core/java/android/os/BinderProxy.java @@ -28,6 +28,8 @@ import android.util.SparseIntArray; import com.android.internal.annotations.GuardedBy; +import com.android.internal.gmscompat.GmsCompatLib; +import com.android.internal.gmscompat.IGmsCompatLib; import com.android.internal.os.BinderInternal; import libcore.util.NativeAllocationRegistry; @@ -534,11 +536,27 @@ private static class NoImagePreloadHolder { @RavenwoodKeep public native boolean isBinderAlive(); + private IInterface mLocalInterface; + private static final IInterface NO_LOCAL_INTERFACE = () -> null; + /** * Retrieve a local interface - always null in case of a proxy */ @RavenwoodKeep public IInterface queryLocalInterface(String descriptor) { + IGmsCompatLib gmcLib = GmsCompatLib.get(); + if (gmcLib != null) { + synchronized (this) { + IInterface cache = mLocalInterface; + if (cache != null) { + return cache == NO_LOCAL_INTERFACE ? null : cache; + } + IInterface res = gmcLib.maybeProvideBinderProxyInterface(this, descriptor); + mLocalInterface = res != null ? res : NO_LOCAL_INTERFACE; + return res; + } + + } return null; } @@ -547,6 +565,14 @@ public IInterface queryLocalInterface(String descriptor) { @RavenwoodKeep public native @Nullable IBinder getExtension() throws RemoteException; + private static final String LOG_TAG_TXN = "BinderProxyTxn"; + private static boolean LOG_TXNS = Log.isLoggable(LOG_TAG_TXN, Log.VERBOSE); + + /** @hide */ + public static void onZygotePostForkChild() { + LOG_TXNS = Log.isLoggable(LOG_TAG_TXN, Log.VERBOSE); + } + /** * Perform a binder transaction on a proxy. * @@ -566,6 +592,10 @@ public IInterface queryLocalInterface(String descriptor) { */ @RavenwoodKeep public boolean transact(int code, Parcel data, Parcel reply, int flags) throws RemoteException { + if (LOG_TXNS) { + Log.v(LOG_TAG_TXN, getInterfaceDescriptor() + ", code " + code, new Throwable()); + } + Binder.checkParcel(this, code, data, "Unreasonably large binder buffer"); boolean warnOnBlocking = mWarnOnBlocking; // Cache it to reduce volatile access. @@ -775,6 +805,17 @@ private native boolean removeFrozenStateChangeCallbackNative( */ @RavenwoodKeep public void dump(FileDescriptor fd, String[] args) throws RemoteException { + if (LOG_TXNS) { + Log.v(LOG_TAG_TXN, "dump: " + getInterfaceDescriptor() + ", args " + Arrays.toString(args), new Throwable()); + } + + IGmsCompatLib gmcLib = GmsCompatLib.get(); + if (gmcLib != null) { + if (gmcLib.maybeInterceptBinderProxyDump(this, fd, args, false)) { + return; + } + } + Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); data.writeFileDescriptor(fd); @@ -797,6 +838,17 @@ public void dump(FileDescriptor fd, String[] args) throws RemoteException { */ @RavenwoodKeep public void dumpAsync(FileDescriptor fd, String[] args) throws RemoteException { + if (LOG_TXNS) { + Log.v(LOG_TAG_TXN, "dumpAsync: " + getInterfaceDescriptor() + ", args " + Arrays.toString(args), new Throwable()); + } + + IGmsCompatLib gmcLib = GmsCompatLib.get(); + if (gmcLib != null) { + if (gmcLib.maybeInterceptBinderProxyDump(this, fd, args, true)) { + return; + } + } + Parcel data = Parcel.obtain(); Parcel reply = Parcel.obtain(); data.writeFileDescriptor(fd); diff --git a/core/java/android/os/BinderWrapper.java b/core/java/android/os/BinderWrapper.java new file mode 100644 index 0000000000000..95fe9cc09f711 --- /dev/null +++ b/core/java/android/os/BinderWrapper.java @@ -0,0 +1,83 @@ +package android.os; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; + +import java.io.FileDescriptor; + +/** @hide */ +public class BinderWrapper implements IBinder { + protected final IBinder base; + + public BinderWrapper(IBinder base) { + this.base = base; + } + + @Override + public boolean transact(int code, @NonNull Parcel data, @Nullable Parcel reply, int flags) throws RemoteException { + return base.transact(code, data, reply, flags); + } + + @Nullable + @Override + public IInterface queryLocalInterface(@NonNull String descriptor) { + return base.queryLocalInterface(descriptor); + } + + @Nullable + @Override + public String getInterfaceDescriptor() throws RemoteException { + return base.getInterfaceDescriptor(); + } + + @Override + public boolean pingBinder() { + return base.pingBinder(); + } + + @Override + public boolean isBinderAlive() { + return base.isBinderAlive(); + } + + @Override + public void dump(@NonNull FileDescriptor fd, @Nullable String[] args) throws RemoteException { + base.dump(fd, args); + } + + @Override + public void dumpAsync(@NonNull FileDescriptor fd, @Nullable String[] args) throws RemoteException { + base.dumpAsync(fd, args); + } + + @Override + public void shellCommand(@Nullable FileDescriptor in, @Nullable FileDescriptor out, @Nullable FileDescriptor err, @NonNull String[] args, @Nullable ShellCallback shellCallback, @NonNull ResultReceiver resultReceiver) throws RemoteException { + base.shellCommand(in, out, err, args, shellCallback, resultReceiver); + } + + @Override + public void linkToDeath(@NonNull DeathRecipient recipient, int flags) throws RemoteException { + base.linkToDeath(recipient, flags); + } + + @Override + public boolean unlinkToDeath(@NonNull DeathRecipient recipient, int flags) { + return base.unlinkToDeath(recipient, flags); + } + + @Nullable + public IBinder getExtension() throws RemoteException { + return base.getExtension(); + } + + @Override + public void addFrozenStateChangeCallback(@NonNull FrozenStateChangeCallback callback) throws RemoteException { + base.addFrozenStateChangeCallback(callback); + } + + @Override + public boolean removeFrozenStateChangeCallback(@NonNull FrozenStateChangeCallback callback) { + return base.removeFrozenStateChangeCallback(callback); + } +} diff --git a/core/java/android/os/Build.java b/core/java/android/os/Build.java index 25912a0a53436..05dbe1d6e9bdf 100644 --- a/core/java/android/os/Build.java +++ b/core/java/android/os/Build.java @@ -28,6 +28,7 @@ import android.annotation.TestApi; import android.app.ActivityThread; import android.app.Application; +import android.app.compat.gms.GmsCompat; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.ravenwood.annotation.RavenwoodKeepWholeClass; @@ -40,6 +41,7 @@ import android.util.Slog; import android.view.View; +import com.android.internal.gmscompat.GmsHooks; import com.android.internal.util.FrameworkStatsLog; import dalvik.system.VMRuntime; @@ -297,6 +299,10 @@ public class Build { @SuppressAutoDoc // No support for device / profile owner. @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public static String getSerial() { + if (GmsCompat.isEnabled() && !GmsCompat.isAndroidAuto()) { + return GmsHooks.getSerial(); + } + IDeviceIdentifiersPolicyService service = IDeviceIdentifiersPolicyService.Stub .asInterface(ServiceManager.getService(Context.DEVICE_IDENTIFIERS_SERVICE)); try { @@ -305,6 +311,11 @@ public static String getSerial() { return service.getSerialForPackage(callingPackage, null); } catch (RemoteException e) { e.rethrowFromSystemServer(); + } catch (SecurityException e) { + if (GmsCompat.isEnabled()) { + return GmsHooks.getSerial(); + } + throw e; } return UNKNOWN; } diff --git a/core/java/android/os/Bundle.java b/core/java/android/os/Bundle.java index 3dfe8842cefcb..4db29c2211c0e 100644 --- a/core/java/android/os/Bundle.java +++ b/core/java/android/os/Bundle.java @@ -1588,6 +1588,16 @@ public synchronized String toString() { return "Bundle[" + mMap.toString() + "]"; } + /** + * Same as {@link #toString()}, but unparcels the internal map if it isn't unparcelled already, + * which ensures that the internal map contents are included in the result. + * @hide + */ + public String toStringDeep() { + unparcel((mFlags & FLAG_DEFUSABLE) != 0); + return toString(); + } + /** * @hide */ diff --git a/core/java/android/os/ChildZygoteProcess.java b/core/java/android/os/ChildZygoteProcess.java index 3f91041a1b15d..82003f0dcb0d9 100644 --- a/core/java/android/os/ChildZygoteProcess.java +++ b/core/java/android/os/ChildZygoteProcess.java @@ -62,7 +62,7 @@ private ChildZygoteProcess(IZygoteProcess zygoteProcess, int pid, int uid) { */ public static ChildZygoteProcess createManagedChildZygoteProcess( LocalSocketAddress socketAddress, int pid, int uid) { - return new ChildZygoteProcess(new ZygoteProcess(socketAddress, null), pid, uid); + return new ChildZygoteProcess(ZygoteProcess.createChildZygoteProcess(socketAddress), pid, uid); } /** diff --git a/core/java/android/os/Debug.java b/core/java/android/os/Debug.java index 52e5608c8236d..ba32b969e236d 100644 --- a/core/java/android/os/Debug.java +++ b/core/java/android/os/Debug.java @@ -20,6 +20,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.app.AppGlobals; +import android.app.compat.gms.GmsCompat; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.graphics.Bitmap; @@ -50,6 +51,7 @@ import java.lang.annotation.Target; import java.lang.reflect.Field; import java.lang.reflect.Modifier; +import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -2710,6 +2712,23 @@ public static boolean dumpService(String name, FileDescriptor fd, String[] args) return false; } + if (GmsCompat.isEnabled()) { + Log.d(TAG, "GmsCompat: dumpService() called for " + name + " service, args: " + Arrays.toString(args)); + if ("media_projection".equals(name)) { + try (var os = new FileOutputStream(fd)) { + os.write((""" + MEDIA PROJECTION MANAGER (dumpsys media_projection) + Media Projection:\s + null + """).getBytes()); + return true; + } catch (IOException e) { + Log.e(TAG, "", e); + return false; + } + } + } + try { service.dump(fd, args); return true; diff --git a/core/java/android/os/Environment.java b/core/java/android/os/Environment.java index 1bddf271fb21d..d4fd41830ca62 100644 --- a/core/java/android/os/Environment.java +++ b/core/java/android/os/Environment.java @@ -1718,4 +1718,32 @@ private static File buildPathPrep(File base, String... segments) { public static File maybeTranslateEmulatedPathToInternal(File path) { return StorageManager.maybeTranslateEmulatedPathToInternal(path); } + + /** @hide */ + @UnsupportedAppUsage + public static boolean isExecmemBlocked() { + return com.android.internal.os.SELinuxFlags.isExecmemBlocked(); + } + + private static boolean checkDclAppBindFlag(int flag) { + return (android.ext.dcl.DynCodeLoading.getAppBindFlags() & flag) != 0; + } + + /** @hide */ + @UnsupportedAppUsage + public static boolean isMemoryDclRestricted() { + return checkDclAppBindFlag(android.ext.dcl.DynCodeLoading.RESTRICT_MEMORY_DCL); + } + + /** @hide */ + @UnsupportedAppUsage + public static boolean isStorageDclRestricted() { + return checkDclAppBindFlag(android.ext.dcl.DynCodeLoading.RESTRICT_STORAGE_DCL); + } + + /** @hide */ + @UnsupportedAppUsage + public static boolean isWebViewDclRestricted() { + return checkDclAppBindFlag(android.ext.dcl.DynCodeLoading.RESTRICT_WEBVIEW_DCL); + } } diff --git a/core/java/android/os/HybridBinder.java b/core/java/android/os/HybridBinder.java new file mode 100644 index 0000000000000..32ce9e9973dc0 --- /dev/null +++ b/core/java/android/os/HybridBinder.java @@ -0,0 +1,46 @@ +package android.os; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.util.Log; + +import java.io.FileDescriptor; +import java.util.Arrays; + +/** + * Fuses two binders together. + * Transaction routing decisions are made by looking at transaction codes. + * The rest of operations are forwarded to the first ("base") binder. + * + * @hide + */ +public final class HybridBinder extends BinderWrapper { + private static final String TAG = "HybridBinder"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.VERBOSE); + + private final IBinder secondBinder; + // sorted array of handled transactions codes + private final int[] secondBinderTxnCodes; + + public HybridBinder(Context ctx, IBinder base, BinderDef secondBinderDef) { + super(base); + this.secondBinder = secondBinderDef.getInstance(ctx); + this.secondBinderTxnCodes = secondBinderDef.transactionCodes; + } + + public boolean transact(int code, @NonNull Parcel data, @Nullable Parcel reply, int flags) throws RemoteException { + if (DEBUG) { + Log.d(TAG, "call " + (code - IBinder.FIRST_CALL_TRANSACTION)); + } + if (Arrays.binarySearch(secondBinderTxnCodes, code) >= 0) { + return secondBinder.transact(code, data, reply, flags); + } + return base.transact(code, data, reply, flags); + } + + @Nullable + public IInterface queryLocalInterface(@NonNull String descriptor) { + return null; + } +} diff --git a/core/java/android/os/IZygoteProcess.java b/core/java/android/os/IZygoteProcess.java index ff014914f867d..7f7025709428a 100644 --- a/core/java/android/os/IZygoteProcess.java +++ b/core/java/android/os/IZygoteProcess.java @@ -22,6 +22,8 @@ import android.net.LocalSocketAddress; import android.util.Pair; +import com.android.internal.os.ZygoteExtraArgs; + import java.io.IOException; import java.util.Map; @@ -70,7 +72,8 @@ interface IZygoteProcess { * @return An object that describes the result of the attempt to start the process. * @throws RuntimeException on fatal start failure */ - Process.ProcessStartResult start(@NonNull String processClass, + Process.ProcessStartResult start(@NonNull final ZygoteExtraArgs zygoteExtArgs, + @NonNull String processClass, String niceName, int uid, int gid, @Nullable int[] gids, int runtimeFlags, int mountExternal, @@ -120,7 +123,7 @@ Process.ProcessStartResult start(@NonNull String processClass, * @param appInfo The ApplicationInfo used to derive the linker namespace parameters, the target * SDK version, and zygotePreloadNativeLib/zygotePreloadNativeFunc from. */ - ChildZygoteProcess startChildZygote(String processClass, + ChildZygoteProcess startChildZygote(ZygoteExtraArgs zygoteExtArgs, String processClass, String niceName, int uid, int gid, int[] gids, int runtimeFlags, @@ -141,7 +144,7 @@ ChildZygoteProcess startChildZygote(String processClass, * Instructs the zygote to pre-load the application code for the given Application. * Only the app zygote supports this function. */ - boolean preloadApp(ApplicationInfo appInfo, String abi) + boolean preloadApp(ApplicationInfo appInfo, String abi, ZygoteSelectionMode zsm) throws ZygoteStartFailedEx, IOException; /** diff --git a/core/java/android/os/NativeZygoteProcess.java b/core/java/android/os/NativeZygoteProcess.java index 17cbbaba7ce2e..5d3bd79493dec 100644 --- a/core/java/android/os/NativeZygoteProcess.java +++ b/core/java/android/os/NativeZygoteProcess.java @@ -26,10 +26,12 @@ import android.util.Pair; import com.android.internal.os.Zygote; +import com.android.internal.os.ZygoteExtraArgs; import java.io.FileDescriptor; import java.io.IOException; import java.util.Map; +import java.util.Objects; import java.util.UUID; /** @@ -60,6 +62,10 @@ public NativeZygoteProcess(LocalSocketAddress socketAddress) { /** Prewarm the native zygote daemon before actually using it. */ public static void prewarmNativeZygote() { + if (android.ext.settings.ExtSettings.EXEC_SPAWNING.get()) { + // pre-warm only if exec spawning is disabled by default + return; + } nativePrewarmNativeZygote(); } @@ -80,13 +86,14 @@ private synchronized void connectToZygote() throws IOException { } } - private static native int nativeStartNativeProcess( + private static native int nativeStartNativeProcess(byte[][] outCapturedCommand, long selinuxFlags, + byte[] preloadCommand, FileDescriptor fd, int uid, int gid, long startSeq, String packageName, String niceName, int targetSdkVersion, boolean startChildZygote, int runtimeFlags, String seInfo, boolean isTopApp) throws IOException; - private static native int nativeStartNativeChildZygote( + private static native int nativeStartNativeChildZygote(byte[][] outCapturedCommand, long selinuxFlags, FileDescriptor parentFd, int uid, int gid, String niceName, String seInfo, int targetSdkVersion, int runtimeFlags, String serverAddress, int uidRangeStart, int uidRangeEnd, String allowedLibPath, String librarySearchPaths, boolean isShared, @@ -94,7 +101,8 @@ private static native int nativeStartNativeChildZygote( throws IOException; @Override - public final Process.ProcessStartResult start(@NonNull final String processClass, + public final Process.ProcessStartResult start(@NonNull final ZygoteExtraArgs zygoteExtArgs, + @NonNull final String processClass, final String niceName, int uid, int gid, @Nullable int[] gids, int runtimeFlags, int mountExternal, @@ -119,12 +127,56 @@ public final Process.ProcessStartResult start(@NonNull final String processClass boolean bindOverrideSysprops, long startSeq, @Nullable String[] zygoteArgs) { - int pid; + // ZygoteExtraArgs.FORCIBLY_ENABLE_MEMORY_TAGGING is not supported for native zygote codepath + // since it's low-impact and isn't needed in practice since native zygote is mainly used by + // Chromium browsers which opt-in to memory tagging + + boolean useExecSpawning = zygoteExtArgs.shouldUseExecSpawning(); + + final int pid; try { - connectToZygote(); - pid = nativeStartNativeProcess(mSocket.getFileDescriptor(), uid, gid, startSeq, + final FileDescriptor fd; + final byte[][] outCapturedCommand; + final byte[] preloadCommand; + if (useExecSpawning) { + fd = null; + outCapturedCommand = new byte[1][]; + ApplicationInfo appInfo = zygoteExtArgs.getPreloadAppInfo(); + if (appInfo != null) { + zygoteExtArgs.setPreloadAppInfo(null); // null it out to skip its serialization + LoadedApk.LinkerNamespaceParams params = createLinkerNamespaceParams(appInfo); + int ret = nativeStartNativeChildZygote(outCapturedCommand, 0L, null, 0, 0, null, + null, appInfo.targetSdkVersion, 0, null, + 0, 0, params.permittedLibsDir, params.libPath, + params.isShared, params.zipPath, params.nativeSharedLibs, + appInfo.zygotePreloadNativeLib, appInfo.zygotePreloadNativeFunc); + if (ret != 0) { + throw new RuntimeException("Native process preload command capture failed"); + } + preloadCommand = outCapturedCommand[0]; + outCapturedCommand[0] = null; + } else { + preloadCommand = null; + } + } else { + connectToZygote(); + fd = mSocket.getFileDescriptor(); + outCapturedCommand = null; + preloadCommand = null; + } + int res = nativeStartNativeProcess(outCapturedCommand, zygoteExtArgs.getSelinuxFlags(), preloadCommand, fd, uid, gid, startSeq, packageName, niceName, targetSdkVersion, /*startChildZygote=*/false, runtimeFlags, seInfo, isTopApp); + if (!useExecSpawning) { + pid = res; + } else { + if (res != 0) { + throw new RuntimeException("Native process exec spawning command capture failed"); + } + byte[] command = Objects.requireNonNull(outCapturedCommand[0], "outCapturedCommand[0]"); + zygoteExtArgs.setNativeAppLaunchCommand(command); + return Process.ZYGOTE_PROCESS.execSpawnNativeAppProcess(zygoteExtArgs); + } if (pid == -1) { throw new RuntimeException("Failed to fork a native process"); } @@ -137,8 +189,22 @@ public final Process.ProcessStartResult start(@NonNull final String processClass return result; } + // extracted from startChildZygote() + private static LoadedApk.LinkerNamespaceParams createLinkerNamespaceParams(ApplicationInfo appInfo) { + LoadedApk loadedApk = new LoadedApk( + /*activityThread*/ null, + appInfo, + /*compatInfo*/ null, + /*classLoader*/ null, + /*securityViolation*/ false, + /*includeCode*/ true, + /*registerPackage*/ false); + return loadedApk.createLinkerNamespaceParams(); + } + @Override - public ChildZygoteProcess startChildZygote(final String processClass, + public ChildZygoteProcess startChildZygote(final ZygoteExtraArgs zygoteExtArgs, + final String processClass, final String niceName, int uid, int gid, int[] gids, int runtimeFlags, @@ -156,20 +222,12 @@ public ChildZygoteProcess startChildZygote(final String processClass, // path to Native Zygote over the JNI code. String serverAddressForNative = "@" + serverAddress; - LoadedApk loadedApk = new LoadedApk( - /*activityThread*/ null, - appInfo, - /*compatInfo*/ null, - /*classLoader*/ null, - /*securityViolation*/ false, - /*includeCode*/ true, - /*registerPackage*/ false); - LoadedApk.LinkerNamespaceParams params = loadedApk.createLinkerNamespaceParams(); + LoadedApk.LinkerNamespaceParams params = createLinkerNamespaceParams(appInfo); int pid; try { connectToZygote(); - pid = nativeStartNativeChildZygote(mSocket.getFileDescriptor(), uid, gid, niceName, + pid = nativeStartNativeChildZygote(null, zygoteExtArgs.getSelinuxFlags(), mSocket.getFileDescriptor(), uid, gid, niceName, seInfo, appInfo.targetSdkVersion, runtimeFlags, serverAddressForNative, uidRangeStart, uidRangeEnd, params.permittedLibsDir, params.libPath, params.isShared, params.zipPath, params.nativeSharedLibs, @@ -190,7 +248,7 @@ public LocalSocketAddress getPrimarySocketAddress() { } @Override - public boolean preloadApp(ApplicationInfo appInfo, String abi) { + public boolean preloadApp(ApplicationInfo appInfo, String abi, ZygoteSelectionMode zsm) { return false; } diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java index ddb905096c15e..f2f97023f261b 100644 --- a/core/java/android/os/Parcel.java +++ b/core/java/android/os/Parcel.java @@ -30,6 +30,7 @@ import android.annotation.SuppressLint; import android.annotation.TestApi; import android.app.AppOpsManager; +import android.app.compat.gms.GmsCompat; import android.app.compat.CompatChanges; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; @@ -52,6 +53,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.gmscompat.GmsHooks; import com.android.internal.util.ArrayUtils; import dalvik.annotation.optimization.CriticalNative; @@ -3363,6 +3365,13 @@ public final void readException(int code, String msg) { "Remote stack trace:\n" + remoteStackTrace, null, false, false); ExceptionUtils.appendCause(e, cause); } + + if (GmsCompat.isEnabled()) { + if (GmsHooks.interceptException(e, this)) { + return; + } + } + SneakyThrow.sneakyThrow(e); } @@ -3490,6 +3499,12 @@ && hasFlags( | FLAG_PROPAGATE_ALLOW_BLOCKING)) { Binder.allowBlocking(result); } + if (mCallMaybeOverrideBinder && result != null) { + IBinder override = GmsHooks.maybeOverrideBinder(result); + if (override != null) { + return override; + } + } return result; } @@ -3510,6 +3525,9 @@ public final CharSequence readCharSequence() { return TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(this); } + /** {@hide} */ + boolean mCallMaybeOverrideBinder; + /** * Read an object from the parcel at the current dataPosition(). */ diff --git a/core/java/android/os/ParcelFileDescriptor.java b/core/java/android/os/ParcelFileDescriptor.java index 96e963c665407..d348f0b37ba3c 100644 --- a/core/java/android/os/ParcelFileDescriptor.java +++ b/core/java/android/os/ParcelFileDescriptor.java @@ -50,6 +50,8 @@ import android.util.Log; import android.util.Slog; +import com.android.internal.gmscompat.fileservice.GmsCoreFileServerClientHooks; + import dalvik.system.VMRuntime; import libcore.io.IoUtils; @@ -350,6 +352,14 @@ private static FileDescriptor openInternal(File file, int mode) throws FileNotFo if ((mode & MODE_WORLD_WRITEABLE) != 0) realMode |= S_IWOTH; final String path = file.getPath(); + + if (GmsCoreFileServerClientHooks.isEnabled()) { + FileDescriptor override = GmsCoreFileServerClientHooks.openParcelFileDescriptorHook(path, flags); + if (override != null) { + return override; + } + } + try { return Os.open(path, flags, realMode); } catch (ErrnoException e) { diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java index 00dd71b722675..4191941614c5c 100644 --- a/core/java/android/os/Process.java +++ b/core/java/android/os/Process.java @@ -43,6 +43,7 @@ import android.util.Pair; import android.webkit.WebViewZygote; +import com.android.internal.os.ZygoteExtraArgs; import com.android.sdksandbox.flags.Flags; import dalvik.annotation.optimization.CriticalNative; @@ -766,7 +767,8 @@ public class Process { * * @hide */ - public static ProcessStartResult start(@NonNull final String processClass, + public static ProcessStartResult start(@NonNull final ZygoteExtraArgs zygoteExtArgs, + @NonNull final String processClass, @Nullable final String niceName, int uid, int gid, @Nullable int[] gids, int runtimeFlags, @@ -795,7 +797,7 @@ public static ProcessStartResult start(@NonNull final String processClass, boolean isNative = android.os.Flags.nativeFrameworkPrototype() && (zygotePolicyFlags & ZYGOTE_POLICY_FLAG_NATIVE_PROCESS) != 0; IZygoteProcess process = isNative ? NATIVE_ZYGOTE_PROCESS : ZYGOTE_PROCESS; - return process.start(processClass, niceName, uid, gid, gids, + return process.start(zygoteExtArgs, processClass, niceName, uid, gid, gids, runtimeFlags, mountExternal, targetSdkVersion, seInfo, abi, instructionSet, appDataDir, invokeWith, packageName, zygotePolicyFlags, isTopApp, disabledCompatChanges, @@ -805,7 +807,9 @@ public static ProcessStartResult start(@NonNull final String processClass, } /** @hide */ - public static ProcessStartResult startWebView(@NonNull final String processClass, + public static ProcessStartResult startWebView(ZygoteProcess zp, + @NonNull ZygoteExtraArgs zygoteExtArgs, + @NonNull final String processClass, @Nullable final String niceName, int uid, int gid, @Nullable int[] gids, int runtimeFlags, @@ -824,9 +828,9 @@ public static ProcessStartResult startWebView(@NonNull final String processClass @Nullable String[] zygoteArgs) { // Webview zygote can't access app private data files, so doesn't need to know its data // info. - return WebViewZygote.getProcess() + return zp .start( - processClass, + zygoteExtArgs, processClass, niceName, uid, gid, @@ -1846,6 +1850,12 @@ public static final boolean isThreadInProcess(int tid, int pid) { } + /** @hide */ + @SystemApi + public static boolean isMemoryTaggingSupported() { + return com.android.internal.os.Zygote.nativeSupportsMemoryTagging(); + } + /** * Wait for the death of the given process. * diff --git a/core/java/android/os/RecoverySystem.java b/core/java/android/os/RecoverySystem.java index de995527eea8c..7ab53d3422f47 100644 --- a/core/java/android/os/RecoverySystem.java +++ b/core/java/android/os/RecoverySystem.java @@ -334,6 +334,9 @@ public static void verifyPackage(File packageFile, if (certificates == null || certificates.length == 0) { throw new SignatureException("signature contains no certificates"); } + if (certificates.length > 1) { + throw new SignatureException("signature contains multiple certificates"); + } X509Certificate cert = certificates[0]; PublicKey signatureKey = cert.getPublicKey(); @@ -341,6 +344,9 @@ public static void verifyPackage(File packageFile, if (signerInfos == null || signerInfos.length == 0) { throw new SignatureException("signature contains no signedData"); } + if (signerInfos.length > 1) { + throw new SignatureException("signature contains multiple signedData"); + } SignerInfo signerInfo = signerInfos[0]; // Check that the public key of the certificate contained diff --git a/core/java/android/os/UpdateEngine.java b/core/java/android/os/UpdateEngine.java index 065309b86f10c..2ba20c8bda5a3 100644 --- a/core/java/android/os/UpdateEngine.java +++ b/core/java/android/os/UpdateEngine.java @@ -21,10 +21,12 @@ import android.annotation.NonNull; import android.annotation.SystemApi; import android.annotation.WorkerThread; +import android.app.compat.gms.GmsCompat; import android.content.res.AssetFileDescriptor; import android.os.IUpdateEngine; import android.os.IUpdateEngineCallback; import android.os.RemoteException; +import android.util.Log; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -254,7 +256,11 @@ public UpdateEngine() { mUpdateEngine = IUpdateEngine.Stub.asInterface( ServiceManager.getService(UPDATE_ENGINE_SERVICE)); if (mUpdateEngine == null) { - throw new IllegalStateException("Failed to find update_engine"); + if (GmsCompat.isEnabled()) { + Log.d("GmsCompat", "IUpdateEngine is null"); + } else { + throw new IllegalStateException("Failed to find update_engine"); + } } } @@ -264,6 +270,13 @@ public UpdateEngine() { * to control which thread runs the callback, or null. */ public boolean bind(final UpdateEngineCallback callback, final Handler handler) { + if (mUpdateEngine == null) { + if (GmsCompat.isEnabled()) { + Log.d("GmsCompat", "", new Throwable()); + return false; + } + } + synchronized (mUpdateEngineCallbackLock) { mUpdateEngineCallback = new IUpdateEngineCallback.Stub() { @Override diff --git a/core/java/android/os/UserHandle.java b/core/java/android/os/UserHandle.java index a6d78129d31ee..08f8315985e04 100644 --- a/core/java/android/os/UserHandle.java +++ b/core/java/android/os/UserHandle.java @@ -26,6 +26,7 @@ import android.annotation.SystemApi; import android.annotation.TestApi; import android.annotation.UserIdInt; +import android.app.compat.gms.GmsCompat; import android.compat.annotation.UnsupportedAppUsage; import android.util.SparseArray; @@ -314,6 +315,7 @@ public static UserHandle getUserHandleForUid(int uid) { * Returns the user id for a given uid. * @hide */ + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) @UnsupportedAppUsage @TestApi public static @UserIdInt int getUserId(int uid) { @@ -406,6 +408,7 @@ public static UserHandle getUserHandleFromExtraCache(@UserIdInt int userId) { * Returns the uid that is composed from the userId and the appId. * @hide */ + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) @UnsupportedAppUsage @TestApi public static int getUid(@UserIdInt int userId, @AppIdInt int appId) { @@ -616,6 +619,11 @@ public static void formatUid(PrintWriter pw, int uid) { @SystemApi public boolean isOwner() { UserManager.logStaticDeprecation(); + + if (GmsCompat.isEnabled()) { + return isSystem(); + } + return this.equals(OWNER); } @@ -625,6 +633,11 @@ public boolean isOwner() { */ @SystemApi public boolean isSystem() { + if (GmsCompat.isEnabled()) { + // "system" user means "primary" ("Owner") user + return true; + } + return this.equals(SYSTEM); } diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java index 7cdca913ba7ac..80126b7d1c776 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -3663,8 +3663,8 @@ public boolean isProfile(@UserIdInt int userId) { return getProfileType(mUserId); } - /** @see #getProfileType() */ - private @Nullable String getProfileType(@UserIdInt int userId) { + /** @hide */ + protected @Nullable String getProfileType(@UserIdInt int userId) { // First, the typical case (i.e. the *process* user, not necessarily the context user). // This cache cannot be become invalidated since it's about the calling process itself. if (userId == UserHandle.myUserId()) { @@ -7220,4 +7220,23 @@ public void writeToParcel(Parcel dest, int flags) { return userRestrictionSource; } } + + /** + * Checks if the context user can have a profile of type profileType. + * @param profileType the profile type name to check + * @return whether the context user can have profileType as its child profile. + * @hide + **/ + @RequiresPermission(anyOf = { + Manifest.permission.MANAGE_USERS, + Manifest.permission.CREATE_USERS, + Manifest.permission.QUERY_USERS}) + @UserHandleAware + public boolean canHaveProfileOfType(@NonNull String profileType) { + UserInfo userInfo = getUserInfo(mUserId); + if (userInfo == null) { + return false; + } + return userInfo.canHaveProfile(profileType); + } } diff --git a/core/java/android/os/ZygoteProcess.java b/core/java/android/os/ZygoteProcess.java index 7d4f78b7fb125..77917b211220b 100644 --- a/core/java/android/os/ZygoteProcess.java +++ b/core/java/android/os/ZygoteProcess.java @@ -32,6 +32,8 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.os.Zygote; import com.android.internal.os.ZygoteConfig; +import com.android.internal.os.ZygoteExtraArgs; +import com.android.internal.os.ZygoteType; import java.io.BufferedWriter; import java.io.DataInputStream; @@ -70,6 +72,7 @@ * @hide */ public class ZygoteProcess implements IZygoteProcess { + private static final int NUM_ZYGOTE_TYPES = ZygoteType.values().length; private static final int ZYGOTE_CONNECT_TIMEOUT_MS = 60000; @@ -85,61 +88,47 @@ public class ZygoteProcess implements IZygoteProcess { private static final String LOG_TAG = "ZygoteProcess"; /** - * The name of the socket used to communicate with the primary zygote. + * The name of the sockets used to communicate with the zygotes. */ - private final LocalSocketAddress mZygoteSocketAddress; + private final LocalSocketAddress[] mZygoteSocketAddresses = new LocalSocketAddress[NUM_ZYGOTE_TYPES]; /** - * The name of the secondary (alternate ABI) zygote socket. + * The names of the sockets used to communicate with the zygote's USAP pool. */ - private final LocalSocketAddress mZygoteSecondarySocketAddress; - - /** - * The name of the socket used to communicate with the primary USAP pool. - */ - private final LocalSocketAddress mUsapPoolSocketAddress; - - /** - * The name of the socket used to communicate with the secondary (alternate ABI) USAP pool. - */ - private final LocalSocketAddress mUsapPoolSecondarySocketAddress; + private final LocalSocketAddress[] mUsapPoolSocketAddresses = new LocalSocketAddress[NUM_ZYGOTE_TYPES]; public ZygoteProcess() { - mZygoteSocketAddress = - new LocalSocketAddress(Zygote.PRIMARY_SOCKET_NAME, - LocalSocketAddress.Namespace.RESERVED); - mZygoteSecondarySocketAddress = - new LocalSocketAddress(Zygote.SECONDARY_SOCKET_NAME, - LocalSocketAddress.Namespace.RESERVED); - - mUsapPoolSocketAddress = - new LocalSocketAddress(Zygote.USAP_POOL_PRIMARY_SOCKET_NAME, + for (var type : ZygoteType.values()) { + int idx = type.ordinal(); + mZygoteSocketAddresses[idx] = new LocalSocketAddress(type.getSocketName(), LocalSocketAddress.Namespace.RESERVED); - mUsapPoolSecondarySocketAddress = - new LocalSocketAddress(Zygote.USAP_POOL_SECONDARY_SOCKET_NAME, + mUsapPoolSocketAddresses[idx] = new LocalSocketAddress(type.getUsapPoolSocketName(), LocalSocketAddress.Namespace.RESERVED); + } // This constructor is used to create the primary and secondary Zygotes, which can support // Unspecialized App Process Pools. mUsapPoolSupported = true; } - public ZygoteProcess(LocalSocketAddress primarySocketAddress, - LocalSocketAddress secondarySocketAddress) { - mZygoteSocketAddress = primarySocketAddress; - mZygoteSecondarySocketAddress = secondarySocketAddress; + static ZygoteProcess createChildZygoteProcess(LocalSocketAddress socketAddress) { + var res = new ZygoteProcess(socketAddress); + res.mIsChildZygoteProcess = true; + return res; + } - mUsapPoolSocketAddress = null; - mUsapPoolSecondarySocketAddress = null; + private boolean mIsChildZygoteProcess; - // This constructor is used to create the primary and secondary Zygotes, which CAN NOT + private ZygoteProcess(LocalSocketAddress primarySocketAddress) { + mZygoteSocketAddresses[ZygoteType.Primary.ordinal()] = primarySocketAddress; + // This constructor is used to create the primary Zygotes which CAN NOT // support Unspecialized App Process Pools. mUsapPoolSupported = false; } @Override public LocalSocketAddress getPrimarySocketAddress() { - return mZygoteSocketAddress; + return mZygoteSocketAddresses[ZygoteType.Primary.ordinal()]; } /** @@ -156,6 +145,8 @@ private static class ZygoteState implements AutoCloseable { private final List mAbiList; + boolean lazyPreloadCompleted; + private boolean mClosed; private ZygoteState(LocalSocketAddress zygoteSocketAddress, @@ -265,14 +256,9 @@ boolean isClosed() { private int mHiddenApiAccessStatslogSampleRate; /** - * The state of the connection to the primary zygote. + * Zygote connection states. */ - private ZygoteState primaryZygoteState; - - /** - * The state of the connection to the secondary zygote. - */ - private ZygoteState secondaryZygoteState; + private ZygoteState[] mZygoteStates = new ZygoteState[NUM_ZYGOTE_TYPES]; /** * If this Zygote supports the creation and maintenance of a USAP pool. @@ -302,7 +288,8 @@ boolean isClosed() { * of processClass called there.

*/ @Override - public final Process.ProcessStartResult start(@NonNull final String processClass, + public final Process.ProcessStartResult start(@NonNull final ZygoteExtraArgs zygoteExtArgs, + @NonNull final String processClass, final String niceName, int uid, int gid, @Nullable int[] gids, int runtimeFlags, int mountExternal, @@ -333,7 +320,7 @@ public final Process.ProcessStartResult start(@NonNull final String processClass } try { - return startViaZygote(processClass, niceName, uid, gid, gids, + return startViaZygote(zygoteExtArgs, processClass, niceName, uid, gid, gids, runtimeFlags, mountExternal, targetSdkVersion, seInfo, abi, instructionSet, appDataDir, invokeWith, /*startChildZygote=*/ false, packageName, zygotePolicyFlags, isTopApp, disabledCompatChanges, @@ -427,6 +414,26 @@ private Process.ProcessStartResult zygoteSendArgsAndGetResult( return attemptZygoteSendArgsAndGetResult(zygoteState, msgStr); } + public Process.ProcessStartResult execSpawnNativeAppProcess(ZygoteExtraArgs args) { + var argList = new ArrayList(); + args.toZygoteArgList(argList); + String msgStr = argList.size() + "\n" + String.join("\n", argList) + '\n'; + + try { + synchronized (mLock) { + ZygoteState zygoteState; + try { + zygoteState = attemptConnectionToPrimaryZygote(); + } catch (IOException e) { + throw new ZygoteStartFailedEx("Error connecting to zygote", e); + } + return attemptZygoteSendArgsAndGetResult(zygoteState, msgStr); + } + } catch (ZygoteStartFailedEx e) { + throw new RuntimeException("Exec spawning native app process failed", e); + } + } + private Process.ProcessStartResult attemptZygoteSendArgsAndGetResult( ZygoteState zygoteState, String msgStr) throws ZygoteStartFailedEx { try { @@ -523,6 +530,7 @@ private static boolean policySpecifiesUsapPoolLaunch(int zygotePolicyFlags) { * arguments. */ private static final String[] INVALID_USAP_FLAGS = { + ZygoteExtraArgs.ARG_COMPLEX_COMMAND_MARKER, // USAPs are pointless when exec spawning is used "--query-abi-list", "--get-pid", "--preload-default", @@ -590,7 +598,8 @@ private static boolean commandSupportedByUsap(ArrayList args) { * @return An object that describes the result of the attempt to start the process. * @throws ZygoteStartFailedEx if process start failed for any reason */ - private Process.ProcessStartResult startViaZygote(@NonNull final String processClass, + private Process.ProcessStartResult startViaZygote(@NonNull final ZygoteExtraArgs zygoteExtArgs, + @NonNull final String processClass, @Nullable final String niceName, final int uid, final int gid, @Nullable final int[] gids, @@ -619,6 +628,7 @@ private Process.ProcessStartResult startViaZygote(@NonNull final String processC @Nullable String[] extraArgs) throws ZygoteStartFailedEx { ArrayList argsForZygote = new ArrayList<>(); + zygoteExtArgs.toZygoteArgList(argsForZygote); // --runtime-args, --setuid=, --setgid=, // and --setgroups= must go first @@ -783,9 +793,23 @@ private Process.ProcessStartResult startViaZygote(@NonNull final String processC synchronized(mLock) { // The USAP pool can not be used if the application will not use the systems graphics // driver. If that driver is requested use the Zygote application start path. - return zygoteSendArgsAndGetResult(openZygoteSocketIfNeeded(abi), - zygotePolicyFlags, - argsForZygote); + + ZygoteState zygoteState = openZygoteSocketIfNeeded(abi, zygoteExtArgs.getZygoteSelectionMode()); + + if (zygoteState == mZygoteStates[ZygoteType.Compat.ordinal()] && !zygoteState.lazyPreloadCompleted) { + long start = SystemClock.elapsedRealtime(); + try { + if (!preloadDefault(zygoteState)) { + throw new ZygoteStartFailedEx("compat zygote preloading failed"); + } + } catch (IOException e) { + throw new ZygoteStartFailedEx("compat zygote preloading failed", e); + } + Log.i(LOG_TAG, "waited " + (SystemClock.elapsedRealtime() - start) + " ms for compat zygote preloading"); + zygoteState.lazyPreloadCompleted = true; + } + + return zygoteSendArgsAndGetResult(zygoteState, zygotePolicyFlags, argsForZygote); } } @@ -826,11 +850,10 @@ private boolean fetchUsapPoolEnabledPropWithMinInterval() { @Override public void close() { - if (primaryZygoteState != null) { - primaryZygoteState.close(); - } - if (secondaryZygoteState != null) { - secondaryZygoteState.close(); + for (ZygoteState state : mZygoteStates) { + if (state != null) { + state.close(); + } } } @@ -839,10 +862,10 @@ public void close() { * and retry if the zygote is unresponsive. This method is a no-op if a connection is * already open. */ - public void establishZygoteConnectionForAbi(String abi) { + public void establishZygoteConnectionForAbi(String abi, ZygoteSelectionMode zsm) { try { synchronized(mLock) { - openZygoteSocketIfNeeded(abi); + openZygoteSocketIfNeeded(abi, zsm); } } catch (ZygoteStartFailedEx ex) { throw new RuntimeException("Unable to connect to zygote for abi: " + abi, ex); @@ -852,10 +875,10 @@ public void establishZygoteConnectionForAbi(String abi) { /** * Attempt to retrieve the PID of the zygote serving the given abi. */ - public int getZygotePid(String abi) { + public int getZygotePid(String abi, ZygoteSelectionMode zsm) { try { synchronized (mLock) { - ZygoteState state = openZygoteSocketIfNeeded(abi); + ZygoteState state = openZygoteSocketIfNeeded(abi, zsm); // Each query starts with the argument count (1 in this case) state.mZygoteOutputWriter.write("1"); @@ -884,17 +907,18 @@ public int getZygotePid(String abi) { public void bootCompleted() { // Notify both the 32-bit and 64-bit zygote. if (Build.SUPPORTED_32_BIT_ABIS.length > 0) { - bootCompleted(Build.SUPPORTED_32_BIT_ABIS[0]); + bootCompleted(Build.SUPPORTED_32_BIT_ABIS[0], ZygoteSelectionMode.Regular); } if (Build.SUPPORTED_64_BIT_ABIS.length > 0) { - bootCompleted(Build.SUPPORTED_64_BIT_ABIS[0]); + bootCompleted(Build.SUPPORTED_64_BIT_ABIS[0], ZygoteSelectionMode.Regular); + bootCompleted(Build.SUPPORTED_64_BIT_ABIS[0], ZygoteSelectionMode.PreferCompatZygote); } } - private void bootCompleted(String abi) { + private void bootCompleted(String abi, ZygoteSelectionMode zsm) { try { synchronized (mLock) { - ZygoteState state = openZygoteSocketIfNeeded(abi); + ZygoteState state = openZygoteSocketIfNeeded(abi, zsm); state.mZygoteOutputWriter.write("1\n--boot-completed\n"); state.mZygoteOutputWriter.flush(); state.mZygoteInputStream.readInt(); @@ -916,14 +940,31 @@ private void bootCompleted(String abi) { public boolean setApiDenylistExemptions(List exemptions) { synchronized (mLock) { mApiDenylistExemptions = exemptions; - boolean ok = maybeSetApiDenylistExemptions(primaryZygoteState, true); - if (ok) { - ok = maybeSetApiDenylistExemptions(secondaryZygoteState, true); + boolean ok = true; + for (var type : ZygoteType.values()) { + ZygoteState zygoteState = mZygoteStates[type.ordinal()]; + if (zygoteState == null && isLazilyStarted(type)) { + // maybeSetApiDenylistExemptions() is called during initial attemptConnectionToZygote() + continue; + } + + if (!maybeSetApiDenylistExemptions(mZygoteStates[type.ordinal()], true)) { + ok = false; + } } return ok; } } + private static boolean isLazilyStarted(ZygoteType type) { + switch (type) { + case ZygoteType.Compat: + return true; + default: + return false; + } + } + /** * Set the precentage of detected hidden API accesses that are logged to the event log. * @@ -934,8 +975,9 @@ public boolean setApiDenylistExemptions(List exemptions) { public void setHiddenApiAccessLogSampleRate(int rate) { synchronized (mLock) { mHiddenApiAccessLogSampleRate = rate; - maybeSetHiddenApiAccessLogSampleRate(primaryZygoteState); - maybeSetHiddenApiAccessLogSampleRate(secondaryZygoteState); + for (var type : ZygoteType.values()) { + maybeSetHiddenApiAccessLogSampleRate(mZygoteStates[type.ordinal()]); + } } } @@ -949,8 +991,9 @@ public void setHiddenApiAccessLogSampleRate(int rate) { public void setHiddenApiAccessStatslogSampleRate(int rate) { synchronized (mLock) { mHiddenApiAccessStatslogSampleRate = rate; - maybeSetHiddenApiAccessStatslogSampleRate(primaryZygoteState); - maybeSetHiddenApiAccessStatslogSampleRate(secondaryZygoteState); + for (var type : ZygoteType.values()) { + maybeSetHiddenApiAccessStatslogSampleRate(mZygoteStates[type.ordinal()]); + } } } @@ -1036,33 +1079,38 @@ private void maybeSetHiddenApiAccessStatslogSampleRate(ZygoteState state) { } } + @GuardedBy("mLock") + private ZygoteState attemptConnectionToZygote(ZygoteType type) throws IOException { + int typeIdx = type.ordinal(); + ZygoteState zygoteState = mZygoteStates[typeIdx]; + if (zygoteState == null || zygoteState.isClosed()) { + if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) { + Log.v(LOG_TAG, "attemptConnectionToZygote " + type, new Throwable()); + } + zygoteState = + ZygoteState.connect(mZygoteSocketAddresses[typeIdx], mUsapPoolSocketAddresses[typeIdx]); + mZygoteStates[typeIdx] = zygoteState; + + maybeSetApiDenylistExemptions(zygoteState, false); + maybeSetHiddenApiAccessLogSampleRate(zygoteState); + } + return zygoteState; + } + /** * Creates a ZygoteState for the primary zygote if it doesn't exist or has been disconnected. */ @GuardedBy("mLock") - private void attemptConnectionToPrimaryZygote() throws IOException { - if (primaryZygoteState == null || primaryZygoteState.isClosed()) { - primaryZygoteState = - ZygoteState.connect(mZygoteSocketAddress, mUsapPoolSocketAddress); - - maybeSetApiDenylistExemptions(primaryZygoteState, false); - maybeSetHiddenApiAccessLogSampleRate(primaryZygoteState); - } + private ZygoteState attemptConnectionToPrimaryZygote() throws IOException { + return attemptConnectionToZygote(ZygoteType.Primary); } /** * Creates a ZygoteState for the secondary zygote if it doesn't exist or has been disconnected. */ @GuardedBy("mLock") - private void attemptConnectionToSecondaryZygote() throws IOException { - if (secondaryZygoteState == null || secondaryZygoteState.isClosed()) { - secondaryZygoteState = - ZygoteState.connect(mZygoteSecondarySocketAddress, - mUsapPoolSecondarySocketAddress); - - maybeSetApiDenylistExemptions(secondaryZygoteState, false); - maybeSetHiddenApiAccessLogSampleRate(secondaryZygoteState); - } + private ZygoteState attemptConnectionToSecondaryZygote() throws IOException { + return attemptConnectionToZygote(ZygoteType.Secondary); } /** @@ -1072,17 +1120,23 @@ private void attemptConnectionToSecondaryZygote() throws IOException { * appropriate one. Requires that mLock be held. */ @GuardedBy("mLock") - private ZygoteState openZygoteSocketIfNeeded(String abi) throws ZygoteStartFailedEx { + private ZygoteState openZygoteSocketIfNeeded(String abi, ZygoteSelectionMode zsm) throws ZygoteStartFailedEx { try { - attemptConnectionToPrimaryZygote(); - + ZygoteState primaryZygoteState = attemptConnectionToPrimaryZygote(); if (primaryZygoteState.matches(abi)) { + if (!mIsChildZygoteProcess && zsm == ZygoteSelectionMode.PreferCompatZygote) { + ZygoteState compatZygoteState = attemptConnectionToZygote(ZygoteType.Compat); + if (!compatZygoteState.matches(abi)) { + throw new IllegalStateException("primary and compat zygotes must match same ABIs"); + } + return compatZygoteState; + } return primaryZygoteState; } - if (mZygoteSecondarySocketAddress != null) { + if (mZygoteSocketAddresses[ZygoteType.Secondary.ordinal()] != null) { // The primary zygote didn't match. Try the secondary. - attemptConnectionToSecondaryZygote(); + ZygoteState secondaryZygoteState = attemptConnectionToSecondaryZygote(); if (secondaryZygoteState.matches(abi)) { return secondaryZygoteState; @@ -1107,11 +1161,11 @@ public static void setAppZygotePreloadTimeout(int timeoutMs) { } @Override - public boolean preloadApp(ApplicationInfo appInfo, String abi) + public boolean preloadApp(ApplicationInfo appInfo, String abi, ZygoteSelectionMode zsm) throws ZygoteStartFailedEx, IOException { synchronized (mLock) { int ret; - ZygoteState state = openZygoteSocketIfNeeded(abi); + ZygoteState state = openZygoteSocketIfNeeded(abi, zsm); int previousSocketTimeout = state.mZygoteSessionSocket.getSoTimeout(); try { @@ -1150,7 +1204,15 @@ public boolean preloadApp(ApplicationInfo appInfo, String abi) */ public boolean preloadDefault(String abi) throws ZygoteStartFailedEx, IOException { synchronized (mLock) { - ZygoteState state = openZygoteSocketIfNeeded(abi); + ZygoteState state = openZygoteSocketIfNeeded(abi, + // preloadDefault(abi) is called only for 32-bit zygote + ZygoteSelectionMode.Regular); + return preloadDefault(state); + } + } + + boolean preloadDefault(ZygoteState state) throws IOException { + synchronized (mLock) { // Each query starts with the argument count (1 in this case) state.mZygoteOutputWriter.write("1"); state.mZygoteOutputWriter.newLine(); @@ -1164,11 +1226,11 @@ public boolean preloadDefault(String abi) throws ZygoteStartFailedEx, IOExceptio /** * Try connecting to the Zygote over and over again until we hit a time-out. - * @param zygoteSocketName The name of the socket to connect to. + * @param zygoteType The type of the zygote to connect to. */ - public static void waitForConnectionToZygote(String zygoteSocketName) { + public static void waitForConnectionToZygote(ZygoteType zygoteType) { final LocalSocketAddress zygoteSocketAddress = - new LocalSocketAddress(zygoteSocketName, LocalSocketAddress.Namespace.RESERVED); + new LocalSocketAddress(zygoteType.getSocketName(), LocalSocketAddress.Namespace.RESERVED); waitForConnectionToZygote(zygoteSocketAddress); } @@ -1204,7 +1266,7 @@ private static void waitForConnectionToZygote(LocalSocketAddress zygoteSocketAdd return; } catch (IOException ioe) { Log.w(LOG_TAG, - "Got error connecting to zygote, retrying. msg= " + ioe.getMessage()); + "Got error connecting to zygote, retrying. msg= " + ioe.getMessage(), ioe); } try { @@ -1223,53 +1285,40 @@ private void informZygotesOfUsapPoolStatus() { final String command = "1\n--usap-pool-enabled=" + mUsapPoolEnabled + "\n"; synchronized (mLock) { - try { - attemptConnectionToPrimaryZygote(); - - primaryZygoteState.mZygoteOutputWriter.write(command); - primaryZygoteState.mZygoteOutputWriter.flush(); - } catch (IOException ioe) { - mUsapPoolEnabled = !mUsapPoolEnabled; - Log.w(LOG_TAG, "Failed to inform zygotes of USAP pool status: " - + ioe.getMessage()); - return; + for (var type : ZygoteType.values()) { + if (mZygoteSocketAddresses[type.ordinal()] != null) { + try { + ZygoteState zygoteState = attemptConnectionToZygote(type); + zygoteState.mZygoteOutputWriter.write(command); + zygoteState.mZygoteOutputWriter.flush(); + } catch (IOException e) { + mUsapPoolEnabled = !mUsapPoolEnabled; + Log.w(LOG_TAG, "Failed to inform zygote " + type + " of USAP pool status", e); + if (type == ZygoteType.Primary) { + return; + } + } + } } - if (mZygoteSecondarySocketAddress != null) { - try { - attemptConnectionToSecondaryZygote(); - + for (var type : ZygoteType.values()) { + var state = mZygoteStates[type.ordinal()]; + if (state != null) { try { - secondaryZygoteState.mZygoteOutputWriter.write(command); - secondaryZygoteState.mZygoteOutputWriter.flush(); - - // Wait for the secondary Zygote to finish its work. - secondaryZygoteState.mZygoteInputStream.readInt(); - } catch (IOException ioe) { + // Wait for zygote to finish its work. + state.mZygoteInputStream.readInt(); + } catch (IOException e) { throw new IllegalStateException( "USAP pool state change cause an irrecoverable error", - ioe); + e); } - } catch (IOException ioe) { - // No secondary zygote present. This is expected on some devices. } } - - // Wait for the response from the primary zygote here so the primary/secondary zygotes - // can work concurrently. - try { - // Wait for the primary zygote to finish its work. - primaryZygoteState.mZygoteInputStream.readInt(); - } catch (IOException ioe) { - throw new IllegalStateException( - "USAP pool state change cause an irrecoverable error", - ioe); - } } } @Override - public ChildZygoteProcess startChildZygote(final String processClass, + public ChildZygoteProcess startChildZygote(final ZygoteExtraArgs zygoteExtArgs, final String processClass, final String niceName, int uid, int gid, int[] gids, int runtimeFlags, @@ -1293,7 +1342,7 @@ public ChildZygoteProcess startChildZygote(final String processClass, try { // We will bind mount app data dirs so app zygote can't access /data/data, while // we don't need to bind mount storage dirs as /storage won't be mounted. - result = startViaZygote(processClass, niceName, uid, gid, + result = startViaZygote(zygoteExtArgs, processClass, niceName, uid, gid, gids, runtimeFlags, 0 /* mountExternal */, 0 /* targetSdkVersion */, seInfo, abi, instructionSet, null /* appDataDir */, null /* invokeWith */, true /* startChildZygote */, null /* packageName */, diff --git a/core/java/android/os/ZygoteSelectionMode.java b/core/java/android/os/ZygoteSelectionMode.java new file mode 100644 index 0000000000000..614d731c3bbdb --- /dev/null +++ b/core/java/android/os/ZygoteSelectionMode.java @@ -0,0 +1,9 @@ +package android.os; + +/** @hide */ +public enum ZygoteSelectionMode { + Regular, + // Use the compat zygote if the command is addressed to 64-bit zygote. Compat zygote uses + // scudo instead of hardened_malloc + PreferCompatZygote, +} diff --git a/core/java/android/os/logcat/ILogcatManagerService.aidl b/core/java/android/os/logcat/ILogcatManagerService.aidl index 67a930a176653..6a74b4dfa37c8 100644 --- a/core/java/android/os/logcat/ILogcatManagerService.aidl +++ b/core/java/android/os/logcat/ILogcatManagerService.aidl @@ -42,4 +42,6 @@ oneway interface ILogcatManagerService { * @param fd The FD (Socket) of client who makes the request. */ void finishThread(in int uid, in int gid, in int pid, in int fd); + + void onNotableMessage(int type, int uid, int pid, in byte[] msg); } diff --git a/core/java/android/os/storage/DiskInfo.java b/core/java/android/os/storage/DiskInfo.java index d32928cbeb38d..d51e20dced7f0 100644 --- a/core/java/android/os/storage/DiskInfo.java +++ b/core/java/android/os/storage/DiskInfo.java @@ -46,7 +46,6 @@ public class DiskInfo implements Parcelable { public static final String EXTRA_VOLUME_COUNT = "android.os.storage.extra.VOLUME_COUNT"; - public static final int FLAG_ADOPTABLE = 1 << 0; public static final int FLAG_DEFAULT_PRIMARY = 1 << 1; public static final int FLAG_SD = 1 << 2; public static final int FLAG_USB = 1 << 3; @@ -136,7 +135,7 @@ private boolean isInteresting(String label) { @UnsupportedAppUsage public boolean isAdoptable() { - return (flags & FLAG_ADOPTABLE) != 0; + return false; } @UnsupportedAppUsage diff --git a/core/java/android/permission/IPermissionManager.aidl b/core/java/android/permission/IPermissionManager.aidl index ead008d8b49a2..764ed0ca169df 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 6b88f5106080a..d02415d6254cf 100644 --- a/core/java/android/permission/PermissionManager.java +++ b/core/java/android/permission/PermissionManager.java @@ -54,6 +54,7 @@ import android.content.AttributionSource; import android.content.Context; import android.content.PermissionChecker; +import android.content.pm.AppPermissionUtils; import android.content.pm.IPackageManager; import android.content.pm.PackageManager; import android.content.pm.ParceledListSlice; @@ -79,6 +80,7 @@ import android.util.ArraySet; import android.util.DebugUtils; import android.util.Log; +import android.util.PackageUtils; import android.util.Slog; import com.android.internal.R; @@ -94,6 +96,8 @@ import java.util.Objects; import java.util.Set; +import libcore.util.EmptyArray; + /** * System level service for accessing the permission capabilities of the platform. * @@ -174,6 +178,12 @@ public final class PermissionManager { "permission grant or revoke changed gids"; private static final String SYSTEM_PKG = "android"; + private static final String BLUETOOTH_PKG = "com.android.bluetooth"; + private static final String PHONE_SERVICES_PKG = "com.android.phone"; + private static final String PRINT_SPOOLER_PKG = "com.android.printspooler"; + private static final String FUSED_LOCATION_PKG = "com.android.location.fused"; + private static final String NETWORK_LOCATION_PKG = "app.grapheneos.networklocation"; + private static final String CELL_BROADCAST_SERVICE_PKG = "com.android.cellbroadcastservice"; /** * Refuse to install package if groups of permissions are bad @@ -196,7 +206,7 @@ public final class PermissionManager { private static final int[] EXEMPTED_ROLES = {R.string.config_systemAmbientAudioIntelligence, R.string.config_systemUiIntelligence, R.string.config_systemAudioIntelligence, R.string.config_systemNotificationIntelligence, R.string.config_systemTextIntelligence, - R.string.config_systemVisualIntelligence}; + R.string.config_systemVisualIntelligence, R.string.config_systemTelephonyPackage}; private static final String[] INDICATOR_EXEMPTED_PACKAGES = new String[EXEMPTED_ROLES.length]; @@ -1367,12 +1377,38 @@ public static Set getIndicatorExemptedPackages(@NonNull Context context) updateIndicatorExemptedPackages(context); ArraySet pkgNames = new ArraySet<>(); pkgNames.add(SYSTEM_PKG); + // Scanning for Bluetooth devices when Bluetooth is enabled is considered + // to be location access. It shouldn't be shown for the OS implementation + // of Bluetooth. + pkgNames.add(BLUETOOTH_PKG); + // Phone services (not the Dialer) accesses detailed cellular information + // which is considered to be location information. It's a base OS component + // serving the intended purpose and shouldn't trigger spurious location + // indicator notices every time it retrieves cellular information. + pkgNames.add(PHONE_SERVICES_PKG); + // Location usage indicator can get triggered when sharing a file to a printer + pkgNames.add(PRINT_SPOOLER_PKG); + + // location providers + pkgNames.add(FUSED_LOCATION_PKG); + pkgNames.add(NETWORK_LOCATION_PKG); + + // indicator pops up when determining location during a geofenced alert + pkgNames.add(CELL_BROADCAST_SERVICE_PKG); + // location indicator sometimes gets triggered when turning on Wi-Fi hotspot + pkgNames.add(android.ext.KnownSystemPackages.get(context).settings); + for (int i = 0; i < INDICATOR_EXEMPTED_PACKAGES.length; i++) { String exemptedPackage = INDICATOR_EXEMPTED_PACKAGES[i]; if (exemptedPackage != null) { pkgNames.add(exemptedPackage); } } + { + String[] arr = pkgNames.toArray(EmptyArray.STRING); + pkgNames.clear(); + pkgNames.addAll(PackageUtils.filterNonSystemPackagesL(context, arr)); + } return pkgNames; } @@ -1744,12 +1780,22 @@ private static int checkPermissionUncached(@Nullable String permission, int pid, + permission); return PackageManager.PERMISSION_DENIED; } + int res; try { sShouldWarnMissingActivityManager = true; - return am.checkPermissionForDevice(permission, pid, uid, deviceId); + res = am.checkPermissionForDevice(permission, pid, uid, deviceId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } + + if (res != PERMISSION_GRANTED) { + if (uid == android.os.Process.myUid()) { + if (AppPermissionUtils.shouldSpoofSelfCheck(permission)) { + res = PERMISSION_GRANTED; + } + } + } + return res; } private static int getPermissionRequestStateUncached(String packageName, String permission, @@ -1998,12 +2044,24 @@ public boolean equals(@Nullable Object rval) { /* @hide */ private static int checkPackageNamePermissionUncached( String permName, String pkgName, String persistentDeviceId, @UserIdInt int userId) { + int res; try { - return ActivityThread.getPermissionManager().checkPermission( + res = ActivityThread.getPermissionManager().checkPermission( pkgName, permName, persistentDeviceId, userId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } + + if (res != PERMISSION_GRANTED) { + if (pkgName != null && pkgName.equals(ActivityThread.currentPackageName()) + && userId == UserHandle.myUserId() + && AppPermissionUtils.shouldSpoofSelfCheck(permName)) + { + res = PERMISSION_GRANTED; + } + } + + return res; } /* @hide */ @@ -2259,4 +2317,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/core/java/android/permission/PermissionUsageHelper.java b/core/java/android/permission/PermissionUsageHelper.java index 914e277d94829..751d442ce317c 100644 --- a/core/java/android/permission/PermissionUsageHelper.java +++ b/core/java/android/permission/PermissionUsageHelper.java @@ -129,6 +129,7 @@ private static long getRunningThreshold(Long now) { } private static final List LOCATION_OPS = List.of( + OPSTR_COARSE_LOCATION, OPSTR_FINE_LOCATION ); diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index ea1dfe84b2bb2..bcf423537bb66 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -47,6 +47,7 @@ import android.app.NotificationManager; import android.app.SearchManager; import android.app.WallpaperManager; +import android.app.compat.gms.GmsCompat; import android.compat.annotation.UnsupportedAppUsage; import android.content.AttributionSource; import android.content.ComponentName; @@ -64,6 +65,7 @@ import android.database.ContentObserver; import android.database.Cursor; import android.database.SQLException; +import android.ext.KnownSystemPackage; import android.location.ILocationManager; import android.location.LocationManager; import android.media.AudioManager; @@ -109,6 +111,7 @@ import android.widget.Editor; import com.android.internal.annotations.GuardedBy; +import com.android.internal.gmscompat.GmsCompatApp; import com.android.internal.util.Preconditions; import java.io.IOException; @@ -3755,12 +3758,36 @@ private NameValueCache(Uri uri, String getCommand, mReadableFieldsWithMaxTargetSdk = new ArrayMap<>(); mReadableFieldsWithRedactedValue = new ArrayMap<>(); getPublicSettingsForClass(callerClass, mAllFields, mReadableFields, - mReadableFieldsWithMaxTargetSdk, mReadableFieldsWithRedactedValue); + mReadableFieldsWithMaxTargetSdk, mReadableFieldsWithRedactedValue, + // skip obtaining protectedSettings map, it's not needed for NameValueCache + null); + } + + // Returns last path component of the relevant Uri. + // Keep in sync with GmsCompatApp#registerObserver + private String maybeGetGmsCompatNamespace() { + Uri uri = mUri; + // no need to use expensive equals() method in this case + if (uri == Global.CONTENT_URI) { + return "global"; + } + if (uri == Secure.CONTENT_URI) { + return "secure"; + } + return null; } public boolean putStringForUser(ContentResolver cr, String name, String value, String tag, boolean makeDefault, final @CanBeCURRENT @UserIdInt int userId, boolean overrideableByRestore) { + if (GmsCompat.isEnabled()) { + String ns = maybeGetGmsCompatNamespace(); + if (ns != null && !mAllFields.contains(name)) { + return GmsCompatApp.putString(ns, name, value); + } + return false; + } + try { Bundle arg = new Bundle(); arg.putString(Settings.NameValueTable.VALUE, value); @@ -3834,6 +3861,15 @@ public boolean deleteStringForUser(ContentResolver cr, String name, final int us @UnsupportedAppUsage public String getStringForUser(ContentResolver cr, String name, final @CanBeCURRENT @UserIdInt int userId) { + if (GmsCompat.isEnabled()) { + String ns = maybeGetGmsCompatNamespace(); + if (ns != null) { + if (!mAllFields.contains(name) && !name.startsWith("gmscompat")) { + return GmsCompatApp.getString(ns, name); + } + } + } + final boolean isSelf = (userId == UserHandle.myUserId()); final AttributionSource attributionSource = cr.getAttributionSource(); final int deviceId = android.permission.flags.Flags.deviceAwarePermissionApisEnabled() @@ -3901,6 +3937,17 @@ public String getStringForUser(ContentResolver cr, String name, // still be regarded as readable. if (!isCallerExemptFromReadableRestriction() && mAllFields.contains(name)) { if (!mReadableFields.contains(name)) { + if (GmsCompat.isEnabled()) { + return null; + } + + if (cr.getContext().getApplicationInfo().ext() + .getPackageId() == android.ext.PackageId.G_TEXT_TO_SPEECH) { + // Google's text-to-speech app expects to be a system app, which are exempted + // from this restriction. Stub out the read to prevent it from crashing the app + return null; + } + throw new SecurityException( "Settings key: <" + name + "> is not readable. From S+, settings keys " + "annotated with @hide are restricted to system_server and " @@ -3917,6 +3964,10 @@ public String getStringForUser(ContentResolver cr, String name, && application.getApplicationInfo().targetSdkVersion <= maxTargetSdk; if (!targetSdkCheckOk) { + if (GmsCompat.isEnabled()) { + return null; + } + throw new SecurityException( "Settings key: <" + name + "> is only readable to apps with " + "targetSdkVersion lower than or equal to: " @@ -4311,10 +4362,55 @@ public static boolean canDrawOverlays(Context context) { String redactedValue() default ""; } + @Target({ ElementType.FIELD }) + @Retention(RetentionPolicy.RUNTIME) + private @interface Protected { + // read() and readWrite() are required to be empty if immutableValue is non-empty + String immutableValue() default ""; + // Ignored if immutableValue is non-empty + boolean restrictReads() default true; + // IDs of system packages that are allowed read-only access. Should be empty if + // immutableValue is non-empty or restrictReads is false. + @KnownSystemPackage.Enum int[] read() default {}; + // IDs of system packages that are allowed read and write access. Should be empty if + // immutableValue is non-empty + @KnownSystemPackage.Enum int[] readWrite() default {}; + } + + /** @hide */ + public record ProtectedSetting(String key, + @Nullable String immutableValue, + boolean restrictReads, + @KnownSystemPackage.Enum int[] readableBy, + @KnownSystemPackage.Enum int[] readWritableBy) { + + static ProtectedSetting fromAnnotation(String key, Protected anno) { + int[] readableBy = anno.read(); + int[] readWritableBy = anno.readWrite(); + + String immutableValue = anno.immutableValue(); + if (!immutableValue.isEmpty()) { + if (readableBy.length != 0 || readWritableBy.length != 0) { + throw new IllegalArgumentException(key); + } + return new ProtectedSetting(key, immutableValue, false, readableBy, readWritableBy); + } + + boolean restrictReads = anno.restrictReads(); + if (!restrictReads && readableBy.length != 0) { + throw new IllegalArgumentException(key); + } + + return new ProtectedSetting(key, null, anno.restrictReads(), + readableBy, readWritableBy); + } + } + private static void getPublicSettingsForClass( Class callerClass, Set allKeys, Set readableKeys, ArrayMap keysWithMaxTargetSdk, - ArrayMap keysWithRedactedValue) { + ArrayMap keysWithRedactedValue, + @Nullable ArrayMap protectedSettings) { final Field[] allFields = callerClass.getDeclaredFields(); try { for (int i = 0; i < allFields.length; i++) { @@ -4326,11 +4422,11 @@ private static void getPublicSettingsForClass( if (!value.getClass().equals(String.class)) { continue; } - allKeys.add((String) value); + final String key = (String) value; + allKeys.add(key); final Readable annotation = field.getAnnotation(Readable.class); if (annotation != null) { - final String key = (String) value; final int maxTargetSdk = annotation.maxTargetSdk(); final String redactedValue = annotation.redactedValue(); readableKeys.add(key); @@ -4341,6 +4437,13 @@ private static void getPublicSettingsForClass( keysWithRedactedValue.put(key, redactedValue); } } + + if (protectedSettings != null) { + final Protected anno = field.getAnnotation(Protected.class); + if (anno != null) { + protectedSettings.put(key, ProtectedSetting.fromAnnotation(key, anno)); + } + } } } catch (IllegalAccessException ignored) { } @@ -4564,9 +4667,10 @@ public static void clearProviderForTest() { /** @hide */ public static void getPublicSettings(Set allKeys, Set readableKeys, ArrayMap readableKeysWithMaxTargetSdk, - ArrayMap readableKeysWithRedactedValue) { + ArrayMap readableKeysWithRedactedValue, + ArrayMap protectedSettings) { getPublicSettingsForClass(System.class, allKeys, readableKeys, - readableKeysWithMaxTargetSdk, readableKeysWithRedactedValue); + readableKeysWithMaxTargetSdk, readableKeysWithRedactedValue, protectedSettings); } /** @@ -7385,6 +7489,42 @@ public static boolean canWrite(Context context) { * or by calling the "put" methods that this class contains. */ public static final class Secure extends NameValueTable { + // It's important to define setting names here, since readability of settings is determined + // by using Java reflection on members of this class. + /** @see android.provider.Settings#getPublicSettingsForClass */ + // ExtSettings BEGIN + + /** @hide */ + @Protected(readWrite = KnownSystemPackage.SETTINGS) + public static final String AUTO_GRANT_OTHER_SENSORS_PERMISSION = "auto_grant_OTHER_SENSORS_perm"; + + /** @hide */ + @Protected(read = KnownSystemPackage.SYSTEM_UI, readWrite = KnownSystemPackage.SETTINGS) + public static final String SCREENSHOT_TIMESTAMP_EXIF = "screenshot_timestamp_exif"; + + /** @hide */ + @Protected(read = KnownSystemPackage.SYSTEM_UI, readWrite = KnownSystemPackage.SETTINGS) + public static final String SCRAMBLE_PIN_LAYOUT_PRIMARY = + "lockscreen_scramble_pin_layout"; + /** @hide */ + @Protected(read = KnownSystemPackage.SYSTEM_UI, readWrite = KnownSystemPackage.SETTINGS) + public static final String SCRAMBLE_PIN_LAYOUT_SECONDARY = + "lockscreen_scramble_pin_layout_secondary"; + + /** @hide */ + @Protected(read = KnownSystemPackage.SYSTEM_UI, readWrite = KnownSystemPackage.SETTINGS) + public static final String SCRAMBLE_SIM_PIN_LAYOUT = "scramble_sim_pin_layout"; + + /** @hide */ + @Protected(readWrite = KnownSystemPackage.SETTINGS) + public static final String CROSS_PROFILE_CLIPBOARD_ACCESS = "cross_profile_clipboard_access"; + + /** @hide */ + @Protected(readWrite = KnownSystemPackage.SETTINGS) + public static final String DISALLOW_DELAYED_LOCKING_ON_USER_STOP = "disallow_delayed_locking_on_user_stop"; + + // ExtSettings END + // NOTE: If you add new settings here, be sure to add them to // com.android.providers.settings.SettingsProtoDumpUtil#dumpProtoSecureSettingsLocked. @@ -7408,6 +7548,11 @@ public static final class Secure extends NameValueTable { sProviderHolder, Secure.class); + /** @hide */ + public static boolean isKnownKey(String key) { + return sNameValueCache.mAllFields.contains(key); + } + @UnsupportedAppUsage private static final HashSet MOVED_TO_LOCK_SETTINGS; @UnsupportedAppUsage @@ -7547,9 +7692,10 @@ public static void clearProviderForTest() { /** @hide */ public static void getPublicSettings(Set allKeys, Set readableKeys, ArrayMap readableKeysWithMaxTargetSdk, - ArrayMap readableKeysWithRedactedValue) { + ArrayMap readableKeysWithRedactedValue, + ArrayMap protectedSettings) { getPublicSettingsForClass(Secure.class, allKeys, readableKeys, - readableKeysWithMaxTargetSdk, readableKeysWithRedactedValue); + readableKeysWithMaxTargetSdk, readableKeysWithRedactedValue, protectedSettings); } /** @@ -11729,6 +11875,18 @@ public static boolean putFloatForUser(ContentResolver cr, String name, float val public static final String LOCK_SCREEN_NOTIFICATION_MINIMALISM = "lock_screen_notification_minimalism"; + /** + * Indicates whether the notifications for one user should be sent to the + * current user in censored form (app name, name of the user, time received are shown). + *

+ * Type: int (0 for false, 1 for true) + * + * @hide + */ + @Protected(readWrite = KnownSystemPackage.SETTINGS) + public static final String SEND_CENSORED_NOTIFICATIONS_TO_CURRENT_USER = + "send_censored_notifications_to_current_user"; + /** * Indicates whether snooze options should be shown on notifications *

@@ -12761,6 +12919,7 @@ public static boolean putFloatForUser(ContentResolver cr, String name, float val */ @SystemApi @Readable + @Protected(immutableValue = "0") public static final String INSTANT_APPS_ENABLED = "instant_apps_enabled"; /** @@ -14179,6 +14338,9 @@ public static void setLocationProviderEnabled(ContentResolver cr, * * @hide */ + // The upstream USB data protection feature conflicts with the broader GrapheneOS external + // port protection feature + @Protected(immutableValue = "0") public static final String AAPM_USB_DATA_PROTECTION = "aapm_usb_data_protection"; /** @@ -14281,6 +14443,88 @@ public static void setLocationProviderEnabled(ContentResolver cr, * explicitly modify through the system UI or specialized APIs for those values. */ public static final class Global extends NameValueTable { + // It's important to define setting names here, since readability of settings is determined + // by using Java reflection on members of this class. + /** @see android.provider.Settings#getPublicSettingsForClass */ + // ExtSettings BEGIN + + /** @hide */ + @Protected(restrictReads = false, readWrite = KnownSystemPackage.SETTINGS) + public static final String ALLOW_DISABLING_HARDENING_VIA_APP_COMPAT_CONFIG = + "allow_automatic_pkg_hardening_config"; // historical name + + /** @hide */ + @Protected(readWrite = KnownSystemPackage.SETTINGS) + public static final String AUTO_REBOOT_TIMEOUT = "settings_reboot_after_timeout"; + + /** @hide */ + @Protected(restrictReads = false, readWrite = KnownSystemPackage.SETTINGS) + public static final String GNSS_SUPL = "force_disable_supl"; // historical name + + /** @hide */ + @Protected(restrictReads = false, readWrite = KnownSystemPackage.SETTINGS) + public static final String GNSS_PSDS_STANDARD = "psds_server"; // historical name + + /** @hide */ + @Protected(readWrite = KnownSystemPackage.SETTINGS) + public static final String WIFI_AUTO_OFF = "wifi_off_timeout"; + + /** @hide */ + @Protected(readWrite = KnownSystemPackage.SETTINGS) + public static final String BLUETOOTH_AUTO_OFF = "bluetooth_off_timeout"; + + /** @hide */ + @Protected(readWrite = KnownSystemPackage.SETTINGS) + public static final String NFC_AUTO_OFF = "nfc_off_timeout"; + + /** @hide */ + @Protected(restrictReads = false, readWrite = KnownSystemPackage.SETTINGS) + public static final String REMOTE_KEY_PROVISIONING_SERVER = "attest_remote_provisioner_server"; + + /** @hide */ + @Protected(readWrite = KnownSystemPackage.SETTINGS) + public static final String RESTRICT_MEMORY_DYN_CODE_LOADING_BY_DEFAULT = "restrict_memory_dyn_code_exec"; + + /** @hide */ + @Protected(readWrite = KnownSystemPackage.SETTINGS) + public static final String RESTRICT_STORAGE_DYN_CODE_LOADING_BY_DEFAULT = "restrict_storage_dyn_code_exec"; + + /** @hide */ + @Protected(readWrite = KnownSystemPackage.SETTINGS) + public static final String RESTRICT_WEBVIEW_DYN_CODE_LOADING_BY_DEFAULT = "restrict_webview_dyn_code_exec"; + + /** @hide */ + @Protected(readWrite = KnownSystemPackage.SETTINGS) + public static final String FORCE_APP_MEMTAG_BY_DEFAULT = "force_app_memtag"; + + /** @hide */ + @Protected(readWrite = KnownSystemPackage.SETTINGS) + public static final String SHOW_SYSTEM_PROCESS_CRASH_NOTIFICATIONS = "show_system_process_crash_notifs"; + + /** @hide */ + @Protected(restrictReads = false, readWrite = KnownSystemPackage.SETTINGS) + public static final String WIDEVINE_PROVISIONING_SERVER = "widevine_provisioner_server"; + + /** @hide */ + @Protected(read = KnownSystemPackage.SYSTEM_UI, readWrite = KnownSystemPackage.SETTINGS) + public static final String BATTERY_CHARGE_LIMIT = "battery_charge_limit"; + + /** @hide */ + @Protected(restrictReads = false, readWrite = {KnownSystemPackage.SETTINGS, + KnownSystemPackage.SETUP_WIZARD}) + public static final String NETWORK_LOCATION = "network_location"; + + /** @hide */ + @Protected(restrictReads = false, readWrite = {KnownSystemPackage.SETTINGS, + KnownSystemPackage.SETUP_WIZARD}) + public static final String GEOCODER = "geocoder"; + + /** @hide */ + @Protected(restrictReads = false, readWrite = KnownSystemPackage.SETTINGS) + public static final String CERT_TRANSPARENCY_DOWNLOADER = "cert_transparency_downloader"; + + // ExtSettings END + // NOTE: If you add new settings here, be sure to add them to // com.android.providers.settings.SettingsProtoDumpUtil#dumpProtoGlobalSettingsLocked. @@ -14307,6 +14551,7 @@ public static final class Global extends NameValueTable { * @hide */ @Readable + @Protected(immutableValue = "0") public static final String ADD_USERS_WHEN_LOCKED = "add_users_when_locked"; /** @@ -19022,6 +19267,7 @@ public static final class Global extends NameValueTable { * @hide */ @Readable + @Protected(immutableValue = "0") public static final String ENABLE_EPHEMERAL_FEATURE = "enable_ephemeral_feature"; /** @@ -19602,6 +19848,11 @@ public static final class Global extends NameValueTable { sProviderHolder, Global.class); + /** @hide */ + public static boolean isKnownKey(String key) { + return sNameValueCache.mAllFields.contains(key); + } + // Certain settings have been moved from global to the per-user secure namespace @UnsupportedAppUsage private static final HashSet MOVED_TO_SECURE; @@ -19643,14 +19894,15 @@ public static void clearProviderForTest() { /** @hide */ public static void getPublicSettings(Set allKeys, Set readableKeys, ArrayMap readableKeysWithMaxTargetSdk, - ArrayMap readableKeysWithRedactedValue) { + ArrayMap readableKeysWithRedactedValue, + ArrayMap protectedSettings) { getPublicSettingsForClass(Global.class, allKeys, readableKeys, - readableKeysWithMaxTargetSdk, readableKeysWithRedactedValue); + readableKeysWithMaxTargetSdk, readableKeysWithRedactedValue, protectedSettings); // Add Global.Wearable keys on watches. if (ActivityThread.currentApplication().getApplicationContext().getPackageManager() .hasSystemFeature(PackageManager.FEATURE_WATCH)) { getPublicSettingsForClass(Global.Wearable.class, allKeys, readableKeys, - readableKeysWithMaxTargetSdk, readableKeysWithRedactedValue); + readableKeysWithMaxTargetSdk, readableKeysWithRedactedValue, protectedSettings); } } @@ -19883,6 +20135,19 @@ public static Uri getUriFor(String name) { * or not a valid integer. */ public static int getInt(ContentResolver cr, String name, int def) { + if (GmsCompat.isEnabled()) { + if ("google_play_store_system_component_update".equals(name)) { + // Stop Play Store from attempting to auto-install some system component + // packages, such as "Android System SafetyCore" (com.google.android.safetycore) + // and "Android System Key Verifier" (com.google.android.contactkeys) + // + // This setting also disables auto-updates of GmsCore and Play Store. + if (!"1".equals(getString(cr, "gmscompat_bypass_sys_component_update_stub"))) { + return 0; + } + } + } + String v = getString(cr, name); return parseIntSettingWithDefault(v, def); } diff --git a/core/java/android/provider/Telephony.java b/core/java/android/provider/Telephony.java index 0e0ebea50d073..141ec438dc1ba 100644 --- a/core/java/android/provider/Telephony.java +++ b/core/java/android/provider/Telephony.java @@ -5628,6 +5628,12 @@ private SimInfo() {} public static final String COLUMN_SATELLITE_ENTITLEMENT_VOICE_SERVICE_POLICY = "satellite_entitlement_voice_service_policy"; + /** + * TelephonyProvider column name for extended SIM state + * @hide + */ + public static final String COLUMN_EXT_SIM_STATE = "ext_sim_state"; + /** * TelephonyProvider column name for whether a subscription is for private network. *

Type: INTEGER (int), 1 for private network or 0 for not. @@ -5674,6 +5680,7 @@ private SimInfo() {} /** All columns in {@link SimInfo} table. */ private static final List ALL_COLUMNS = List.of( + COLUMN_EXT_SIM_STATE, COLUMN_UNIQUE_KEY_SUBSCRIPTION_ID, COLUMN_ICC_ID, COLUMN_SIM_SLOT_INDEX, diff --git a/core/java/android/speech/tts/TtsEngines.java b/core/java/android/speech/tts/TtsEngines.java index a8aea7c1eb599..3697b9494949d 100644 --- a/core/java/android/speech/tts/TtsEngines.java +++ b/core/java/android/speech/tts/TtsEngines.java @@ -498,7 +498,7 @@ static public String[] toOldLocaleStringFormat(Locale locale) { * specific preference in the list. */ private static String parseEnginePrefFromList(String prefValue, String engineName) { - if (TextUtils.isEmpty(prefValue)) { + if (TextUtils.isEmpty(prefValue) || TextUtils.isEmpty(engineName)) { return null; } diff --git a/core/java/android/telephony/TelephonyRegistryManager.java b/core/java/android/telephony/TelephonyRegistryManager.java index bec2f720881b6..30be7487e1eb7 100644 --- a/core/java/android/telephony/TelephonyRegistryManager.java +++ b/core/java/android/telephony/TelephonyRegistryManager.java @@ -23,6 +23,7 @@ import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.annotation.SystemService; +import android.app.compat.gms.GmsCompat; import android.compat.Compatibility; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledAfter; @@ -54,6 +55,7 @@ import android.util.Log; import com.android.internal.annotations.GuardedBy; +import com.android.internal.gmscompat.sysservice.GmcTelephonyManager; import com.android.internal.listeners.ListenerExecutor; import com.android.internal.telephony.ICarrierConfigChangeListener; import com.android.internal.telephony.ICarrierPrivilegesCallback; @@ -298,6 +300,13 @@ public void listenFromListener(int subId, @NonNull boolean renounceFineLocationA } else if (listener.mSubId != null) { subId = listener.mSubId; } + + if (GmsCompat.isEnabled()) { + eventsList = GmcTelephonyManager.filterTelephonyCallbackEvents(eventsList); + // empty eventsList means "unregister the listener". It's fine if eventsList becomes + // empty after filtering, unregistration of a never-registered listener is allowed. + } + sRegistry.listenWithEventList(renounceFineLocationAccess, renounceCoarseLocationAccess, subId, pkg, featureId, listener.callback, eventsList, notifyNow); } catch (RemoteException e) { @@ -319,6 +328,13 @@ private void listenFromCallback(boolean renounceFineLocationAccess, @NonNull String pkg, @NonNull String featureId, @NonNull TelephonyCallback telephonyCallback, @NonNull int[] events, boolean notifyNow) { + + if (GmsCompat.isEnabled()) { + events = GmcTelephonyManager.filterTelephonyCallbackEvents(events); + // empty events array means "unregister the listener". It's fine if events array becomes + // empty after filtering, unregistration of a never-registered listener is allowed. + } + try { sRegistry.listenWithEventList(renounceFineLocationAccess, renounceCoarseLocationAccess, subId, pkg, featureId, telephonyCallback.callback, events, notifyNow); diff --git a/core/java/android/text/ShowSecretsSetting.java b/core/java/android/text/ShowSecretsSetting.java index 2c690b6d5034b..29aed68a2b327 100644 --- a/core/java/android/text/ShowSecretsSetting.java +++ b/core/java/android/text/ShowSecretsSetting.java @@ -68,7 +68,7 @@ public static boolean shouldShowTouchInput(@NonNull Context context) { return Secure.getIntForUser( Objects.requireNonNull(context).getContentResolver(), Secure.TEXT_SHOW_PASSWORD_TOUCH, - SHOW, + HIDE, context.getUser().getIdentifier()) == SHOW; } diff --git a/core/java/android/util/LongArray.java b/core/java/android/util/LongArray.java index 4c7ef08ddfcb3..703a424c2a233 100644 --- a/core/java/android/util/LongArray.java +++ b/core/java/android/util/LongArray.java @@ -119,6 +119,14 @@ public void add(int index, long value) { mValues[index] = value; } + public void addAll(long[] arr) { + final int len = arr.length; + ensureCapacity(len); + + System.arraycopy(arr, 0, mValues, mSize, len); + mSize += len; + } + /** * Adds the values in the specified array to this array. */ @@ -133,7 +141,7 @@ public void addAll(LongArray values) { /** * Ensures capacity to append at least count values. */ - private void ensureCapacity(int count) { + public void ensureCapacity(int count) { final int currentSize = mSize; final int minCapacity = currentSize + count; if (minCapacity >= mValues.length) { diff --git a/core/java/android/util/NtpTrustedTime.java b/core/java/android/util/NtpTrustedTime.java index 3adbd686cd2c1..cf64566ca8e8c 100644 --- a/core/java/android/util/NtpTrustedTime.java +++ b/core/java/android/util/NtpTrustedTime.java @@ -23,6 +23,7 @@ import android.content.Context; import android.content.res.Resources; import android.net.ConnectivityManager; +import android.net.HttpsTimeClient; import android.net.Network; import android.net.NetworkInfo; import android.net.SntpClient; @@ -38,6 +39,7 @@ import java.net.InetSocketAddress; import java.net.URI; import java.net.URISyntaxException; +import java.net.URL; import java.time.Duration; import java.time.Instant; import java.util.ArrayList; @@ -246,6 +248,10 @@ public String toString() { @Nullable private volatile URI mLastSuccessfulNtpServerUri; + @GuardedBy("mRefreshLock") + @Nullable + private URL mLastSuccessfulHttpsTimeUrl; + protected NtpTrustedTime() { } @@ -270,7 +276,7 @@ public void setServerConfigForTests(@NonNull NtpConfig ntpConfig) { /** Forces a refresh using the default network. */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - public boolean forceRefresh() { + public final boolean forceRefresh() { synchronized (mRefreshLock) { Network network = getDefaultNetwork(); if (network == null) { @@ -283,7 +289,7 @@ public boolean forceRefresh() { } /** Forces a refresh using the specified network. */ - public boolean forceRefresh(@NonNull Network network) { + public final boolean forceRefresh(@NonNull Network network) { Objects.requireNonNull(network); synchronized (mRefreshLock) { @@ -296,11 +302,36 @@ public boolean forceRefresh(@NonNull Network network) { private boolean forceRefreshLocked(@NonNull Network network) { Objects.requireNonNull(network); + final ContentResolver resolver = getContext().getContentResolver(); + + if (Settings.Global.getInt(resolver, Settings.Global.AUTO_TIME, 0) == 0) { + Log.d(TAG, "skipped forceRefresh: auto time is disabled"); + return false; + } + if (!isNetworkConnected(network)) { if (LOGD) Log.d(TAG, "forceRefreshLocked: network=" + network + " is not connected"); return false; } + final String networkTimeMode = "https"; + + if ("https".equals(networkTimeMode)) { + var client = new HttpsTimeClient(getHttpsTimeConfig(), network); + HttpsTimeClient.Result res = client.requestTime(mLastSuccessfulHttpsTimeUrl); + if (res == null) { + return false; + } + + mTimeResult = res.timeResult; + mLastSuccessfulHttpsTimeUrl = res.url; + return true; + } + + if (!"ntp".equals(networkTimeMode)) { + throw new IllegalStateException(networkTimeMode); + } + NtpConfig ntpConfig = getNtpConfig(); if (ntpConfig == null) { // missing server config, so no NTP time available @@ -376,6 +407,10 @@ private NtpConfig getNtpConfig() { } } + private HttpsTimeClient.Config getHttpsTimeConfig() { + return HttpsTimeClient.Config.getDefault(getContext()); + } + /** * Returns the {@link NtpConfig} to use during an NTP query. This method can return {@code null} * if there is no config, or the config found is invalid. @@ -606,6 +641,8 @@ private static URI validateNtpServerUri(@NonNull URI uri) throws URISyntaxExcept /** Prints debug information. */ public void dump(PrintWriter pw) { synchronized (mConfigLock) { + pw.println("getHttpsTimeConfig()=" + getHttpsTimeConfig()); + pw.println("mLastSuccessfulHttpsTimeUrl=" + mLastSuccessfulHttpsTimeUrl); pw.println("getNtpConfig()=" + getNtpConfig()); pw.println("mNtpConfigForTests=" + mNtpConfigForTests); } @@ -748,5 +785,14 @@ private static int saturatedCast(long longValue) { } return (int) longValue; } + + @NonNull + @Override + protected Context getContext() { + return mContext; + } } + + @NonNull + protected abstract Context getContext(); } diff --git a/core/java/android/util/PackageUtils.java b/core/java/android/util/PackageUtils.java index c1ed19fef032b..6dc86a0c767bf 100644 --- a/core/java/android/util/PackageUtils.java +++ b/core/java/android/util/PackageUtils.java @@ -19,9 +19,13 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; import android.content.pm.Signature; import android.text.TextUtils; +import libcore.util.EmptyArray; import libcore.util.HexEncoding; import java.io.ByteArrayOutputStream; @@ -31,7 +35,9 @@ import java.security.DigestInputStream; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; import java.util.Arrays; +import java.util.List; /** * Helper functions applicable to packages. @@ -246,4 +252,36 @@ private PackageUtils() { } return TextUtils.join(separator, pieces); } + + public static boolean isSystemPackage(Context ctx, @Nullable String pkg) { + if (TextUtils.isEmpty(pkg)) { + return false; + } + PackageManager pm = ctx.getPackageManager(); + try { + ApplicationInfo appInfo = pm.getApplicationInfo(pkg, 0); + // even though getApplicationInfo() is @NonNull, it returns null in some tests + return appInfo != null && appInfo.isSystemApp(); + } catch (PackageManager.NameNotFoundException e) { + return false; + } + } + + public static String[] filterNonSystemPackages(Context ctx, String[] pkgs) { + return filterNonSystemPackagesL(ctx, pkgs).toArray(EmptyArray.STRING); + } + + public static List filterNonSystemPackagesL(Context ctx, String[] pkgs) { + var list = new ArrayList(pkgs.length); + for (String pkg : pkgs) { + if (isSystemPackage(ctx, pkg)) { + list.add(pkg); + } + } + return list; + } + + public static String getFirstPartyAppSourcePackageName(Context ctx) { + return ctx.getString(com.android.internal.R.string.config_first_party_app_source_package_name); + } } diff --git a/core/java/android/util/Singleton.java b/core/java/android/util/Singleton.java index 9f8469d5a5bd0..c1827592f0144 100644 --- a/core/java/android/util/Singleton.java +++ b/core/java/android/util/Singleton.java @@ -56,4 +56,10 @@ private synchronized T maybeCreate() { } return instance; } + + public void clear() { + synchronized (this) { + mInstance = null; + } + } } diff --git a/core/java/android/util/apk/SourceStampVerifier.java b/core/java/android/util/apk/SourceStampVerifier.java index 11d7a005a6479..94ff47614a05d 100644 --- a/core/java/android/util/apk/SourceStampVerifier.java +++ b/core/java/android/util/apk/SourceStampVerifier.java @@ -25,6 +25,7 @@ import static android.util.apk.ApkSigningBlockUtils.readLengthPrefixedByteArray; import static android.util.apk.ApkSigningBlockUtils.verifyProofOfRotationStruct; +import android.annotation.Nullable; import android.util.Pair; import android.util.Slog; import android.util.jar.StrictJarFile; @@ -94,10 +95,16 @@ private SourceStampVerifier() { /** Verifies SourceStamp present in a list of (split) APKs for the same app. */ public static SourceStampVerificationResult verify(List apkFiles) { + return verify(apkFiles, null); + } + + public static SourceStampVerificationResult verify(List apkFiles, + @Nullable byte[] requiredSourceStampCertDigest) { Certificate stampCertificate = null; List stampCertificateLineage = Collections.emptyList(); for (String apkFile : apkFiles) { - SourceStampVerificationResult sourceStampVerificationResult = verify(apkFile); + SourceStampVerificationResult sourceStampVerificationResult = verify(apkFile, + requiredSourceStampCertDigest); if (!sourceStampVerificationResult.isPresent() || !sourceStampVerificationResult.isVerified()) { return sourceStampVerificationResult; @@ -116,6 +123,11 @@ public static SourceStampVerificationResult verify(List apkFiles) { /** Verifies SourceStamp present in the provided APK. */ public static SourceStampVerificationResult verify(String apkFile) { + return verify(apkFile, null); + } + + public static SourceStampVerificationResult verify(String apkFile, + @Nullable byte[] requiredSourceStampCertDigest) { StrictJarFile apkJar = null; try (RandomAccessFile apk = new RandomAccessFile(apkFile, "r")) { apkJar = @@ -129,6 +141,11 @@ public static SourceStampVerificationResult verify(String apkFile) { // SourceStamp present. return SourceStampVerificationResult.notPresent(); } + if (requiredSourceStampCertDigest != null) { + if (!Arrays.equals(sourceStampCertificateDigest, requiredSourceStampCertDigest)) { + return SourceStampVerificationResult.notVerified(); + } + } byte[] manifestBytes = getManifestBytes(apkJar); return verify(apk, sourceStampCertificateDigest, manifestBytes); } catch (IOException e) { diff --git a/core/java/android/view/Window.java b/core/java/android/view/Window.java index 363af7fe89092..81eb46424db38 100644 --- a/core/java/android/view/Window.java +++ b/core/java/android/view/Window.java @@ -37,6 +37,7 @@ import android.annotation.TestApi; import android.annotation.UiContext; import android.app.WindowConfiguration; +import android.app.compat.gms.GmsCompat; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.content.pm.ActivityInfo; @@ -1161,8 +1162,9 @@ public final void setHideOverlayWindows(boolean hide) { // asynchronously. if (mContext.checkSelfPermission(HIDE_NON_SYSTEM_OVERLAY_WINDOWS) != PERMISSION_GRANTED && mContext.checkSelfPermission(HIDE_OVERLAY_WINDOWS) != PERMISSION_GRANTED) { - throw new SecurityException( + var se = new SecurityException( "Permission denial: setHideOverlayWindows: HIDE_OVERLAY_WINDOWS"); + GmsCompat.catchOrRethrow(se); } setPrivateFlags(hide ? SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS : 0, SYSTEM_FLAG_HIDE_NON_SYSTEM_OVERLAY_WINDOWS); diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index 6019ed0b18955..a6561139e21c4 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -1752,6 +1752,13 @@ private static InputMethodManager forContextInternal(int displayId, Looper loope } } + /** @hide */ + public static boolean isInstanceCacheEmpty() { + synchronized (sLock) { + return sInstanceMap.size() == 0; + } + } + /** * Deprecated. Do not use. * diff --git a/core/java/android/view/inputmethod/InputMethodManagerGlobal.java b/core/java/android/view/inputmethod/InputMethodManagerGlobal.java index 78be2961eea41..d2703e471710d 100644 --- a/core/java/android/view/inputmethod/InputMethodManagerGlobal.java +++ b/core/java/android/view/inputmethod/InputMethodManagerGlobal.java @@ -63,6 +63,13 @@ public static void stopImeTrace(@Nullable Consumer exceptionHan IInputMethodManagerGlobalInvoker.stopImeTrace(exceptionHandler); } + @AnyThread + @Nullable + @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true) + public static InputMethodSubtype getCurrentInputMethodSubtype(int userId) { + return IInputMethodManagerGlobalInvoker.getCurrentInputMethodSubtype(userId); + } + /** * Invokes {@link IInputMethodManager#isImeTraceEnabled()}. * diff --git a/core/java/android/webkit/WebViewFactory.java b/core/java/android/webkit/WebViewFactory.java index 2f740ba6c3141..2b743c1c35665 100644 --- a/core/java/android/webkit/WebViewFactory.java +++ b/core/java/android/webkit/WebViewFactory.java @@ -289,6 +289,11 @@ public static int loadWebViewNativeLibraryFromPackage(String packageName, return LIBLOAD_WRONG_PACKAGE_NAME; } + if (com.android.internal.os.ExecSpawning.isExecSpawnedProcess()) { + Log.d(LOGTAG, "loadWebViewNativeLibraryFromPackage called in exec spawned process, returning LIBLOAD_ADDRESS_SPACE_NOT_RESERVED early"); + return WebViewFactory.LIBLOAD_ADDRESS_SPACE_NOT_RESERVED; + } + Application initialApplication = AppGlobals.getInitialApplication(); WebViewProviderResponse response = null; try { diff --git a/core/java/android/webkit/WebViewZygote.java b/core/java/android/webkit/WebViewZygote.java index efe7f354af477..1ee8107d160a3 100644 --- a/core/java/android/webkit/WebViewZygote.java +++ b/core/java/android/webkit/WebViewZygote.java @@ -22,11 +22,13 @@ import android.os.Process; import android.os.UserHandle; import android.os.ZygoteProcess; +import android.os.ZygoteSelectionMode; import android.text.TextUtils; import android.util.Log; import com.android.internal.annotations.GuardedBy; import com.android.internal.os.Zygote; +import com.android.internal.os.ZygoteExtraArgs; /** @hide */ public class WebViewZygote { @@ -108,7 +110,7 @@ private static void connectToZygoteIfNeededLocked() { sPackage.applicationInfo, null); final int[] sharedAppGid = { UserHandle.getSharedAppGid(UserHandle.getAppId(sPackage.applicationInfo.uid)) }; - sZygote = Process.ZYGOTE_PROCESS.startChildZygote( + sZygote = Process.ZYGOTE_PROCESS.startChildZygote(ZygoteExtraArgs.createForWebviewZygote(), "com.android.internal.os.WebViewZygoteInit", "webview_zygote", Process.WEBVIEW_ZYGOTE_UID, @@ -124,7 +126,7 @@ private static void connectToZygoteIfNeededLocked() { sPackage.applicationInfo); ZygoteProcess.waitForConnectionToZygote( sZygote.getZygoteProcessAsManaged().getPrimarySocketAddress()); - sZygote.getZygoteProcessAsManaged().preloadApp(sPackage.applicationInfo, abi); + sZygote.getZygoteProcessAsManaged().preloadApp(sPackage.applicationInfo, abi, ZygoteSelectionMode.Regular); } catch (Exception e) { Log.e(LOGTAG, "Error connecting to webview zygote", e); stopZygoteLocked(); diff --git a/core/java/android/window/WindowContext.java b/core/java/android/window/WindowContext.java index 4cf699b03a829..efdde63cf97b9 100644 --- a/core/java/android/window/WindowContext.java +++ b/core/java/android/window/WindowContext.java @@ -23,6 +23,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.UiContext; +import android.app.ActivityThread; import android.content.ComponentCallbacks; import android.content.ComponentCallbacksController; import android.content.Context; @@ -139,7 +140,7 @@ public Object getSystemService(String name) { @Override protected void finalize() throws Throwable { try { - release(); + getMainThreadHandler().post(this::release); } finally { super.finalize(); } diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java index 164da11f4f4ed..0720fcdc8da7d 100644 --- a/core/java/com/android/internal/app/ChooserActivity.java +++ b/core/java/com/android/internal/app/ChooserActivity.java @@ -2815,7 +2815,7 @@ public Intent getReferrerFillInIntent() { @Override // ChooserListCommunicator public int getMaxRankedTargets() { - return mMaxTargetsPerRow; + return mMaxTargetsPerRow * 2; } @Override diff --git a/core/java/com/android/internal/app/ContactScopes.java b/core/java/com/android/internal/app/ContactScopes.java new file mode 100644 index 0000000000000..2ba307f82094e --- /dev/null +++ b/core/java/com/android/internal/app/ContactScopes.java @@ -0,0 +1,167 @@ +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.database.ContentObserver; +import android.ext.DerivedPackageFlag; +import android.ext.cscopes.ContactScopesApi; +import android.net.Uri; +import android.provider.ContactsContract; +import android.provider.SimPhonebookContract; + +import com.android.internal.app.ContentProviderRedirector.ContentObserverList; + +public class ContactScopes { + private static volatile boolean isEnabled; + private static int gosPsDerivedFlags; + + private static ContentObserverList contentObserverList; + + public static boolean isEnabled() { + return isEnabled; + } + + @AnyThread + public static void maybeEnable(Context ctx, GosPackageState ps) { + synchronized (ContactScopes.class) { + if (isEnabled) { + return; + } + + if (ps.hasFlag(GosPackageStateFlag.CONTACT_SCOPES_ENABLED)) { + gosPsDerivedFlags = ps.derivedFlags; + { + var col = new ContentObserverList(); + String intentAction = ContactScopesApi.ACTION_NOTIFY_CONTENT_OBSERVERS; + // this broadcast is sent by PermissionController + String permission = Manifest.permission.GRANT_RUNTIME_PERMISSIONS; + col.registerNotificationReceiver(ctx, intentAction, permission); + contentObserverList = col; + } + ContentProviderRedirector.enable(); + 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 boolean maybeInterceptRegisterContentObserver(Uri uri, ContentObserver observer) { + if (!isEnabled) { + return false; + } + + if (isContactsUri(uri)) { + contentObserverList.add(observer, uri); + return true; + } + + return false; + } + + public static boolean maybeInterceptUnregisterContentObserver(ContentObserver observer) { + if (!isEnabled) { + return false; + } + + return contentObserverList.remove(observer); + } + + public static boolean shouldSkipNotifyChange(Uri uri) { + if (!isEnabled) { + return false; + } + + return isContactsUri(uri); + } + + public static boolean isContactsUri(Uri uri) { + return isContactsUriAuthority(uri.getAuthority()); + } + + public static boolean isContactsUriAuthority(String authority) { + if (authority == null) { + return false; + } + switch (authority) { + case ContactsContract.AUTHORITY: + case SimPhonebookContract.AUTHORITY: + case ICC_PROVIDER_AUTHORITY: + return true; + } + return false; + } + + // legacy SIM phonebook provider + public static final String ICC_PROVIDER_AUTHORITY = "icc"; + + public static String maybeTranslateAuthority(String auth) { + if (!isEnabled) { + return null; + } + + if (ContactsContract.AUTHORITY.equals(auth)) { + return ContactScopesApi.SCOPED_CONTACTS_PROVIDER_AUTHORITY; + } + + if (SimPhonebookContract.AUTHORITY.equals(auth)) { + return SimPhonebookContract.AUTHORITY + ".stub"; + } + + if (ICC_PROVIDER_AUTHORITY.equals(auth)) { + return ICC_PROVIDER_AUTHORITY + ".stub"; + } + + return null; + } + + public static int getSpoofablePermissionDflag(String permName) { + switch (permName) { + case Manifest.permission.READ_CONTACTS: + return DerivedPackageFlag.HAS_READ_CONTACTS_DECLARATION; + case Manifest.permission.WRITE_CONTACTS: + return DerivedPackageFlag.HAS_WRITE_CONTACTS_DECLARATION; + case Manifest.permission.GET_ACCOUNTS: + return DerivedPackageFlag.HAS_GET_ACCOUNTS_DECLARATION; + default: + return 0; + } + } + + private static int getSpoofableAppOpPermissionDflag(int op) { + switch (op) { + case AppOpsManager.OP_READ_CONTACTS: + return DerivedPackageFlag.HAS_READ_CONTACTS_DECLARATION; + case AppOpsManager.OP_WRITE_CONTACTS: + return DerivedPackageFlag.HAS_WRITE_CONTACTS_DECLARATION; + case AppOpsManager.OP_GET_ACCOUNTS: + return DerivedPackageFlag.HAS_GET_ACCOUNTS_DECLARATION; + default: + return 0; + } + } +} diff --git a/core/java/com/android/internal/app/ContentProviderRedirector.java b/core/java/com/android/internal/app/ContentProviderRedirector.java new file mode 100644 index 0000000000000..a326ad7bd101b --- /dev/null +++ b/core/java/com/android/internal/app/ContentProviderRedirector.java @@ -0,0 +1,145 @@ +package com.android.internal.app; + +import android.annotation.Nullable; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Handler; +import android.os.Looper; +import android.os.UserHandle; +import android.util.Log; +import android.util.Pair; + +import java.util.ArrayList; +import java.util.Collections; + +public class ContentProviderRedirector { + + private static volatile boolean isEnabled; + + public static void enable() { + isEnabled = true; + } + + public static String translateContentProviderAuthority(String auth) { + if (!isEnabled) { + return auth; + } + + String t = null; + + if (t == null) { + t = ContactScopes.maybeTranslateAuthority(auth); + } + + return t != null ? t : auth; + } + + // registering a ContentObserver requires having the read permission for the underlying + // ContentProvider + public static boolean shouldSkipRegisterContentObserver(Uri uri, boolean notifyForDescendants, + ContentObserver observer, int userId) { + if (!isEnabled) { + return false; + } + + if (ContactScopes.maybeInterceptRegisterContentObserver(uri, observer)) { + return true; + } + + return false; + } + + public static boolean shouldSkipUnregisterContentObserver(ContentObserver observer) { + if (!isEnabled) { + return false; + } + + if (ContactScopes.maybeInterceptUnregisterContentObserver(observer)) { + return true; + } + + return false; + } + + public static boolean shouldSkipNotifyChange(Uri uri, @Nullable ContentObserver observer, + @ContentResolver.NotifyFlags int flags) { + if (!isEnabled) { + return false; + } + + if (ContactScopes.shouldSkipNotifyChange(uri)) { + return true; + } + + return false; + } + + // a helper class for keeping track of skipped ContentObservers and for delivering content + // change notifications to them manually (see shouldSkipRegisterContentObserver()) + public static class ContentObserverList { + private final ArrayList>> list = new ArrayList<>(); + + public void add(ContentObserver observer, Uri uri) { + synchronized (list) { + for (var pair : list) { + if (pair.first == observer) { + pair.second.add(uri); + return; + } + } + var uris = new ArrayList(); + uris.add(uri); + list.add(Pair.create(observer, uris)); + } + } + + @Nullable + public boolean remove(ContentObserver observer) { + synchronized (list) { + for (int i = 0, m = list.size(); i < m; ++i) { + var pair = list.get(i); + if (pair.first == observer) { + list.remove(i); + return true; + } + } + + return false; + } + } + + public void notifyAllObservers() { + final Pair>[] arr; + synchronized (list) { + arr = list.toArray(new Pair[0]); + } + + new Handler(Looper.getMainLooper()).post(() -> { + int myUserId = UserHandle.myUserId(); + for (var pair : arr) { + var uris = Collections.unmodifiableList(pair.second); + pair.first.dispatchChange(false, uris, 0, myUserId); + } + }); + } + + class Receiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + Log.d("ContentChangeReceiver", intent.toString()); + notifyAllObservers(); + } + } + + public void registerNotificationReceiver(Context ctx, String intentAction, String permission) { + var receiver = new Receiver(); + var filter = new IntentFilter(intentAction); + ctx.registerReceiver(receiver, filter, permission, null, Context.RECEIVER_EXPORTED); + } + } +} diff --git a/core/java/com/android/internal/app/IAppOpsService.aidl b/core/java/com/android/internal/app/IAppOpsService.aidl index 6858905e2713f..523c1cb052567 100644 --- a/core/java/com/android/internal/app/IAppOpsService.aidl +++ b/core/java/com/android/internal/app/IAppOpsService.aidl @@ -78,6 +78,7 @@ interface IAppOpsService { // Deprecated, use finishProxyOperationWithState instead. void finishProxyOperation(IBinder clientId, int code, in AttributionSource attributionSource, boolean skipProxyOperation); + void checkHistoricalRegistryConfig(in String[] extraPermissions); int checkPackage(int uid, String packageName); RuntimeAppOpAccessMessage collectRuntimeAppOpAccessMessage(); MessageSamplingConfig reportRuntimeAppOpAccessMessageAndGetConfig(String packageName, diff --git a/core/java/com/android/internal/app/LocaleStore.java b/core/java/com/android/internal/app/LocaleStore.java index 072c0e9660af2..e4abaa18cd74c 100644 --- a/core/java/com/android/internal/app/LocaleStore.java +++ b/core/java/com/android/internal/app/LocaleStore.java @@ -453,8 +453,6 @@ public static void fillCache(Context context) { sSimCountries = getSimCountries(context); - final boolean isInDeveloperMode = Settings.Global.getInt(context.getContentResolver(), - Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0) != 0; Set numberSystemLocaleList = new HashSet<>(); for (String localeId : LocalePicker.getSupportedLocales(context)) { if (Locale.forLanguageTag(localeId).getUnicodeLocaleType("nu") != null) { @@ -468,14 +466,8 @@ public static void fillCache(Context context) { LocaleInfo li = new LocaleInfo(localeId); if (LocaleList.isPseudoLocale(li.getLocale())) { - if (isInDeveloperMode) { - li.setTranslated(true); - li.mIsPseudo = true; - li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM; - } else { - // Do not display pseudolocales unless in development mode. - continue; - } + // Do not display pseudolocales + continue; } if (sSimCountries.contains(li.getLocale().getCountry())) { diff --git a/core/java/com/android/internal/app/PairipHooks.java b/core/java/com/android/internal/app/PairipHooks.java new file mode 100644 index 0000000000000..6ddd1259fcf35 --- /dev/null +++ b/core/java/com/android/internal/app/PairipHooks.java @@ -0,0 +1,93 @@ +package com.android.internal.app; + +import android.app.AppGlobals; +import android.content.ContentProvider; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.ext.PackageId; +import android.os.RemoteException; +import android.provider.Settings; +import android.util.Log; + +import static java.util.Objects.requireNonNull; + +/** + * Hooks for bypassing the Play Store automatic protection system (pairip) for apps that have their + * integrity verified by the Play Store APK source stamp. + * For more info, see https://support.google.com/googleplay/android-developer/answer/10183279 + * + * @hide + */ +public class PairipHooks { + private static final String TAG = "PairipHooks"; + + public static boolean shouldSkipOnCreate(ContentProvider p) { + // startsWith() is used since the class name might be suffixed with a number in some cases + if (!p.getClass().getName().startsWith("com.pairip.licensecheck.LicenseContentProvider")) { + return false; + } + if (shouldBypass(p.requireContext())) { + Log.d(TAG, "skipping pairip LicenseContentProvider checks"); + return true; + } + return false; + } + + public static String maybeReplaceApplicationClassName(Context context, ClassLoader classLoader, + String className) throws ClassNotFoundException { + if (!"com.pairip.application.Application".equals(className)) { + return className; + } + if (!shouldBypass(context)) { + return className; + } + Class cls = classLoader.loadClass(className); + String res = requireNonNull(cls.getSuperclass()).getName(); + Log.d(TAG, "replaced pairip Application class with its parent " + res); + return res; + } + + private static Boolean shouldBypassCached; + + private static boolean shouldBypass(Context context) { + Boolean cache = shouldBypassCached; + if (cache != null) { + return cache.booleanValue(); + } + + boolean res = context.getApplicationInfo().hasPlayStoreSourceStamp(); + if (res) { + boolean installedFromPlayStore = false; + String installerPkg; + try { + installerPkg = AppGlobals.getPackageManager().getInstallerPackageName(context.getPackageName()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + if (PackageId.PLAY_STORE_NAME.equals(installerPkg)) { + PackageManager pm = context.getPackageManager(); + try { + ApplicationInfo ai = pm.getApplicationInfo(PackageId.PLAY_STORE_NAME, 0); + installedFromPlayStore = ai.ext().getPackageId() == PackageId.PLAY_STORE; + } catch (PackageManager.NameNotFoundException e) {} + } + if (installedFromPlayStore) { + Log.d(TAG, "app is installed from Play Store, skipping bypass"); + res = false; + } + } + + if (res && android.os.Flags.isDevBuild()) { + if (Settings.Global.getInt(context.getContentResolver(), "skip_pairip_bypass", 0) == 1) { + Log.d(TAG, "skip_pairip_bypass is set, keeping pairip check", new Throwable()); + shouldBypassCached = Boolean.FALSE; + return false; + } + Log.d(TAG, "bypassing pairip check", new Throwable()); + } + + shouldBypassCached = Boolean.valueOf(res); + return res; + } +} diff --git a/core/java/com/android/internal/app/RedirectedContentProvider.java b/core/java/com/android/internal/app/RedirectedContentProvider.java new file mode 100644 index 0000000000000..91d2a6121f6cf --- /dev/null +++ b/core/java/com/android/internal/app/RedirectedContentProvider.java @@ -0,0 +1,107 @@ +package com.android.internal.app; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.ContentProvider; +import android.content.ContentValues; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import java.util.Arrays; + +/** + * A base class for stubbed out or scoped content providers. + * Stubbed out providers are fully empty and immutable. + * Scoped providers export a subset of data from a different content provider and are expected to + * be immutable. + */ +public class RedirectedContentProvider extends ContentProvider { + protected String TAG; + protected boolean DEBUG; + + protected String authorityOverride; + + @Override + public boolean onCreate() { + DEBUG = Log.isLoggable(TAG, Log.DEBUG); + if (DEBUG) Log.d(TAG, "onCreate"); + return true; + } + + @Override + public final Cursor query(Uri uri, @Nullable String[] projection, @Nullable String selection, + @Nullable String[] selectionArgs, @Nullable String sortOrder) { + if (DEBUG) Log.d(TAG, "query from " + getCallingPackage() + "; uri " + uri + + ", projection " + Arrays.toString(projection) + ", selection " + selection + + ", selectionArgs " + Arrays.toString(selectionArgs) + + ", sortOrder " + sortOrder); + return queryInner(uri, projection, selection, selectionArgs, sortOrder); + } + + public Cursor queryInner(Uri uri, @Nullable String[] projection, @Nullable String selection, + @Nullable String[] selectionArgs, @Nullable String sortOrder) { + return null; + } + + @Override + public final String getType(Uri uri) { + if (DEBUG) Log.d(TAG, "getType from " + getCallingPackage() + "; uri " + uri); + + // getType() call doesn't require any permission, can always forward it to the original provider + return requireContext().getContentResolver().getType(uri); + } + + @Override + public final Uri insert(Uri uri, ContentValues values) { + if (DEBUG) Log.d(TAG, "insert from " + getCallingPackage() + "; uri " + uri + ", values " + values); + return null; + } + + @Override + public final int delete(Uri uri, String selection, String[] selectionArgs) { + if (DEBUG) Log.d(TAG, "delete from " + getCallingPackage() + "; uri " + uri + + ", selection " + selection + ", selectionArgs " + Arrays.toString(selectionArgs)); + return 0; + } + + @Override + public final int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + if (DEBUG) Log.d(TAG, "update from " + getCallingPackage() + "; uri " + uri + ", values " + values + + ", selection " + selection + ", selectionArgs " + Arrays.toString(selectionArgs)); + return 0; + } + + @Nullable + @Override + public final ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) { + if (DEBUG) Log.d(TAG, "openFile from " + getCallingPackage() + "; uri " + uri + ", mode " + mode); + return null; + } + + @Override + public Bundle call(@NonNull String method, @Nullable String arg, @Nullable Bundle extras) { + if (DEBUG) Log.d(TAG, "call from " + getCallingPackage() + "; method " + method + ", arg " + arg + + ", extras " + (extras != null ? extras.deepCopy() : extras)); + return new Bundle(); + } + + @Override + public Uri validateIncomingUri(Uri uri) throws SecurityException { + if (DEBUG) Log.d(TAG, "validateIncomingUri: " + uri); + uri = uri.buildUpon().authority(authorityOverride).build(); + + uri = super.validateIncomingUri(uri); + + if (DEBUG) Log.d(TAG, "override uri to: " + uri); + + return uri; + } + + @Override + protected final void validateIncomingAuthority(String authority) throws SecurityException { + if (DEBUG) Log.d(TAG, "validateIncomingAuthority: " + authority); + } +} diff --git a/core/java/com/android/internal/app/StorageScopesAppHooks.java b/core/java/com/android/internal/app/StorageScopesAppHooks.java new file mode 100644 index 0000000000000..6864603c3d792 --- /dev/null +++ b/core/java/com/android/internal/app/StorageScopesAppHooks.java @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2022 GrapheneOS + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.app; + +import android.Manifest; +import android.annotation.AnyThread; +import android.app.AppOpsManager; +import android.content.Context; +import android.content.Intent; +import android.content.pm.GosPackageState; +import android.content.pm.GosPackageStateFlag; +import android.ext.DerivedPackageFlag; +import android.net.Uri; +import android.os.Environment; +import android.provider.MediaStore; +import android.provider.Settings; + +public class StorageScopesAppHooks { + private static final String TAG = "StorageScopesAppHooks"; + + private static volatile boolean isEnabled; + private static int gosPsDerivedFlags; + + @AnyThread + public static void maybeEnable(GosPackageState ps) { + if (isEnabled) { + return; + } + + if (ps.hasFlag(GosPackageStateFlag.STORAGE_SCOPES_ENABLED)) { + gosPsDerivedFlags = ps.derivedFlags; + isEnabled = true; + } + } + + public static boolean isEnabled() { + return isEnabled; + } + + public static boolean shouldSkipPermissionCheckSpoof(int gosPsDflags, int permDerivedFlag) { + if ((gosPsDflags & DerivedPackageFlag.HAS_READ_MEDIA_VISUAL_USER_SELECTED_DECLARATION) != 0) { + switch (permDerivedFlag) { + case DerivedPackageFlag.HAS_READ_MEDIA_AUDIO_DECLARATION: + case DerivedPackageFlag.HAS_READ_MEDIA_VIDEO_DECLARATION: + // see https://developer.android.com/about/versions/14/changes/partial-photo-video-access + return false; + } + } + + return false; + } + + // call only if isEnabled == true + private static boolean shouldSpoofSelfPermissionCheckInner(int permDerivedFlag) { + if (permDerivedFlag == 0) { + return false; + } + + if (shouldSkipPermissionCheckSpoof(gosPsDerivedFlags, permDerivedFlag)) { + return false; + } + + return (gosPsDerivedFlags & permDerivedFlag) != 0; + } + + public static boolean shouldSpoofSelfPermissionCheck(String permName) { + if (!isEnabled) { + return false; + } + + return shouldSpoofSelfPermissionCheckInner(getSpoofablePermissionDflag(permName)); + } + + public static boolean shouldSpoofSelfAppOpCheck(int op) { + if (!isEnabled) { + return false; + } + + return shouldSpoofSelfPermissionCheckInner(getSpoofableAppOpPermissionDflag(op)); + } + + // Instrumentation#execStartActivity(Context, IBinder, IBinder, Activity, Intent, int, Bundle) + public static void maybeModifyActivityIntent(Context ctx, Intent i) { + String action = i.getAction(); + if (action == null) { + return; + } + + int op; + switch (action) { + case Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION: + op = AppOpsManager.OP_MANAGE_EXTERNAL_STORAGE; + break; + case Settings.ACTION_REQUEST_MANAGE_MEDIA: + op = AppOpsManager.OP_MANAGE_MEDIA; + break; + default: + return; + } + + Uri uri = i.getData(); + if (uri == null || !"package".equals(uri.getScheme())) { + return; + } + + String pkgName = uri.getSchemeSpecificPart(); + + if (pkgName == null) { + return; + } + + if (!pkgName.equals(ctx.getPackageName())) { + return; + } + + boolean shouldModify = false; + + if (shouldSpoofSelfAppOpCheck(op)) { + // in case a buggy app launches intent again despite pseudo-having the permission + shouldModify = true; + } else { + if (op == AppOpsManager.OP_MANAGE_EXTERNAL_STORAGE) { + shouldModify = !Environment.isExternalStorageManager(); + } else if (op == AppOpsManager.OP_MANAGE_MEDIA) { + shouldModify = !MediaStore.canManageMedia(ctx); + } + } + + if (shouldModify) { + i.setAction(action + "_PROMPT"); + } + } + + public static int getSpoofablePermissionDflag(String permName) { + switch (permName) { + case Manifest.permission.READ_EXTERNAL_STORAGE: + return DerivedPackageFlag.HAS_READ_EXTERNAL_STORAGE_DECLARATION; + + case Manifest.permission.WRITE_EXTERNAL_STORAGE: + return DerivedPackageFlag.HAS_WRITE_EXTERNAL_STORAGE_DECLARATION; + + case Manifest.permission.ACCESS_MEDIA_LOCATION: + return DerivedPackageFlag.HAS_ACCESS_MEDIA_LOCATION_DECLARATION; + + case Manifest.permission.READ_MEDIA_AUDIO: + return DerivedPackageFlag.HAS_READ_MEDIA_AUDIO_DECLARATION; + + case Manifest.permission.READ_MEDIA_IMAGES: + return DerivedPackageFlag.HAS_READ_MEDIA_IMAGES_DECLARATION; + + case Manifest.permission.READ_MEDIA_VIDEO: + return DerivedPackageFlag.HAS_READ_MEDIA_VIDEO_DECLARATION; + + case Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED: + return DerivedPackageFlag.HAS_READ_MEDIA_VISUAL_USER_SELECTED_DECLARATION; + + default: + return 0; + } + } + + private static int getSpoofableAppOpPermissionDflag(int op) { + switch (op) { + case AppOpsManager.OP_READ_EXTERNAL_STORAGE: + return DerivedPackageFlag.HAS_READ_EXTERNAL_STORAGE_DECLARATION; + + case AppOpsManager.OP_WRITE_EXTERNAL_STORAGE: + return DerivedPackageFlag.HAS_WRITE_EXTERNAL_STORAGE_DECLARATION; + + case AppOpsManager.OP_READ_MEDIA_AUDIO: + return DerivedPackageFlag.HAS_READ_MEDIA_AUDIO_DECLARATION; + + case AppOpsManager.OP_READ_MEDIA_IMAGES: + return DerivedPackageFlag.HAS_READ_MEDIA_IMAGES_DECLARATION; + + case AppOpsManager.OP_READ_MEDIA_VIDEO: + return DerivedPackageFlag.HAS_READ_MEDIA_VIDEO_DECLARATION; + + case AppOpsManager.OP_MANAGE_EXTERNAL_STORAGE: + return DerivedPackageFlag.HAS_MANAGE_EXTERNAL_STORAGE_DECLARATION; + + case AppOpsManager.OP_MANAGE_MEDIA: + return DerivedPackageFlag.HAS_MANAGE_MEDIA_DECLARATION; + + case AppOpsManager.OP_ACCESS_MEDIA_LOCATION: + return DerivedPackageFlag.HAS_ACCESS_MEDIA_LOCATION_DECLARATION; + + case AppOpsManager.OP_READ_MEDIA_VISUAL_USER_SELECTED: + return DerivedPackageFlag.HAS_READ_MEDIA_VISUAL_USER_SELECTED_DECLARATION; + + default: + return 0; + } + } + + private StorageScopesAppHooks() {} +} diff --git a/core/java/com/android/internal/content/F2fsUtils.java b/core/java/com/android/internal/content/F2fsUtils.java index 27f1b308ed9c3..afa8d7773b0b4 100644 --- a/core/java/com/android/internal/content/F2fsUtils.java +++ b/core/java/com/android/internal/content/F2fsUtils.java @@ -266,7 +266,10 @@ private static List getFilesRecursive(@NonNull File path) { final ArrayList files = new ArrayList<>(); for (File f : allFiles) { if (f.isDirectory()) { - files.addAll(getFilesRecursive(f)); + List inner = getFilesRecursive(f); + if (inner != null) { + files.addAll(inner); + } } else if (f.isFile()) { files.add(f); } diff --git a/core/java/com/android/internal/ext/EuiccGoogleHooks.java b/core/java/com/android/internal/ext/EuiccGoogleHooks.java new file mode 100644 index 0000000000000..6d337d67d5472 --- /dev/null +++ b/core/java/com/android/internal/ext/EuiccGoogleHooks.java @@ -0,0 +1,33 @@ +package com.android.internal.ext; + +public class EuiccGoogleHooks { + + private static final ThreadLocal resourceFilteringSuppressionCounter = + ThreadLocal.withInitial(() -> Integer.valueOf(0)); + + public static boolean isResourceFilteringSuppressed() { + return resourceFilteringSuppressionCounter.get() > 0; + } + + public static class SuppressResourceFiltering implements AutoCloseable { + + public SuppressResourceFiltering() { + var tl = resourceFilteringSuppressionCounter; + int newValue = tl.get() + 1; + if (newValue <= 0) { + throw new IllegalStateException(); + } + tl.set(newValue); + } + + @Override + public void close() { + var tl = resourceFilteringSuppressionCounter; + int newValue = tl.get() - 1; + if (newValue < 0) { + throw new IllegalStateException(); + } + tl.set(newValue); + } + } +} diff --git a/core/java/com/android/internal/gmscompat/BinderGca2Gms.java b/core/java/com/android/internal/gmscompat/BinderGca2Gms.java new file mode 100644 index 0000000000000..09b773a5ff24d --- /dev/null +++ b/core/java/com/android/internal/gmscompat/BinderGca2Gms.java @@ -0,0 +1,98 @@ +package com.android.internal.gmscompat; + +import android.app.Activity; +import android.app.compat.gms.GmsCompat; +import android.content.Intent; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.util.ArraySet; +import android.util.Log; + +import com.android.internal.gmscompat.flags.GmsFlagOverrides; +import com.android.internal.gmscompat.util.GmcActivityUtils; + +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; + +import static com.android.internal.gmscompat.GmsHooks.inPersistentGmsCoreProcess; +import static com.android.internal.gmscompat.GmsInfo.PACKAGE_GMS_CORE; + +// see IGca2Gms +class BinderGca2Gms extends IGca2Gms.Stub { + private static final String TAG = "BinderGca2Gms"; + private static final boolean DEV = false; + + @Override + public void updateConfig(GmsCompatConfig newConfig) { + GmsHooks.setConfig(newConfig); + } + + @Override + public void invalidateConfigCaches() { + if (!inPersistentGmsCoreProcess) { + throw new IllegalStateException(); + } + + SQLiteOpenHelper phenotypeDb = GmsHooks.getPhenotypeDb(); + + if (phenotypeDb == null) { + // shouldn't happen in practice, phenotype db is opened on startup + Log.e(TAG, "phenotypeDb is null"); + } else { + notifyForceDefaultFlagsChanged(phenotypeDb, GmsHooks.config()); + } + + GmsFlagOverrides.applyOverrides(); + } + + private static void notifyForceDefaultFlagsChanged(SQLiteOpenHelper phenotypeDb, GmsCompatConfig newConfig) { + SQLiteDatabase db = phenotypeDb.getReadableDatabase(); + String[] columns = { "androidPackageName" }; + String selection = "packageName = ?"; + + ArraySet configPackageNames = new ArraySet<>(newConfig.forceDefaultFlagsMap.keySet()); + + for (String configPackageName : configPackageNames) { + String[] selectionArgs = { configPackageName }; + String packageName = null; + try (Cursor c = db.query("Packages", columns, selection, selectionArgs, null, null, null, "1")) { + if (c.moveToFirst()) { + packageName = c.getString(0); + } + } + + if (packageName == null) { + Log.d(TAG, "unknown configPackageName " + configPackageName); + continue; + } + + Intent i = new Intent(PACKAGE_GMS_CORE + ".phenotype.UPDATE"); + i.putExtra(PACKAGE_GMS_CORE + ".phenotype.PACKAGE_NAME", configPackageName); + i.setPackage(packageName); + GmsCompat.appContext().sendBroadcast(i); + } + } + + @Override + public boolean startActivityIfVisible(Intent intent) { + Callable callable = () -> { + Activity activity = GmcActivityUtils.getMostRecentVisibleActivity(); + if (activity == null) { + return false; + } + activity.startActivity(intent); + return true; + }; + var task = new FutureTask<>(callable); + // getMostRecentVisibleActivity() needs to be called from main thread to avoid races + GmsCompat.appContext().getMainThreadHandler().post(task); + try { + return task.get().booleanValue(); + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); + return false; + } + } +} diff --git a/core/java/com/android/internal/gmscompat/GmcBinderDefs.java b/core/java/com/android/internal/gmscompat/GmcBinderDefs.java new file mode 100644 index 0000000000000..6b88d67855edf --- /dev/null +++ b/core/java/com/android/internal/gmscompat/GmcBinderDefs.java @@ -0,0 +1,111 @@ +package com.android.internal.gmscompat; + +import android.annotation.Nullable; +import android.app.ActivityThread; +import android.app.compat.gms.GmsCompat; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.BinderDef; +import android.os.HybridBinder; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.Log; + +public class GmcBinderDefs { + static final String TAG = GmcBinderDefs.class.getSimpleName(); + + private static final ArrayMap seenInterfaces = new ArrayMap<>(); + + @Nullable + public static IBinder maybeOverrideBinder(IBinder originalBinder, String ifaceName) { + BinderDef bdef = maybeGetBinderDef(ifaceName); + if (bdef == null) { + return null; + } + + Context ctx = GmsCompat.appContext(); + + if (bdef.transactionCodes == null) { + // this is a complete interface implementation + return bdef.getInstance(ctx); + } + + return new HybridBinder(ctx, originalBinder, bdef); + } + + private static BinderDef maybeGetBinderDef(String iface) { + synchronized (seenInterfaces) { + int i = seenInterfaces.indexOfKey(iface); + if (i >= 0) { + return seenInterfaces.valueAt(i); + } + } + + Context ctx = GmsCompat.appContext(); + String pkgName = ctx.getPackageName(); + int processState = ActivityThread.currentActivityThread().getProcessState(); + + BinderDef maybeBinderDef; + try { + if (GmsCompat.isEnabled()) { + maybeBinderDef = GmsCompatApp.iGms2Gca() + .maybeGetBinderDef(pkgName, processState, iface); + } else { + maybeBinderDef = GmsCompatApp.iClientOfGmsCore2Gca() + .maybeGetBinderDef(pkgName, processState, iface); + } + } catch (RemoteException e) { + throw GmsCompatApp.callFailed(e); + } + + synchronized (seenInterfaces) { + // in case seenInterfaces changed since last check + int idx = seenInterfaces.indexOfKey(iface); + if (idx >= 0) { + return seenInterfaces.valueAt(idx); + } + + if (seenInterfaces.size() == 0) { + BinderDefStateListener.register(ctx); + } + + seenInterfaces.put(iface, maybeBinderDef); + } + + return maybeBinderDef; + } + + public static class BinderDefStateListener extends BroadcastReceiver { + public static final String INTENT_ACTION = GmsCompatApp.PKG_NAME + ".ACTION_BINDER_DEFS_CHANGED"; + public static final String KEY_CHANGED_IFACE_NAMES = "changed_iface_names"; + + static void register(Context ctx) { + var l = new BinderDefStateListener(); + var filter = new IntentFilter(INTENT_ACTION); + String permission = GmsCompatApp.SIGNATURE_PROTECTED_PERMISSION; + ctx.registerReceiver(l, filter, permission, null, Context.RECEIVER_EXPORTED); + } + + public void onReceive(Context context, Intent intent) { + String[] changedIfaces = intent.getStringArrayExtra(KEY_CHANGED_IFACE_NAMES); + + boolean exit = false; + synchronized (seenInterfaces) { + for (String changedIface : changedIfaces) { + if (seenInterfaces.containsKey(changedIface)) { + // it's infeasible to apply updated BinderDef without restarting app process + Log.d(TAG, "state of BinderDef " + changedIface + " changed, will call System.exit(0)"); + exit = true; + } + } + } + if (exit) { + System.exit(0); + } + } + } +} diff --git a/core/java/com/android/internal/gmscompat/GmcDebug.java b/core/java/com/android/internal/gmscompat/GmcDebug.java new file mode 100644 index 0000000000000..4854ef21e8b5e --- /dev/null +++ b/core/java/com/android/internal/gmscompat/GmcDebug.java @@ -0,0 +1,167 @@ +package com.android.internal.gmscompat; + +import android.app.BroadcastOptions; +import android.app.Service; +import android.content.BroadcastReceiver; +import android.content.ComponentName; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Bundle; +import android.util.Log; + +import java.util.Arrays; + +public class GmcDebug { + + public static void maybeLogStartService(Intent service, boolean requireForeground) { + String LOG_TAG = "GmcStartService"; + if (!Log.isLoggable(LOG_TAG, Log.VERBOSE)) { + return; + } + var b = new StringBuilder(); + appendIntent(service, b); + if (requireForeground) { + b.append(", requireForeground"); + } + Log.v(LOG_TAG, b.toString(), new Throwable()); + } + + public static void maybeLogStopService(Intent service) { + String LOG_TAG = "GmcStopService"; + if (!Log.isLoggable(LOG_TAG, Log.VERBOSE)) { + return; + } + var b = new StringBuilder(); + appendIntent(service, b); + Log.v(LOG_TAG, b.toString(), new Throwable()); + } + + public static void maybeLogServiceOnStartCommand(Service service, Intent intent, int flags, int startId) { + String LOG_TAG = "GmcServiceCmd"; + if (!Log.isLoggable(LOG_TAG, Log.VERBOSE)) { + return; + } + var b = new StringBuilder(); + b.append(service.getClass().getName()); + b.append(", "); + if (intent != null) { + appendIntent(intent, b); + } else { + b.append("intent is null"); + } + b.append(", flags: "); + b.append(Integer.toHexString(flags)); + b.append(", startId: "); + b.append(startId); + Log.v(LOG_TAG, b.toString()); + } + + public static void maybeLogBindService(Intent service, ServiceConnection conn, long flags, String instanceName) { + String LOG_TAG = "GmcBindService"; + if (!Log.isLoggable(LOG_TAG, Log.VERBOSE)) { + return; + } + var b = new StringBuilder(); + appendIntent(service, b); + b.append(", conn: "); + b.append(System.identityHashCode(conn)); + b.append(", flags: "); + b.append(Long.toHexString(flags)); + if (instanceName != null) { + b.append(", instanceName: "); + b.append(instanceName); + } + Log.v(LOG_TAG, b.toString(), new Throwable()); + } + + public static void maybeLogUnbindService(ServiceConnection conn) { + String LOG_TAG = "GmcUnbindService"; + if (!Log.isLoggable(LOG_TAG, Log.VERBOSE)) { + return; + } + Log.v(LOG_TAG, "conn: " + System.identityHashCode(conn), new Throwable()); + } + + public static void maybeLogServiceConnCallback(String name, ComponentName cn) { + String LOG_TAG = "GmcServiceConnCb"; + if (!Log.isLoggable(LOG_TAG, Log.VERBOSE)) { + return; + } + Log.v(LOG_TAG, name + ", " + cn.toShortString()); + } + + public static void maybeLogSendBroadcast(Intent intent, String receiverPerm, Bundle options, + int appOp) { + maybeLogSendBroadcast(intent, receiverPerm, null, options, null, appOp, 0, null, null); + } + + public static void maybeLogSendBroadcast(Intent intent, String receiverPerm, String[] receiverPerms, Bundle options, BroadcastOptions brOptions, + int appOp) { + maybeLogSendBroadcast(intent, receiverPerm, receiverPerms, options, brOptions, appOp, 0, null, null); + } + + public static void maybeLogSendBroadcast(Intent intent, String receiverPerm, String[] receiverPerms, Bundle options, BroadcastOptions brOptions, + int appOp, int initialCode, String initialData, Bundle initialExtras) { + String LOG_TAG = "GmcSendBroadcast"; + if (!Log.isLoggable(LOG_TAG, Log.VERBOSE)) { + return; + } + var b = new StringBuilder("sendBroadcast: "); + appendIntent(intent, b); + if (options != null) { + b.append(", options: "); + b.append(options.toStringDeep()); + } + if (brOptions != null) { + b.append(", brOptions: "); + b.append(brOptions); + } + if (receiverPerm != null) { + b.append(", receiverPerm: "); + b.append(receiverPerm); + } + if (receiverPerms != null) { + b.append(", receiverPerms: "); + b.append(Arrays.toString(receiverPerms)); + } + if (appOp != 0) { + b.append(", appOp: "); + b.append(appOp); + } + if (initialCode != 0) { + b.append(", initialCode: "); + b.append(initialCode); + } + if (initialData != null) { + b.append(", initialData: "); + b.append(initialData); + } + if (initialExtras != null) { + b.append(", initialExtras: "); + b.append(initialExtras.toStringDeep()); + } + Log.v(LOG_TAG, b.toString(), new Throwable()); + } + + public static void maybeLogReceiveBroadcast(BroadcastReceiver receiver, Intent intent, boolean isManifest) { + String LOG_TAG = "GmcRecvBroadcast"; + if (!Log.isLoggable(LOG_TAG, Log.VERBOSE)) { + return; + } + var b = new StringBuilder(); + b.append(isManifest? "manifest; " : "dynamic; "); + appendIntent(intent, b); + b.append(", receiver: "); + b.append(receiver.getClass().getName()); + Log.v(LOG_TAG, b.toString()); + } + + private static void appendIntent(Intent intent, StringBuilder b) { + intent.toString(b); + Bundle extras = intent.getExtras(); + if (extras != null) { + b.append(", extras: "); + b.append(extras.toStringDeep()); + } + } +} diff --git a/core/java/com/android/internal/gmscompat/GmcMediaProjectionService.java b/core/java/com/android/internal/gmscompat/GmcMediaProjectionService.java new file mode 100644 index 0000000000000..f71ecb2e88a15 --- /dev/null +++ b/core/java/com/android/internal/gmscompat/GmcMediaProjectionService.java @@ -0,0 +1,85 @@ +package com.android.internal.gmscompat; + +import android.app.Notification; +import android.app.Service; +import android.content.Intent; +import android.os.IBinder; +import android.os.RemoteException; +import android.util.ArrayMap; +import android.util.Log; + +import java.util.UUID; +import java.util.concurrent.CountDownLatch; + +import static android.app.compat.gms.GmsCompat.appContext; + +// Unprivileged app that is performing a screen capture is required by the OS to run +// a foreground service with FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION +public class GmcMediaProjectionService extends Service { + private static final String TAG = "GmcMediaProjService"; + + private static final ArrayMap latches = new ArrayMap<>(); + + public static void start() { + if (Thread.currentThread() == appContext().getMainLooper().getThread()) { + // otherwise, latch.await() below would deadlock + throw new IllegalStateException("should never be called from the main thread"); + } + + String id = UUID.randomUUID().toString(); + var latch = new CountDownLatch(1); + synchronized (latches) { + latches.put(id, latch); + } + Intent intent = intent().setIdentifier(id); + Log.d(TAG, "start " + id); + appContext().startForegroundService(intent); + try { + latch.await(); + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } + } + + public static void stop() { + Log.d(TAG, "stop"); + appContext().stopService(intent()); + } + + @Override + public int onStartCommand(Intent intent, int flags, int startId) { + Log.d(TAG, "onStartCommand " + intent); + + Notification n; + try { + // notification icon and text are stored in GmsCompat app + n = GmsCompatApp.iGms2Gca().getMediaProjectionNotification(); + } catch (RemoteException e) { + throw GmsCompatApp.callFailed(e); + } + + startForeground(GmsCoreConst.NOTIF_ID_MEDIA_PROJECTION_SERVICE, n); + + String id = intent.getIdentifier(); + CountDownLatch latch; + + synchronized (latches) { + latch = latches.remove(id); + } + if (latch != null) { + latch.countDown(); + } else { + // can happen if our process died after startForegroundService() but before + // onStartCommand(), OS recreates process in that case + Log.e(TAG, "missing latch"); + } + + return START_NOT_STICKY; + } + + private static Intent intent() { + return new Intent(appContext(), GmcMediaProjectionService.class); + } + + @Override public IBinder onBind(Intent intent) { return null; } +} diff --git a/core/java/com/android/internal/gmscompat/GmsCompatApp.java b/core/java/com/android/internal/gmscompat/GmsCompatApp.java new file mode 100644 index 0000000000000..086e21aa72ddf --- /dev/null +++ b/core/java/com/android/internal/gmscompat/GmsCompatApp.java @@ -0,0 +1,330 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.gmscompat; + +import android.Manifest; +import android.accounts.AccountManager; +import android.annotation.Nullable; +import android.app.compat.gms.GmsCompat; +import android.content.Context; +import android.database.ContentObserver; +import android.net.ConnectivityManager; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.RemoteException; +import android.provider.DeviceConfig; +import android.provider.Settings; +import android.util.ArraySet; +import android.util.Log; + +import com.android.internal.gmscompat.fileservice.FileProxyService; + +import static com.android.internal.gmscompat.GmsHooks.inPersistentGmsCoreProcess; + +public final class GmsCompatApp { + private static final String TAG = "GmsCompat/GCA"; + public static final String PKG_NAME = "app.grapheneos.gmscompat"; + // permission that is held only by GmsCompatApp + public static final String SIGNATURE_PROTECTED_PERMISSION = PKG_NAME + ".SIGNATURE_PROTECTED_PERMISSION"; + + @SuppressWarnings("FieldCanBeLocal") + // written to fields to prevent GC from collecting them + private static BinderGca2Gms binderGca2Gms; + @SuppressWarnings("FieldCanBeLocal") + private static FileProxyService gmsCoreFileProxyService; + + private static IGms2Gca binderGms2Gca; + + static GmsCompatConfig connect(Context ctx, String processName) { + registeredContentObservers = new ArraySet<>(); + + BinderGca2Gms gca2Gms = new BinderGca2Gms(); + binderGca2Gms = gca2Gms; + + try { + IGms2Gca iGms2Gca = IGms2Gca.Stub.asInterface(getBinder(RPC_GET_BINDER_IGms2Gca)); + binderGms2Gca = iGms2Gca; + + if (GmsCompat.isGmsCore()) { + FileProxyService fileProxyService = null; + if (inPersistentGmsCoreProcess) { + // FileProxyService binder needs to be always available to the GmsCore clients. + // "persistent" process launches at bootup and is kept alive by the ServiceConnection + // from the GmsCompatApp, which makes it fit for the purpose of hosting the FileProxyService + fileProxyService = new FileProxyService(ctx); + gmsCoreFileProxyService = fileProxyService; + + Handler handler = ctx.getMainThreadHandler(); + handler.postDelayed(GmsCompatApp::maybeShowContactsSyncNotification, 3000L); + handler.postDelayed(GmsCompatApp::maybeShowGmsCoreRestrictedBackgroundDataNotif, 3000L); + } + return iGms2Gca.connectGmsCore(processName, gca2Gms, fileProxyService); + } else { + return iGms2Gca.connect(ctx.getPackageName(), processName, gca2Gms); + } + } catch (RemoteException e) { + throw callFailed(e); + } + } + + public static IGms2Gca iGms2Gca() { + return binderGms2Gca; + } + + private static volatile IClientOfGmsCore2Gca binderClientOfGmsCore2Gca; + + public static IClientOfGmsCore2Gca iClientOfGmsCore2Gca() { + IClientOfGmsCore2Gca cache = binderClientOfGmsCore2Gca; + if (cache != null) { + return cache; + } + + IBinder binder = getBinder(RPC_GET_BINDER_IClientOfGmsCore2Gca); + IClientOfGmsCore2Gca iface = IClientOfGmsCore2Gca.Stub.asInterface(binder); + // benign race, it's fine to obtain this interface more than once + binderClientOfGmsCore2Gca = iface; + return iface; + } + + private static final String RPC_PROVIDER_AUTHORITY = PKG_NAME + ".RpcProvider"; + public static final String KEY_BINDER = "binder"; + public static final String KEY_PKG_NAME = "pkg"; + + public static final int RPC_GET_BINDER_IGms2Gca = 0; + public static final int RPC_GET_BINDER_IClientOfGmsCore2Gca = 1; + + public static Bundle callRpcProvider(Context ctx, int method, String arg, Bundle extras) { + String authority = RPC_PROVIDER_AUTHORITY; + var cr = ctx.getContentResolver(); + try { + return cr.call(authority, Integer.toString(method), arg, extras); + } catch (Throwable t) { + Log.e(TAG, "call to " + authority + " failed", t); + if (GmsCompat.isEnabled()) { + // content provider calls are infallible unless something goes very wrong, better fail fast in that case + System.exit(1); + } + // don't crash processes that call GmsCompatApp, but don't use GmsCompat layer + return null; + } + } + + private static IBinder getBinder(int which) { + Bundle bundle = callRpcProvider(GmsCompat.appContext(), which, null, null); + IBinder binder = bundle.getBinder(KEY_BINDER); + DeathRecipient.register(binder); + return binder; + } + + static class DeathRecipient implements IBinder.DeathRecipient { + private static final DeathRecipient INSTANCE = new DeathRecipient(); + private DeathRecipient() {} + + static void register(IBinder b) { + try { + b.linkToDeath(INSTANCE, 0); + } catch (RemoteException e) { + // binder already died + INSTANCE.binderDied(); + } + } + + public void binderDied() { + // see comment in callFailed() + Log.e(TAG, PKG_NAME + " died"); + System.exit(1); + } + } + + public static RuntimeException callFailed(RemoteException e) { + // running GmsCompat app process is a hard dependency of sandboxed GMS + Log.e(TAG, "call failed, calling System.exit(1)", e); + System.exit(1); + // unreachable, needed for control flow checks by the compiler + // (Java doesn't have a concept of "noreturn") + return e.rethrowAsRuntimeException(); + } + + public static final String NS_DeviceConfig = "config"; + + public static String deviceConfigNamespace(String namespace) { + // last path component of DeviceConfig.CONTENT_URI + String topNs = "config"; + return NS_DeviceConfig + ':' + namespace; + } + + public static String getString(String ns, String key) { + try { + return iGms2Gca().privSettingsGetString(ns, key); + } catch (RemoteException e) { + throw callFailed(e); + } + } + + public static boolean putString(String ns, String key, @Nullable String value) { + try { + return iGms2Gca().privSettingsPutString(ns, key, value); + } catch (RemoteException e) { + throw callFailed(e); + } + } + + public static boolean setProperties(DeviceConfig.Properties props) { + String[] keys = props.getKeyset().toArray(new String[0]); + String[] values = new String[keys.length]; + + for (int i = 0; i < keys.length; ++i) { + values[i] = props.getString(keys[i], null); + } + + String ns = deviceConfigNamespace(props.getNamespace()); + + try { + return iGms2Gca().privSettingsPutStrings(ns, keys, values); + } catch (RemoteException e) { + throw callFailed(e); + } + } + + private static ArraySet registeredContentObservers; + + public static boolean registerObserver(Uri uri, ContentObserver observer) { + String s = uri.toString(); + + String prefix = "content://settings/"; + + if (!s.startsWith(prefix)) { + return false; + } + + int nsStart = prefix.length(); + int nsEnd = s.indexOf('/', nsStart); + + if (nsEnd < 0 || nsStart == nsEnd) { + return false; + } + + String ns = s.substring(nsStart, nsEnd); + String key = s.substring(nsEnd + 1); + + switch (ns) { + // keep in sync with Settings.NameValueCache#maybeGetGmsCompatNamespace + case "global": + if (Settings.Global.isKnownKey(key)) { + return false; + } + break; + case "secure": + if (Settings.Secure.isKnownKey(key)) { + return false; + } + break; + default: + return false; + } + + android.database.IContentObserver iObserver = observer.getContentObserver(); + + try { + iGms2Gca().privSettingsRegisterObserver(ns, key, iObserver); + } catch (RemoteException e) { + throw callFailed(e); + } + + synchronized (registeredContentObservers) { + registeredContentObservers.add(observer); + } + + return true; + } + + public static boolean unregisterObserver(ContentObserver observer) { + synchronized (registeredContentObservers) { + if (registeredContentObservers.contains(observer)) { + registeredContentObservers.remove(observer); + } else { + return false; + } + } + + android.database.IContentObserver iObserver = observer.getContentObserver(); + + try { + iGms2Gca().privSettingsUnregisterObserver(iObserver); + } catch (RemoteException e) { + throw callFailed(e); + } + return true; + } + + // Bypass some background activity restrictions (e.g. background service starts) for target + // package by binding to it from foreground GmsCompat app + public static void raisePackageToForeground(String targetPkg, long durationMs, + @Nullable String reason, int reasonCode) { + if (durationMs <= 0) { + Log.e(TAG, "invalid duration: " + durationMs, new Throwable()); + return; + } + try { + GmsCompatApp.iGms2Gca().raisePackageToForeground(targetPkg, durationMs, reason, reasonCode); + } catch (RemoteException e) { + throw callFailed(e); + } + } + + static void maybeShowContactsSyncNotification() { + if (GmsCompat.hasPermission(Manifest.permission.WRITE_CONTACTS)) { + return; + } + + Context ctx = GmsCompat.appContext(); + var am = ctx.getSystemService(AccountManager.class); + + am.addOnAccountsUpdatedListener(accounts -> { + // invoked only for Google accounts, "updateImmediately" arg ensures that it'll be called + // even if account is already added + if (accounts.length != 0) { + try { + iGms2Gca().maybeShowContactsSyncNotification(); + } catch (RemoteException e) { + callFailed(e); + } + } + }, ctx.getMainThreadHandler(), true); + } + + static final int RESTRICTED_BACKGROUND_DATA_CHECK_INTERVAL = 3 * 60_000; // 3 minutes + + static void maybeShowGmsCoreRestrictedBackgroundDataNotif() { + Context ctx = GmsCompat.appContext(); + var cm = ctx.getSystemService(ConnectivityManager.class); + if (cm.getRestrictBackgroundStatus() == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED) { + try { + iGms2Gca().maybeShowGmsCoreRestrictedBackgroundDataNotif(); + } catch (RemoteException e) { + throw callFailed(e); + } + } + + // there's no listener API for getRestrictBackgroundStatus(), use polling with a large interval + ctx.getMainThreadHandler().postDelayed(GmsCompatApp::maybeShowGmsCoreRestrictedBackgroundDataNotif, RESTRICTED_BACKGROUND_DATA_CHECK_INTERVAL); + } + + private GmsCompatApp() {} +} diff --git a/core/java/com/android/internal/gmscompat/GmsCompatConfig.aidl b/core/java/com/android/internal/gmscompat/GmsCompatConfig.aidl new file mode 100644 index 0000000000000..23491f79e42cb --- /dev/null +++ b/core/java/com/android/internal/gmscompat/GmsCompatConfig.aidl @@ -0,0 +1,3 @@ +package com.android.internal.gmscompat; + +parcelable GmsCompatConfig; diff --git a/core/java/com/android/internal/gmscompat/GmsCompatConfig.java b/core/java/com/android/internal/gmscompat/GmsCompatConfig.java new file mode 100644 index 0000000000000..c46a7b55713ec --- /dev/null +++ b/core/java/com/android/internal/gmscompat/GmsCompatConfig.java @@ -0,0 +1,238 @@ +/* + * Copyright (C) 2022 GrapheneOS + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.android.internal.gmscompat; + +import android.app.compat.gms.GmsCompat; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.SparseArray; + +import com.android.internal.gmscompat.flags.GmsFlag; + +import java.util.ArrayList; +import java.util.function.BiConsumer; +import java.util.function.Function; + +// Instances of this object should be immutable after publication, make sure to never change it afterwards +public class GmsCompatConfig implements Parcelable { + public long version; + public final ArrayMap> flags = new ArrayMap<>(); + public ArrayMap gservicesFlags; + public final ArrayMap> stubs = new ArrayMap<>(); + // keys are namespaces, values are regexes of flag names that should be forced to default value + public final ArrayMap> forceDefaultFlagsMap = new ArrayMap<>(); + // keys are package names, values are list of permissions self-checks of which should be spoofed + public final ArrayMap> spoofSelfPermissionChecksMap = new ArrayMap<>(); + // keys are serviceIds, values are service permission requirements that need to be bypassed + public final SparseArray> gmsServiceBrokerPermissionBypasses = new SparseArray<>(); + + // keys are package names, values are maps of components names to their component enabled setting + public final ArrayMap> forceComponentEnabledSettingsMap = new ArrayMap<>(); + + // set only in processes for which GmsCompat is enabled, to speed up lookups + public ArraySet spoofSelfPermissionChecks; + + public long maxGmsCoreVersion; + public long maxPlayStoreVersion; + + public void addFlag(String namespace, GmsFlag flag) { + ArrayMap nsFlags = flags.get(namespace); + if (nsFlags == null) { + nsFlags = new ArrayMap<>(); + flags.put(namespace, nsFlags); + } + nsFlags.put(flag.name, flag); + } + + public void addGservicesFlag(String key, String value) { + GmsFlag f = new GmsFlag(key); + f.initAsSetString(value); + addGservicesFlag(f); + } + + public void addGservicesFlag(GmsFlag flag) { + ArrayMap map = gservicesFlags; + if (map == null) { + map = new ArrayMap<>(); + gservicesFlags = map; + } + map.put(flag.name, flag); + } + + public boolean shouldSpoofSelfPermissionCheck(String perm) { + return spoofSelfPermissionChecks.contains(perm); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel p, int wtpFlags) { + p.writeLong(version); + + writeStringArrayMapMap(flags, GmsFlag::writeMapEntry, p); + writeArrayMap(gservicesFlags, GmsFlag::writeMapEntry, p); + + writeStringArrayMapMap(stubs, p); + + p.writeLong(maxGmsCoreVersion); + p.writeLong(maxPlayStoreVersion); + + writeArrayMapStringStringList(forceDefaultFlagsMap, p); + writeArrayMapStringStringList(spoofSelfPermissionChecksMap, p); + { + var map = gmsServiceBrokerPermissionBypasses; + int cnt = map.size(); + p.writeInt(cnt); + for (int i = 0; i < cnt; ++i) { + p.writeInt(map.keyAt(i)); + p.writeArraySet(map.valueAt(i)); + } + } + writeStringArrayMapMap(forceComponentEnabledSettingsMap, Parcel::writeString, Parcel::writeInt, p); + } + + public static final Creator CREATOR = new Creator<>() { + @Override + public GmsCompatConfig createFromParcel(Parcel p) { + GmsCompatConfig r = new GmsCompatConfig(); + r.version = p.readLong(); + + readStringArrayMapMap(p, r.flags, GmsFlag::readMapEntry); + r.gservicesFlags = readArrayMap(p, GmsFlag::readMapEntry); + + readStringArrayMapMap(p, r.stubs, StubDef.CREATOR); + + r.maxGmsCoreVersion = p.readLong(); + r.maxPlayStoreVersion = p.readLong(); + + readArrayMapStringStringList(p, r.forceDefaultFlagsMap); + readArrayMapStringStringList(p, r.spoofSelfPermissionChecksMap); + { + int cnt = p.readInt(); + ClassLoader cl = String.class.getClassLoader(); + for (int i = 0; i < cnt; ++i) { + r.gmsServiceBrokerPermissionBypasses.put(p.readInt(), (ArraySet) p.readArraySet(cl)); + } + } + + readStringArrayMapMap(p, r.forceComponentEnabledSettingsMap, + Parcel::readString, Parcel::readInt); + + if (GmsCompat.isEnabled()) { + String pkgName = GmsCompat.appContext().getPackageName(); + + ArrayList perms = r.spoofSelfPermissionChecksMap.get(pkgName); + r.spoofSelfPermissionChecks = perms != null ? + new ArraySet<>(perms) : + new ArraySet<>(); + } + return r; + } + + @Override + public GmsCompatConfig[] newArray(int size) { + return new GmsCompatConfig[size]; + } + }; + + static void writeStringArrayMapMap( + ArrayMap> outerMap, Parcel p) { + writeStringArrayMapMap(outerMap, Parcel::writeString, + (parcel, v) -> v.writeToParcel(parcel, 0), p); + } + + static void readStringArrayMapMap(Parcel p, + ArrayMap> outerMap, Parcelable.Creator valueCreator) { + + readStringArrayMapMap(p, outerMap, Parcel::readString, valueCreator::createFromParcel); + } + + static void writeStringArrayMapMap(ArrayMap> outerMap, + BiConsumer writeK, BiConsumer writeV, Parcel p) { + ArrayMapEntryWriter entryWriter = (map, i, parcel) -> { + writeK.accept(p, map.keyAt(i)); + writeV.accept(p, map.valueAt(i)); + }; + writeStringArrayMapMap(outerMap, entryWriter, p); + } + + static void readStringArrayMapMap(Parcel p, ArrayMap> outerMap, + Function readK, Function readV) { + ArrayMapEntryReader entryReader = (parcel, map) -> { + map.append(readK.apply(p), readV.apply(p)); + }; + readStringArrayMapMap(p, outerMap, entryReader); + } + + interface ArrayMapEntryWriter { + void write(ArrayMap map, int idx, Parcel dst); + } + + interface ArrayMapEntryReader { + void read(Parcel p, ArrayMap dst); + } + + static void writeStringArrayMapMap(ArrayMap> outerMap, + ArrayMapEntryWriter entryWriter, Parcel p) { + int outerCnt = outerMap.size(); + p.writeInt(outerCnt); + for (int outerIdx = 0; outerIdx < outerCnt; ++outerIdx) { + String outerK = outerMap.keyAt(outerIdx); + p.writeString(outerK); + writeArrayMap(outerMap.valueAt(outerIdx), entryWriter, p); + } + } + + static void readStringArrayMapMap(Parcel p, ArrayMap> outerMap, + ArrayMapEntryReader entryReader) { + int outerCnt = p.readInt(); + outerMap.ensureCapacity(outerCnt); + for (int outerIdx = 0; outerIdx < outerCnt; ++outerIdx) { + String outerK = p.readString(); + outerMap.put(outerK, readArrayMap(p, entryReader)); + } + } + + static void writeArrayMap(ArrayMap map, ArrayMapEntryWriter entryWriter, + Parcel p) { + int cnt = map.size(); + p.writeInt(cnt); + for (int i = 0; i < cnt; ++i) { + entryWriter.write(map, i, p); + } + } + + static ArrayMap readArrayMap(Parcel p, ArrayMapEntryReader entryReader) { + int cnt = p.readInt(); + var map = new ArrayMap(cnt); + for (int i = 0; i < cnt; ++i) { + entryReader.read(p, map); + } + return map; + } + + static void writeArrayMapStringStringList(ArrayMap> map, Parcel p) { + int cnt = map.size(); + p.writeInt(cnt); + for (int i = 0; i < cnt; ++i) { + p.writeString(map.keyAt(i)); + p.writeStringList(map.valueAt(i)); + } + } + + static void readArrayMapStringStringList(Parcel p, ArrayMap> map) { + int cnt = p.readInt(); + map.ensureCapacity(cnt); + for (int i = 0; i < cnt; ++i) { + map.put(p.readString(), p.createStringArrayList()); + } + } +} diff --git a/core/java/com/android/internal/gmscompat/GmsCompatLib.java b/core/java/com/android/internal/gmscompat/GmsCompatLib.java new file mode 100644 index 0000000000000..78f60cba5dd47 --- /dev/null +++ b/core/java/com/android/internal/gmscompat/GmsCompatLib.java @@ -0,0 +1,38 @@ +package com.android.internal.gmscompat; + +import android.content.Context; +import android.content.pm.PackageManager; + +import com.android.internal.util.Preconditions; + +public class GmsCompatLib { + private static IGmsCompatLib instance; + + public static IGmsCompatLib get() { + return instance; + } + + public static void init(Context appContext, String processName) { + Preconditions.checkState(instance == null); + + Context libCtx; + try { + int flags = Context.CONTEXT_INCLUDE_CODE + // GmsCompatLib is part of system image + | Context.CONTEXT_IGNORE_SECURITY; + libCtx = appContext.createPackageContext("app.grapheneos.gmscompat.lib", flags); + } catch (PackageManager.NameNotFoundException e) { + throw new IllegalStateException(e); + } + + IGmsCompatLib lib; + try { + Class cls = libCtx.getClassLoader().loadClass("app.grapheneos.gmscompat.lib.GmsCompatLibImpl"); + lib = (IGmsCompatLib) cls.getDeclaredConstructor().newInstance(); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException(e); + } + instance = lib; + lib.init(appContext, libCtx, processName); + } +} diff --git a/core/java/com/android/internal/gmscompat/GmsCoreConst.java b/core/java/com/android/internal/gmscompat/GmsCoreConst.java new file mode 100644 index 0000000000000..47f737f8f9d94 --- /dev/null +++ b/core/java/com/android/internal/gmscompat/GmsCoreConst.java @@ -0,0 +1,6 @@ +package com.android.internal.gmscompat; + +public interface GmsCoreConst { + int NOTIFICATION_ID_BASE = 0x7fff_0000; + int NOTIF_ID_MEDIA_PROJECTION_SERVICE = NOTIFICATION_ID_BASE + 1; +} diff --git a/core/java/com/android/internal/gmscompat/GmsHooks.java b/core/java/com/android/internal/gmscompat/GmsHooks.java new file mode 100644 index 0000000000000..3fce37b1116cb --- /dev/null +++ b/core/java/com/android/internal/gmscompat/GmsHooks.java @@ -0,0 +1,748 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.gmscompat; + +import android.Manifest; +import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.ActivityManager; +import android.app.ActivityManager.RunningAppProcessInfo; +import android.app.Application; +import android.app.ApplicationErrorReport; +import android.app.BroadcastOptions; +import android.app.PendingIntent; +import android.app.Service; +import android.app.compat.gms.GmsCompat; +import android.content.ComponentName; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +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; +import android.os.Parcel; +import android.os.PowerExemptionManager; +import android.os.Process; +import android.os.RemoteException; +import android.os.SystemClock; +import android.os.UserHandle; +import android.provider.Downloads; +import android.provider.Settings; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.Log; +import android.util.SparseArray; +import android.webkit.WebView; + +import com.android.internal.gmscompat.client.GmsCompatClientService; +import com.android.internal.gmscompat.flags.GmsFlag; +import com.android.internal.gmscompat.flags.GmsFlagOverrides; +import com.android.internal.gmscompat.gcarriersettings.GCarrierSettingsApp; +import com.android.internal.gmscompat.gcarriersettings.TestCarrierConfigService; +import com.android.internal.gmscompat.sysservice.GmcPackageManager; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; +import java.util.function.Consumer; +import java.util.regex.Pattern; + +import static com.android.internal.gmscompat.GmsInfo.PACKAGE_GMS_CORE; + +public final class GmsHooks { + private static final String TAG = "GmsCompat/Hooks"; + + private static volatile GmsCompatConfig config; + + public static final String PERSISTENT_GmsCore_PROCESS = PACKAGE_GMS_CORE + ".persistent"; + public static boolean inPersistentGmsCoreProcess; + public static final String UI_GmsCore_PROCESS = PACKAGE_GMS_CORE + ".ui"; + + public static GmsCompatConfig config() { + // thread-safe: immutable after publication + return config; + } + + public static void init(Context ctx, String packageName, String processName) { + if (!packageName.equals(processName)) { + // Fix RuntimeException: Using WebView from more than one process at once with the same data + // directory is not supported. https://crbug.com/558377 + WebView.setDataDirectorySuffix("process-shim--" + processName); + } + + if (GmsCompat.isGmsCore()) { + inPersistentGmsCoreProcess = processName.equals(PERSISTENT_GmsCore_PROCESS); + } + + GmsCompatLib.init(ctx, processName); + + if (GmsCompat.isPlayStore()) { + PlayStoreHooks.init(); + } + + if (GmsCompat.isGCarrierSettings()) { + GCarrierSettingsApp.init(); + } + + configUpdateLock = new Object(); + tlPermissionsToSpoof = new ThreadLocal<>(); + + // Locking is needed to prevent a race that would occur if config is updated via + // BinderGca2Gms#updateConfig in the time window between BinderGms2Gca#connect and setConfig() + // call below. Older GmsCompatConfig would overwrite the newer one in that case. + synchronized (configUpdateLock) { + GmsCompatConfig config = GmsCompatApp.connect(ctx, processName); + setConfig(config); + } + + Thread.setUncaughtExceptionPreHandler(new UncaughtExceptionPreHandler()); + + if (inPersistentGmsCoreProcess) { + GmsFlagOverrides.init(ctx); + } + + GmcPackageManager.init(ctx); + } + + static Object configUpdateLock; + + static void setConfig(GmsCompatConfig c) { + // configUpdateLock should never be null at this point, it's initialized before GmsCompatApp + // gets a handle to BinderGca2Gms that is used for updating GmsCompatConfig + synchronized (configUpdateLock) { + config = c; + } + } + + static class UncaughtExceptionPreHandler implements Thread.UncaughtExceptionHandler { + final Thread.UncaughtExceptionHandler orig = Thread.getUncaughtExceptionPreHandler(); + + @Override + public void uncaughtException(Thread t, Throwable e) { + Context ctx = GmsCompat.appContext(); + + ApplicationErrorReport aer = new ApplicationErrorReport(); + aer.type = ApplicationErrorReport.TYPE_CRASH; + aer.crashInfo = new ApplicationErrorReport.ParcelableCrashInfo(e); + + ApplicationInfo ai = ctx.getApplicationInfo(); + aer.packageName = ai.packageName; + aer.applicationInfo = ai; + aer.processName = Application.getProcessName(); + + // In some cases, GMS kills its process when it receives an uncaught exception, which + // bypasses the standard crash handling infrastructure. + // Send the report to GmsCompatApp before GMS receives the uncaughtException() callback. + + if (!shouldSkipException(e)) { + try { + GmsCompatApp.iGms2Gca().onUncaughtException(aer); + } catch (RemoteException re) { + Log.e(TAG, "", re); + } + } + + if (orig != null) { + orig.uncaughtException(t, e); + } + } + + private static boolean shouldSkipException(Throwable e) { + for (;;) { + if (e == null) { + return false; + } + + boolean skip = + // in some cases a DeadSystemRuntimeException is thrown despite the system being actually + // still alive, likely when the Binder buffer space is full and a binder transaction with + // system_server fails. + // See https://cs.android.com/android/platform/superproject/+/android-13.0.0_r3:frameworks/base/core/jni/android_util_Binder.cpp;l=894 + // (DeadObjectException is rethrown as DeadSystemRuntimeException by + // android.os.RemoteException#rethrowFromSystemServer()) + e instanceof DeadSystemRuntimeException + ; + + if (skip) { + return true; + } + + e = e.getCause(); + } + } + } + + // ContextImpl#getSystemService(String) + public static boolean isHiddenSystemService(String name) { + // return true only for services that are null-checked + switch (name) { + case Context.WIFI_SCANNING_SERVICE: + return !GmsCompat.isAndroidAuto(); + case Context.CONTEXTHUB_SERVICE: + case Context.APP_INTEGRITY_SERVICE: + // used for factory reset protection + case Context.PERSISTENT_DATA_BLOCK_SERVICE: + // used for updateable fonts + case Context.FONT_SERVICE: + return true; + } + return false; + } + + /** + * Use the per-app SSAID as a random serial number for SafetyNet. This doesn't necessarily make + * pass, but at least it retusn a valid "failed" response and stops spamming device key + * requests. + * + * This isn't a privacy risk because all unprivileged apps already have access to random SSAIDs. + */ + // Build#getSerial() + @SuppressLint("HardwareIds") + public static String getSerial() { + String ssaid = Settings.Secure.getString(GmsCompat.appContext().getContentResolver(), + Settings.Secure.ANDROID_ID); + String serial = ssaid.toUpperCase(); + Log.d(TAG, "Generating serial number from SSAID: " + serial); + return serial; + } + + static class RecentBinderPid implements Comparable { + int pid; + int uid; + long lastSeen; + volatile String[] packageNames; // lazily inited + + static final int MAX_MAP_SIZE = 50; + static final int MAP_SIZE_TRIM_TO = 40; + static final SparseArray map = new SparseArray(MAX_MAP_SIZE + 1); + + public int compareTo(RecentBinderPid b) { + return Long.compare(b.lastSeen, lastSeen); // newest come first + } + } + + // Remember recent Binder peers to include them in the result of ActivityManager.getRunningAppProcesses() + // Binder#execTransact(int, long, long, int) + public static void onBinderTransaction(int pid, int uid) { + SparseArray map = RecentBinderPid.map; + synchronized (map) { + RecentBinderPid rbp = map.get(pid); + if (rbp != null) { + if (rbp.uid != uid) { // pid was reused + rbp = null; + } + } + if (rbp == null) { + rbp = new RecentBinderPid(); + rbp.pid = pid; + rbp.uid = uid; + map.put(pid, rbp); + } + rbp.lastSeen = SystemClock.uptimeMillis(); + + int mapSize = map.size(); + if (mapSize <= RecentBinderPid.MAX_MAP_SIZE) { + return; + } + RecentBinderPid[] arr = new RecentBinderPid[mapSize]; + for (int i = 0; i < mapSize; ++i) { + arr[i] = map.valueAt(i); + } + // sorted by lastSeen field in reverse order + Arrays.sort(arr); + map.clear(); + for (int i = 0; i < RecentBinderPid.MAP_SIZE_TRIM_TO; ++i) { + RecentBinderPid e = arr[i]; + map.put(e.pid, e); + } + } + } + + // In some cases (Play Games Services, Play {Asset, Feature} Delivery) + // GMS relies on getRunningAppProcesses() to figure out whether its client is running. + // This workaround is racy, because unprivileged apps can't know whether an arbitrary pid is alive. + // ActivityManager#getRunningAppProcesses() + public static ArrayList addRecentlyBoundPids(Context context, + List orig) { + final RecentBinderPid[] binderPids; + final int binderPidsCount; + // copy to array to avoid long lock contention with Binder.execTransact(), + // there are expensive getPackagesForUid() calls below + { + SparseArray map = RecentBinderPid.map; + synchronized (map) { + binderPidsCount = map.size(); + binderPids = new RecentBinderPid[binderPidsCount]; + for (int i = 0; i < binderPidsCount; ++i) { + binderPids[i] = map.valueAt(i); + } + } + } + PackageManager pm = context.getPackageManager(); + ArrayList res = new ArrayList<>(orig.size() + binderPidsCount); + res.addAll(orig); + for (int i = 0; i < binderPidsCount; ++i) { + RecentBinderPid rbp = binderPids[i]; + String[] pkgs = rbp.packageNames; + if (pkgs == null) { + if (UserHandle.getUserId(rbp.uid) != UserHandle.myUserId()) { + // SystemUI from userId 0 sends callbacks to apps from all userIds via + // android.window.IOnBackInvokedCallback. + // getPackagesForUid() will fail due to missing privileged + // INTERACT_ACROSS_USERS permission + continue; + } + + pkgs = pm.getPackagesForUid(rbp.uid); + if (pkgs == null || pkgs.length == 0) { + continue; + } + // this field is volatile + rbp.packageNames = pkgs; + } + RunningAppProcessInfo pi = new RunningAppProcessInfo(); + // these fields are immutable after publication + pi.pid = rbp.pid; + pi.uid = rbp.uid; + pi.processName = pkgs[0]; + pi.pkgList = pkgs; + pi.importance = RunningAppProcessInfo.IMPORTANCE_FOREGROUND; + res.add(pi); + } + return res; + } + + // ContentResolver#query(Uri, String[], Bundle, CancellationSignal) + public static Cursor maybeModifyQueryResult(Uri uri, + @Nullable String[] projection, @Nullable Bundle queryArgs, @Nullable Cursor origCursor) { + String uriString = uri.toString(); + Log.d(TAG, "maybeModifyQueryResult for " + uriString); + + Consumer> mutator = null; + if (uriString.startsWith(GmsFlag.PHENOTYPE_URI_PREFIX)) { + List path = uri.getPathSegments(); + if (path.size() != 1) { + Log.e(TAG, "unknown phenotype uri " + uriString, new Throwable()); + return null; + } + + String namespace = path.get(0); + + GmsCompatConfig config = config(); + + ArrayList forceDefaultFlagsRegexes = config.forceDefaultFlagsMap.get(namespace); + + if (forceDefaultFlagsRegexes == null) { + return null; + } + + mutator = map -> { + if (forceDefaultFlagsRegexes != null) { + int patternCnt = forceDefaultFlagsRegexes.size(); + Pattern[] patterns = new Pattern[patternCnt]; + for (int i = 0; i < patternCnt; ++i) { + patterns[i] = Pattern.compile(forceDefaultFlagsRegexes.get(i)); + } + ArrayMap filteredMap = new ArrayMap<>(map.size()); + + outer: + for (int entryIdx = 0, entryCnt = map.size(); entryIdx < entryCnt; ++entryIdx) { + String key = map.keyAt(entryIdx); + for (int patternIdx = 0; patternIdx < patternCnt; ++patternIdx) { + if (patterns[patternIdx].matcher(key).matches()) { + continue outer; + } + } + filteredMap.put(key, map.valueAt(entryIdx)); + } + map.clear(); + map.putAll(filteredMap); + } + }; + } + + if (mutator != null) { + return modifyKvCursor(origCursor, projection, mutator); + } + + return null; + } + + private static Cursor modifyKvCursor(@Nullable Cursor origCursor, @Nullable String[] projection, + Consumer> mutator) { + final int keyIndex = 0; + final int valueIndex = 1; + final int projectionLength = 2; + + if (origCursor != null) { + projection = origCursor.getColumnNames(); + } + + boolean expectedProjection = projection != null && projection.length == projectionLength + && "key".equals(projection[keyIndex]) && "value".equals(projection[valueIndex]); + + if (!expectedProjection) { + Log.e(TAG, "unexpected projection " + Arrays.toString(projection), new Throwable()); + return null; + } + + final ArrayMap map; + if (origCursor == null) { + map = new ArrayMap<>(); + } else { + map = new ArrayMap<>(origCursor.getColumnCount() + 10); + try (Cursor orig = origCursor) { + while (orig.moveToNext()) { + String key = orig.getString(keyIndex); + String value = orig.getString(valueIndex); + + map.put(key, value); + } + } + } + + mutator.accept(map); + + final int mapSize = map.size(); + MatrixCursor result = new MatrixCursor(projection, mapSize); + + for (int i = 0; i < mapSize; ++i) { + Object[] row = new Object[projectionLength]; + row[keyIndex] = map.keyAt(i); + row[valueIndex] = map.valueAt(i); + + result.addRow(row); + } + + return result; + } + + // Instrumentation#execStartActivity(Context, IBinder, IBinder, Activity, Intent, int, Bundle) + public static void onActivityStart(int resultCode, Intent intent, int requestCode, Bundle options) { + if (resultCode != ActivityManager.START_ABORTED) { + return; + } + + // handle background activity starts, which normally require a privileged permission + + if (requestCode >= 0) { + Log.d(TAG, "attempt to call startActivityForResult() from the background " + intent, new Throwable()); + return; + } + + // needed to prevent invalid reuse of PendingIntents, see PendingIntent doc + intent.setIdentifier(UUID.randomUUID().toString()); + + Context ctx = GmsCompat.appContext(); + PendingIntent pendingIntent = PendingIntent.getActivity(ctx, 0, intent, + PendingIntent.FLAG_IMMUTABLE, options); + try { + GmsCompatApp.iGms2Gca().startActivityFromTheBackground(ctx.getPackageName(), pendingIntent); + } catch (RemoteException e) { + GmsCompatApp.callFailed(e); + } + } + + // Activity#onCreate(Bundle) + public static void activityOnCreate(Activity activity) { + + } + + // ContentResolver#insert(Uri, ContentValues, Bundle) + public static void filterContentValues(Uri url, ContentValues values) { + if (values != null && Downloads.Impl.CONTENT_URI.equals(url)) { + Integer otherUid = values.getAsInteger(Downloads.Impl.COLUMN_OTHER_UID); + if (otherUid != null) { + if (otherUid.intValue() != Process.SYSTEM_UID) { + throw new IllegalStateException("unexpected COLUMN_OTHER_UID " + otherUid); + } + // gated by the privileged ACCESS_DOWNLOAD_MANAGER_ADVANCED permission + values.remove(Downloads.Impl.COLUMN_OTHER_UID); + } + } + } + + private static boolean hasNearbyDevicesPermission() { + // "Nearby devices" user-facing permission grants multiple underlying permissions, + // checking one is enough + return GmsCompat.hasPermission(Manifest.permission.BLUETOOTH_SCAN); + } + + // ContextImpl#sendBroadcast + // ContextImpl#sendOrderedBroadcast + // ContextImpl#sendBroadcastAsUser + // ContextImpl#sendOrderedBroadcastAsUser + public static Bundle filterBroadcastOptions(Intent intent, Bundle options) { + if (options == null) { + return null; + } + + String targetPkg = intent.getPackage(); + + if (targetPkg == null) { + ComponentName cn = intent.getComponent(); + if (cn != null) { + targetPkg = cn.getPackageName(); + } + } + + if (targetPkg == null) { + return options; + } + + return filterBroadcastOptions(options, targetPkg); + } + + // PendingIntent#send + public static Bundle filterBroadcastOptions(Bundle options, String targetPkg) { + BroadcastOptions bo = new BroadcastOptions(options); + + if (bo.getTemporaryAppAllowlistType() == PowerExemptionManager.TEMPORARY_ALLOW_LIST_TYPE_NONE) { + return options; + } + // handle privileged BroadcastOptions#setTemporaryAppAllowlist() that is used for + // high-priority FCM pushes, location updates via PendingIntent, + // geofencing and activity detection notifications etc + + long duration = bo.getTemporaryAppAllowlistDuration(); + + if (duration <= 0) { + return options; + } + + GmsCompatApp.raisePackageToForeground(targetPkg, duration, + bo.getTemporaryAppAllowlistReason(), bo.getTemporaryAppAllowlistReasonCode()); + + bo.setTemporaryAppAllowlist(0, PowerExemptionManager.TEMPORARY_ALLOW_LIST_TYPE_NONE, + PowerExemptionManager.REASON_UNKNOWN, null); + return bo.toBundle(); + } + + // Parcel#readException + public static boolean interceptException(Exception e, Parcel p) { + if (!(e instanceof SecurityException)) { + return false; + } + + if (p.dataAvail() != 0) { + Log.w(TAG, "malformed Parcel: dataAvail() " + p.dataAvail() + " after exception", e); + return false; + } + + StubDef stub = StubDef.find(e.getStackTrace(), config(), StubDef.FIND_MODE_Parcel); + + if (stub == null) { + return false; + } + + boolean res = stub.stubOutMethod(p); + + String logTag = "GmcDynStub"; + if (GmsCompat.isDevBuild() || Log.isLoggable(logTag, Log.DEBUG)) { + Log.d(logTag, res ? "intercepted" : "stubOut failed", e); + } + + return res; + } + + public static void onSQLiteOpenHelperConstructed(SQLiteOpenHelper h, @Nullable Context context) { + if (context == null) { + return; + } + + if (GmsCompat.isGmsCore()) { + if (inPersistentGmsCoreProcess) { + if ("phenotype.db".equals(h.getDatabaseName()) && !context.isDeviceProtectedStorage()) { + if (phenotypeDb != null) { + Log.w(TAG, "reassigning phenotypeDb", new Throwable()); + } + phenotypeDb = h; + } + } + } + } + + @Nullable + public static Service maybeInstantiateService(String className) { + if (GmsCompatClientService.class.getName().equals(className)) { + return new GmsCompatClientService(); + } + + if (GmsCompat.isEnabled()) { + if (GmsCompat.isGmsCore()) { + if (GmcMediaProjectionService.class.getName().equals(className)) { + return new GmcMediaProjectionService(); + } + } + if (GmsCompat.isGCarrierSettings()) { + if (TestCarrierConfigService.class.getName().equals(className)) { + return new TestCarrierConfigService(); + } + } + } + + return null; + } + + private static volatile SQLiteOpenHelper phenotypeDb; + public static SQLiteOpenHelper getPhenotypeDb() { return phenotypeDb; } + + private static ThreadLocal> tlPermissionsToSpoof; + + public static boolean shouldSpoofSelfPermissionCheck(String perm) { + ArraySet set = tlPermissionsToSpoof.get(); + if (set == null) { + return false; + } + + return set.contains(perm); + } + + public static final String GMS_SERVICE_BROKER_INTERFACE_DESCRIPTOR = + "com.google.android.gms.common.internal.IGmsServiceBroker"; + + public static boolean onBeginGmsServiceBrokerCall(int transactionCode, Parcel data) { + if (transactionCode != 46) { // getService() method + return false; + } + + try { + data.enforceInterface(GMS_SERVICE_BROKER_INTERFACE_DESCRIPTOR); + // IGmsCallbacks binder + data.readStrongBinder(); + + if (data.readInt() == 1) { // GetServiceRequest is present + // GetServiceRequest object header + data.readInt(); + data.readInt(); + + // version + data.readInt(); + data.readInt(); + + // id of serviceId property + data.readInt(); + + int serviceId = data.readInt(); + + ArraySet permsToSpoof = config().gmsServiceBrokerPermissionBypasses.get(serviceId); + if (permsToSpoof != null) { + Log.d(TAG, "start spoofing self permission checks for getService() call for API " + + serviceId + ", perms: " + Arrays.toString(permsToSpoof.toArray())); + tlPermissionsToSpoof.set(permsToSpoof); + // there's a second layer of caching inside GmsCore, need to notify permission + // change listener used by that cache + GmcPackageManager.notifyPermissionsChangeListeners(); + return true; + } + } + } finally { + data.setDataPosition(0); + } + + return false; + } + + public static void onEndGmsServiceBrokerCall() { + Log.d(TAG, "end self permission check spoofing"); + tlPermissionsToSpoof.set(null); + // invalidate the cache of permission state inside GmsCore + 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) { + return null; + } + + String ifaceName = null; + try { + ifaceName = binder.getInterfaceDescriptor(); + } catch (RemoteException e) { + Log.d(TAG, "", e); + } + + if (ifaceName == null) { + return null; + } + + return GmcBinderDefs.maybeOverrideBinder(binder, ifaceName); + } + + private GmsHooks() {} +} diff --git a/core/java/com/android/internal/gmscompat/GmsInfo.java b/core/java/com/android/internal/gmscompat/GmsInfo.java new file mode 100644 index 0000000000000..d30b8b14162d8 --- /dev/null +++ b/core/java/com/android/internal/gmscompat/GmsInfo.java @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.gmscompat; + +import android.ext.PackageId; + +/** @hide */ +public final class GmsInfo { + // Package names for GMS apps + public static final String PACKAGE_GMS_CORE = PackageId.GMS_CORE_NAME; // "Play services" + public static final String PACKAGE_PLAY_STORE = PackageId.PLAY_STORE_NAME; + + // "Google" app. "GSA" (G Search App) is its internal name + public static final String PACKAGE_GSA = PackageId.G_SEARCH_APP_NAME; + + // Used for restricting accessibility of exported components, reducing the scope of broadcasts, etc. + // Held by GmsCore and Play Store. + public static final String SIGNATURE_PROTECTED_PERMISSION = "com.google.android.providers.gsf.permission.WRITE_GSERVICES"; +} diff --git a/core/java/com/android/internal/gmscompat/IClientOfGmsCore2Gca.aidl b/core/java/com/android/internal/gmscompat/IClientOfGmsCore2Gca.aidl new file mode 100644 index 0000000000000..5cf4000640161 --- /dev/null +++ b/core/java/com/android/internal/gmscompat/IClientOfGmsCore2Gca.aidl @@ -0,0 +1,18 @@ +package com.android.internal.gmscompat; + +import android.os.BinderDef; + +import com.android.internal.gmscompat.fileservice.IFileProxyService; + +// calls from clients of GMS Core to GmsCompatApp +interface IClientOfGmsCore2Gca { + @nullable BinderDef maybeGetBinderDef(String callerPkg, int processState, String ifaceName); + + IFileProxyService getGmsCoreFileProxyService(); + + oneway void showMissingAppNotification(String pkgName); + + oneway void showPlayIntegrityNotification(String pkgName, boolean isBlocked); + + oneway void onGoogleIdCredentialOptionInit(); +} diff --git a/core/java/com/android/internal/gmscompat/IGca2Gms.aidl b/core/java/com/android/internal/gmscompat/IGca2Gms.aidl new file mode 100644 index 0000000000000..455a638ab699e --- /dev/null +++ b/core/java/com/android/internal/gmscompat/IGca2Gms.aidl @@ -0,0 +1,16 @@ +package com.android.internal.gmscompat; + +import android.app.ApplicationErrorReport; +import android.app.PendingIntent; +import android.content.Intent; + +import com.android.internal.gmscompat.GmsCompatConfig; + +// calls from GmsCompatApp to GMS components +interface IGca2Gms { + // intentionally not oneway to simplify code in GmsCompatApp + void updateConfig(in GmsCompatConfig newConfig); + void invalidateConfigCaches(); + + boolean startActivityIfVisible(in Intent intent); +} diff --git a/core/java/com/android/internal/gmscompat/IGms2Gca.aidl b/core/java/com/android/internal/gmscompat/IGms2Gca.aidl new file mode 100644 index 0000000000000..c4a30840c1f48 --- /dev/null +++ b/core/java/com/android/internal/gmscompat/IGms2Gca.aidl @@ -0,0 +1,52 @@ +package com.android.internal.gmscompat; + +import android.app.ApplicationErrorReport; +import android.app.Notification; +import android.app.PendingIntent; +import android.content.Intent; +import android.database.IContentObserver; +import android.os.BinderDef; + +import com.android.internal.gmscompat.GmsCompatConfig; +import com.android.internal.gmscompat.IGca2Gms; +import com.android.internal.gmscompat.fileservice.IFileProxyService; + +// calls from GMS components to GmsCompatApp +interface IGms2Gca { + GmsCompatConfig connectGmsCore(String processName, IGca2Gms iGca2Gms, @nullable IFileProxyService dynamiteFileProxyService); + GmsCompatConfig connect(String packageName, String processName, IGca2Gms iGca2Gms); + + @nullable BinderDef maybeGetBinderDef(String callerPkg, int processState, String ifaceName); + + oneway void onPlayStorePendingUserAction(in Intent actionIntent, @nullable String pkgName); + @nullable Intent maybeGetPlayStorePendingUserActionIntent(); + + oneway void showPlayStoreMissingObbPermissionNotification(); + + oneway void startActivityFromTheBackground(String callerPkg, in PendingIntent intent); + + oneway void showGmsCoreMissingPermissionForNearbyShareNotification(); + + oneway void showGmsCoreMissingNearbyDevicesPermissionGeneric(); + + oneway void showMissingPostNotifsPermissionNotification(String callerPkg); + + oneway void maybeShowContactsSyncNotification(); + + oneway void maybeShowGmsCoreRestrictedBackgroundDataNotif(); + + void onUncaughtException(in ApplicationErrorReport aer); + GmsCompatConfig requestConfigUpdate(String reason); + + @nullable String privSettingsGetString(String ns, String key); + boolean privSettingsPutString(String ns, String key, @nullable String value); + boolean privSettingsPutStrings(String ns, in String[] keys, in String[] values); + void privSettingsRegisterObserver(String ns, String key, IContentObserver observer); + void privSettingsUnregisterObserver(IContentObserver observer); + + 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 new file mode 100644 index 0000000000000..837189b72aafd --- /dev/null +++ b/core/java/com/android/internal/gmscompat/IGmsCompatLib.java @@ -0,0 +1,49 @@ +package com.android.internal.gmscompat; + +import android.annotation.Nullable; +import android.content.Context; +import android.content.Context.BindServiceFlags; +import android.content.Intent; +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 { + void init(Context appContext, Context libContext, String processName); + + /** @see Context#bindService(Intent, BindServiceFlags, Executor, ServiceConnection) */ + @Nullable + ServiceConnection maybeReplaceServiceConnection(Intent service, long flags, UserHandle user, ServiceConnection orig); + + /** @see BinderProxy#queryLocalInterface(String) */ + @Nullable + IInterface maybeProvideBinderProxyInterface(BinderProxy binderProxy, String ifaceDescriptor); + + /** @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; +} diff --git a/core/java/com/android/internal/gmscompat/PlayStoreHooks.java b/core/java/com/android/internal/gmscompat/PlayStoreHooks.java new file mode 100644 index 0000000000000..8f083a3d9b8eb --- /dev/null +++ b/core/java/com/android/internal/gmscompat/PlayStoreHooks.java @@ -0,0 +1,475 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.gmscompat; + +import android.annotation.Nullable; +import android.app.Activity; +import android.app.Application; +import android.app.PendingIntent; +import android.app.compat.gms.GmsCompat; +import android.app.usage.StorageStats; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.IntentSender; +import android.content.pm.ActivityInfo; +import android.content.pm.GosPackageState; +import android.content.pm.GosPackageStateFlag; +import android.content.pm.IPackageDataObserver; +import android.content.pm.IPackageDeleteObserver; +import android.content.pm.PackageInstaller; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.ext.PackageId; +import android.net.Uri; +import android.os.Bundle; +import android.os.Environment; +import android.os.LocaleList; +import android.os.RemoteException; +import android.os.storage.StorageManager; +import android.provider.Settings; +import android.text.TextUtils; +import android.util.ArraySet; +import android.util.Log; + +import com.android.internal.gmscompat.sysservice.GmcPackageManager; +import com.android.internal.gmscompat.util.GmcActivityUtils; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.BiConsumer; + +import static java.util.Objects.requireNonNull; + +public final class PlayStoreHooks { + private static final String TAG = "GmsCompat/PlayStore"; + + static PackageManager packageManager; + + public static void init() { + obbDir = Environment.getExternalStorageDirectory().getPath() + "/Android/obb"; + playStoreObbDir = obbDir + '/' + GmsInfo.PACKAGE_PLAY_STORE; + File.mkdirsFailedHook = PlayStoreHooks::mkdirsFailed; + Context ctx = GmsCompat.appContext(); + packageManager = ctx.getPackageManager(); + InternalBroadcastReceiver.register(ctx); + } + + // PackageInstaller#createSession + public static void adjustSessionParams(PackageInstaller.SessionParams params) { + String pkg = requireNonNull(params.appPackageName); + + switch (pkg) { + case GmsInfo.PACKAGE_GMS_CORE: + case GmsInfo.PACKAGE_PLAY_STORE: + String updateRequestReason = "Play Store created PackageInstaller SessionParams for " + pkg; + GmsCompatConfig config; + try { + config = GmsCompatApp.iGms2Gca().requestConfigUpdate(updateRequestReason); + } catch (RemoteException e) { + throw GmsCompatApp.callFailed(e); + } + if (GmsHooks.config().version != config.version) { + GmsHooks.setConfig(config); + } + break; + } + + switch (pkg) { + case GmsInfo.PACKAGE_GMS_CORE: + params.maxAllowedVersion = GmsHooks.config().maxGmsCoreVersion; + break; + case GmsInfo.PACKAGE_PLAY_STORE: + params.maxAllowedVersion = GmsHooks.config().maxPlayStoreVersion; + break; + } + } + + // PackageInstaller.Session#commit(IntentSender) + public static IntentSender wrapCommitStatusReceiver(PackageInstaller.Session session, IntentSender statusReceiver) { + return PackageInstallerStatusForwarder.register((intent, extras) -> sendIntent(intent, statusReceiver)) + .getIntentSender(); + } + + public static void onActivityResumed(Activity activity) { + Intent pendingActionIntent; + try { + pendingActionIntent = GmsCompatApp.iGms2Gca().maybeGetPlayStorePendingUserActionIntent(); + } catch (RemoteException e) { + throw GmsCompatApp.callFailed(e); + } + if (pendingActionIntent != null) { + activity.startActivity(pendingActionIntent); + } + } + + static class PackageInstallerStatusForwarder extends BroadcastReceiver { + private Context context; + private PendingIntent pendingIntent; + private BiConsumer target; + + private static final AtomicLong lastId = new AtomicLong(); + + static PendingIntent register(BiConsumer target) { + PackageInstallerStatusForwarder sf = new PackageInstallerStatusForwarder(); + Context context = GmsCompat.appContext(); + sf.context = context; + sf.target = target; + + String intentAction = context.getPackageName() + + "." + PackageInstallerStatusForwarder.class.getName() + "." + + lastId.getAndIncrement(); + + var intent = new Intent(intentAction); + intent.setPackage(context.getPackageName()); + + sf.pendingIntent = PendingIntent.getBroadcast(context, 0, intent, + PendingIntent.FLAG_CANCEL_CURRENT | + PendingIntent.FLAG_MUTABLE); + + context.registerReceiver(sf, new IntentFilter(intentAction), Context.RECEIVER_NOT_EXPORTED); + return sf.pendingIntent; + } + + public void onReceive(Context receiverContext, Intent intent) { + Bundle extras = intent.getExtras(); + int status = getIntFromBundle(extras, PackageInstaller.EXTRA_STATUS); + + if (status == PackageInstaller.STATUS_PENDING_USER_ACTION) { + Intent confirmationIntent = intent.getParcelableExtra(Intent.EXTRA_INTENT); + + String packageName = null; + + if (extras.containsKey(PackageInstaller.EXTRA_SESSION_ID)) { + int sessionId = getIntFromBundle(extras, PackageInstaller.EXTRA_SESSION_ID); + PackageInstaller pkgInstaller = packageManager.getPackageInstaller(); + PackageInstaller.SessionInfo si = pkgInstaller.getSessionInfo(sessionId); + if (si != null) { + packageName = si.getAppPackageName(); + } + } + + try { + GmsCompatApp.iGms2Gca().onPlayStorePendingUserAction(confirmationIntent, packageName); + } catch (RemoteException e) { + GmsCompatApp.callFailed(e); + } + + // confirmationIntent has a PendingIntent to this instance, don't unregister yet + return; + } + pendingIntent.cancel(); + context.unregisterReceiver(this); + + target.accept(intent, extras); + } + } + + // Request user action to uninstall a package + public static void deletePackage(PackageManager pm, String packageName, IPackageDeleteObserver observer, int flags) { + if (flags != 0) { + throw new IllegalStateException("unexpected flags: " + flags); + } + + // Play Store expects call to deletePackage() to always succeed, which almost always happens + // when it has the privileged DELETE_PACKAGES permission. + // This is not the case when Play Store has only the unprivileged REQUEST_DELETE_PACKAGES + // permission, which requires confirmation from the user. + // There are two difficulties: + // - user may reject the confirmation prompt, which produces DELETE_FAILED_ABORTED error code, + // which Play Store ignores + // - user may dismiss the confirmation prompt without making a choice, which doesn't make + // any callback at all + // In both cases, Play Store remains stuck in "Uninstalling..." state for that package. + // This state is written to persistent storage, it remains stuck even after device reboot. + // + // To work-around all these issues, pretend that the package was uninstalled and then installed + // again, which moves the package state from "Uninstalling..." to "Installed" state, and + // launch the uninstall request separately. + + PendingIntent pi = PackageInstallerStatusForwarder.register((BiConsumer) (intent, extras) -> { + Log.d(TAG, "uninstall status " + extras.getString(PackageInstaller.EXTRA_STATUS_MESSAGE)); + }); + pm.getPackageInstaller().uninstall(packageName, pi.getIntentSender()); + + GmsCompat.appContext().getMainThreadHandler().postDelayed(() -> { + try { + // Play Store ignores this callback as of version 33.6.13, but provide it anyway + // in case it's fixed + observer.packageDeleted(packageName, PackageManager.DELETE_FAILED_ABORTED); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + + resetPackageState(packageName); + }, 100L); // delay the callback for to workaround a race condition in Play Store + } + + public static class InternalBroadcastReceiver extends BroadcastReceiver { + private static final String TAG = "GmcInternalBroacastReceiver"; + + private static final String ACTION_PREFIX = "GmsCompat.InternalBroadcastReceiver"; + private static final String ACTION_SEND_SELF_BROADCAST = ACTION_PREFIX + ".ACTION_SEND_SELF_BROADCAST"; + private static final String ACTION_REMOVE_PSEUDO_DISABLED_PKG = ACTION_PREFIX + ".ACTION_REMOVE_PSEUDO_DISABLED_PKG"; + private static final String EXTRA_INTENTS = "intents"; + + static void register(Context ctx) { + var f = new IntentFilter(ACTION_SEND_SELF_BROADCAST); + f.addAction(ACTION_REMOVE_PSEUDO_DISABLED_PKG); + ctx.registerReceiver(new InternalBroadcastReceiver(), f, Context.RECEIVER_NOT_EXPORTED); + } + + public static void sendManualBroadcasts(Context ctx, Intent... intents) { + Log.d(TAG, "sendManualBroadcasts: " + Arrays.toString(intents)); + var i = new Intent(ACTION_SEND_SELF_BROADCAST); + i.putExtra(EXTRA_INTENTS, intents); + sendBroadcast(ctx, i); + } + + public static void removePseudoDisabledPackage(Context ctx, String pkgName) { + Log.d(TAG, "removePseudoDisabledPackage: " + pkgName); + var i = new Intent(ACTION_REMOVE_PSEUDO_DISABLED_PKG); + i.putExtra(Intent.EXTRA_PACKAGE_NAME, pkgName); + sendBroadcast(ctx, i); + } + + private static void sendBroadcast(Context ctx, Intent i) { + i.setPackage(ctx.getPackageName()); + ctx.sendBroadcast(i); + } + + @Override + public void onReceive(Context context, Intent intent) { + Log.d(TAG, "onReceive: " + intent + ", process: " + Application.getProcessName()); + switch (intent.getAction()) { + case ACTION_SEND_SELF_BROADCAST: { + Intent[] intents = intent.getParcelableArrayExtra(EXTRA_INTENTS, Intent.class); + sendManualSelfBroadcasts(requireNonNull(intents)); + break; + } + case ACTION_REMOVE_PSEUDO_DISABLED_PKG: { + String pkg = intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME); + GmcPackageManager.removePseudoDisabledPackage(pkg); + break; + } + } + } + } + + static void sendManualSelfBroadcasts(Intent[] broadcasts) { + Context context = GmsCompat.appContext(); + + // default ClassLoader fails to load the needed class + ClassLoader cl = context.getClassLoader(); + + String selfProcessName = Application.getProcessName(); + + for (Intent intent : broadcasts) { + intent.setPackage(context.getPackageName()); + List result = packageManager.queryBroadcastReceivers(intent, 0); + for (ResolveInfo resolveInfo : result) { + ActivityInfo receiver = resolveInfo.activityInfo; + if (receiver == null) { + continue; + } + String processName = receiver.processName; + if (TextUtils.isEmpty(processName)) { + processName = context.getPackageName(); + } + if (!processName.equals(selfProcessName)) { + continue; + } + String clsName = receiver.name; + Log.d(TAG, "sending manual self broadcast to " + clsName + ": " + intent); + try { + Class cls = cl.loadClass(clsName); + BroadcastReceiver br = (BroadcastReceiver) cls.newInstance(); + br.onReceive(context, intent); + } catch (ReflectiveOperationException e) { + Log.e(TAG, clsName, e); + } + } + } + } + + // If state transition that is expected to never fail by Play Store does fail, it may get stuck + // in the old state. This happens, for example, when package uninstall fails. + // To work-around this, pretend that the package was removed and installed again + public static void resetPackageState(String packageName) { + updatePackageState(packageName, Intent.ACTION_PACKAGE_REMOVED, Intent.ACTION_PACKAGE_ADDED); + } + + public static void updatePackageState(String packageName, String... actions) { + var intents = new Intent[actions.length]; + Uri uri = packageUri(packageName); + for (int i = 0; i < intents.length; ++i) { + intents[i] = new Intent(actions[i], uri); + } + InternalBroadcastReceiver.sendManualBroadcasts(GmsCompat.appContext(), intents); + } + + // Called during self-update sequence because PackageManager requires + // the restricted CLEAR_APP_CACHE permission + public static void freeStorageAndNotify(String volumeUuid, long idealStorageSize, + IPackageDataObserver observer) { + if (volumeUuid != null) { + throw new IllegalStateException("unexpected volumeUuid " + volumeUuid); + } + StorageManager sm = GmsCompat.appContext().getSystemService(StorageManager.class); + boolean success = false; + try { + sm.allocateBytes(StorageManager.UUID_DEFAULT, idealStorageSize); + success = true; + } catch (IOException e) { + e.printStackTrace(); + } + try { + // same behavior as PackageManagerService#freeStorageAndNotify() + String packageName = null; + observer.onRemoveCompleted(packageName, success); + } catch (RemoteException e) { + throw e.rethrowAsRuntimeException(); + } + } + + // StorageStatsManager#queryStatsForPackage(UUID, String, UserHandle) + public static StorageStats queryStatsForPackage(String packageName) throws PackageManager.NameNotFoundException { + String apkPath = packageManager.getApplicationInfo(packageName, 0).sourceDir; + + StorageStats stats = new StorageStats(); + stats.codeBytes = new File(apkPath).length(); + // leave dataBytes, cacheBytes, externalCacheBytes at 0 + return stats; + } + + // ApplicationPackageManager#setApplicationEnabledSetting + public static void setApplicationEnabledSetting(String packageName, int newState) { + if (newState == PackageManager.COMPONENT_ENABLED_STATE_ENABLED + && GmcActivityUtils.getMostRecentVisibleActivity() != null) + { + openAppSettings(packageName); + } + } + + private static String obbDir; + private static String playStoreObbDir; + + // File#mkdirs() + public static void mkdirsFailed(File file) { + String path = file.getPath(); + + if (path.startsWith(obbDir) && !path.startsWith(playStoreObbDir)) { + GosPackageState ps = GosPackageState.getForSelf(GmsCompat.appContext()); + boolean hasObbAccess = ps.hasFlag(GosPackageStateFlag.ALLOW_ACCESS_TO_OBB_DIRECTORY); + + if (!hasObbAccess) { + try { + GmsCompatApp.iGms2Gca().showPlayStoreMissingObbPermissionNotification(); + } catch (RemoteException e) { + GmsCompatApp.callFailed(e); + } + } + } + } + + static Uri packageUri(String packageName) { + return Uri.fromParts("package", packageName, null); + } + + // Unfortunately, there's no other way to ensure that the value is present and is of the right type. + // Note that Intent.getExtras() makes a copy of the Bundle each time, so reuse its result + static int getIntFromBundle(Bundle b, String key) { + return ((Integer) b.get(key)).intValue(); + } + + static void openAppSettings(String packageName) { + Intent i = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + i.setData(packageUri(packageName)); + // FLAG_ACTIVITY_CLEAR_TASK is needed to ensure that the right screen is shown (it's a bug in the Settings app) + i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); + GmsCompat.appContext().startActivity(i); + } + + static void sendIntent(Intent intent, IntentSender target) { + try { + target.sendIntent(GmsCompat.appContext(), 0, intent, null, null); + } catch (IntentSender.SendIntentException e) { + Log.d(TAG, "", e); + } + } + + public static boolean isInstallAllowed(String pkgName, ContentResolver cr) { + switch (pkgName) { + case PackageId.GMS_CORE_NAME: + case PackageId.PLAY_STORE_NAME: + case PackageId.ANDROID_AUTO_NAME: + case PackageId.PIXEL_HEALTH_NAME: + return Settings.Global.getInt(cr, "gmscompat_play_store_can_install_" + pkgName, 0) == 1; + } + return true; + } + + @Nullable + public static LocaleList overrideApplicationLocales(LocaleList actualLocales, @Nullable String targetPackage) { + Context ctx = GmsCompat.appContext(); + + ContentResolver resolver = ctx.getContentResolver(); + + String settingRegex = Settings.Global.getString(resolver, "gmscompat_play_store_fetch_all_locales"); + if (settingRegex == null) { + return null; + } + String pkgName = targetPackage != null ? targetPackage : ctx.getPackageName(); + if (!pkgName.matches(settingRegex)) { + return null; + } + + String tag = targetPackage == null ? "GmcAppLocaleSelf" : "GmcAppLocale"; + if (Log.isLoggable(tag, Log.VERBOSE)) { + Log.v(tag, "overriding locales for " + (targetPackage == null ? "self" : targetPackage), new Throwable()); + } + + int numActualLocales = actualLocales.size(); + ArraySet actualLocalesSet = new ArraySet<>(numActualLocales); + + Locale[] allLocales = Locale.getAvailableLocales(); + var res = new ArrayList(allLocales.length); + + for (int i = 0; i < numActualLocales; ++i) { + Locale l = actualLocales.get(i); + res.add(l); + actualLocalesSet.add(l); + } + + for (int i = 0; i < allLocales.length; ++i) { + Locale l = allLocales[i]; + if (!actualLocalesSet.contains(l)) { + res.add(l); + } + } + return new LocaleList(res.toArray(new Locale[0])); + } + + private PlayStoreHooks() {} +} diff --git a/core/java/com/android/internal/gmscompat/StubDef.java b/core/java/com/android/internal/gmscompat/StubDef.java new file mode 100644 index 0000000000000..c4ea421bb4988 --- /dev/null +++ b/core/java/com/android/internal/gmscompat/StubDef.java @@ -0,0 +1,293 @@ +/* + * Copyright (C) 2022 GrapheneOS + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.android.internal.gmscompat; + +import android.annotation.Nullable; +import android.app.compat.gms.GmsCompat; +import android.content.pm.ParceledListSlice; +import android.content.pm.StringParceledListSlice; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.ArrayMap; +import android.util.Log; + +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.List; + +import libcore.util.SneakyThrow; + +public class StubDef implements Parcelable { + private static final String TAG = "StubDef"; + + public int type; + public long integerVal; + public double doubleVal; + public String stringVal; + private volatile Class parcelListType; + + public static final int VOID = 0; + public static final int NULL = 1; // only for types that implement Parcelable + public static final int NULL_STRING = 2; + public static final int NULL_ARRAY = 3; + public static final int EMPTY_BYTE_ARRAY = 4; + public static final int EMPTY_INT_ARRAY = 5; + public static final int EMPTY_LONG_ARRAY = 6; + public static final int EMPTY_STRING = 7; + public static final int EMPTY_LIST = 8; + public static final int EMPTY_MAP = 9; + + public static final int BOOLEAN = 10; + public static final int BYTE = 11; + public static final int INT = 12; + public static final int LONG = 13; + public static final int FLOAT = 14; + public static final int DOUBLE = 15; + public static final int STRING = 16; + + public static final int THROW = 17; + + // see com.android.modules.utils.SynchronousResultReceiver.Result#getValue() + public static final int DEFAULT = 18; + + public static final int FIND_MODE_Parcel = 0; + public static final int FIND_MODE_SynchronousResultReceiver = 1; + + @Nullable + public static StubDef find(StackTraceElement[] stackTrace, GmsCompatConfig config, int mode) { + int firstIndex; + if (mode == FIND_MODE_Parcel) { + // first four stack trace entries are known: + // android.os.Parcel.createExceptionOrNull + // android.os.Parcel.createException + // android.os.Parcel.readException + // android.os.Parcel.readException + firstIndex = 4; + } else if (mode == FIND_MODE_SynchronousResultReceiver) { + // first entry is from GmsModuleHooks method + firstIndex = 1; + } else { + return null; + } + + ClassLoader defaultClassLoader = GmsCompat.appContext().getClassLoader(); + + StackTraceElement targetMethod = null; + Class stubProxyClass = null; + String stubProxyMethodName = null; + + // Iterate through the stack trace to find out which API call caused the exception + for (int i = firstIndex; i < stackTrace.length; ++i) { + StackTraceElement ste = stackTrace[i]; + String className = ste.getClassName(); + Class class_; + try { + class_ = Class.forName(className, false, defaultClassLoader); + } catch (ClassNotFoundException cnfe) { + class_ = null; + } + + if (class_ != null) { + ClassLoader classLoader = class_.getClassLoader(); + if (classLoader == null) { + return null; + } + + String loaderName = classLoader.getClass().getName(); + + if ("java.lang.BootClassLoader".equals(loaderName)) { + if (stubProxyClass == null && className.endsWith("$Stub$Proxy")) { + stubProxyClass = class_; + stubProxyMethodName = ste.getMethodName(); + } + if (!className.startsWith("java.lang.reflect.")) { + // app classes are never loaded with BootClassLoader + continue; + } // else target method is the previous entry that was invoked via reflection + } + } + + if (mode == FIND_MODE_Parcel && stubProxyClass == null) { + return null; + } + + if (i == firstIndex) { + return null; + } + + targetMethod = stackTrace[i - 1]; + break; + } + + if (targetMethod == null) { + return null; + } + + ArrayMap classStubs = config.stubs.get(targetMethod.getClassName()); + + if (classStubs == null) { + return null; + } + + StubDef stub = classStubs.get(targetMethod.getMethodName()); + + if (stub == null) { + return null; + } + + if (stub.type == EMPTY_LIST && stub.parcelListType == null) { + if (stubProxyClass == null) { + Log.d(TAG, "stub proxy class not found for " + targetMethod); + return null; + } + + for (Method m : stubProxyClass.getDeclaredMethods()) { + if (stubProxyMethodName.equals(m.getName())) { + stub.parcelListType = m.getReturnType(); + break; + } + } + + if (stub.parcelListType == null) { + Log.d(TAG, "stub proxy method not found for " + targetMethod); + return null; + } + } + + return stub; + } + + public boolean stubOutMethod(Parcel p) { + p.setDataPosition(0); + p.setDataSize(0); + + final long integer = integerVal; + + switch (type) { + case VOID: + break; + case NULL: + p.writeTypedObject((Parcelable) null, 0); + break; + case NULL_STRING: + p.writeString(null); + break; + case NULL_ARRAY: + p.writeInt(-1); + break; + case EMPTY_BYTE_ARRAY: + p.writeByteArray(new byte[0]); + break; + case EMPTY_INT_ARRAY: + p.writeIntArray(new int[0]); + break; + case EMPTY_LONG_ARRAY: + p.writeLongArray(new long[0]); + break; + case EMPTY_STRING: + p.writeString(""); + break; + case EMPTY_LIST: { + Class t = parcelListType; + if (t == List.class) { + p.writeList(Collections.emptyList()); + } else { + String listTypeName = t.getName(); + // There is android.content.pm.ParceledListSlice and + // com.android.modules.utils.ParceledListSlice. + // Moreover, when the latter is used in an APEX, it's prefixed like this: + // com.android.wifi.x.com.android.modules.utils.ParceledListSlice + + // Same applies to StringParceledListSlice. + + if (listTypeName.endsWith(".ParceledListSlice")) { + p.writeTypedObject(ParceledListSlice.emptyList(), 0); + } else if (listTypeName.endsWith(".StringParceledListSlice")) { + p.writeTypedObject(StringParceledListSlice.emptyList(), 0); + } else { + Log.d(TAG, "unknown parcel list type " + listTypeName); + return false; + } + } + break; + } + case EMPTY_MAP: + p.writeMap(Collections.emptyMap()); + break; + case BOOLEAN: + p.writeBoolean(integer != 0); + break; + case BYTE: + p.writeByte((byte) integer); + break; + case INT: + p.writeInt((int) integer); + break; + case LONG: + p.writeLong(integer); + break; + case FLOAT: + p.writeFloat((float) doubleVal); + break; + case DOUBLE: + p.writeDouble(doubleVal); + break; + case STRING: + p.writeString(stringVal); + break; + case THROW: { + Throwable t; + try { + Class class_ = Class.forName(stringVal); + t = (Throwable) class_.newInstance(); + } catch (ReflectiveOperationException e) { + Log.e(TAG, "", e); + return false; + } + SneakyThrow.sneakyThrow(t); + break; + } + default: + Log.i(TAG, "unknown type " + type); + // it's fine that Parcel is reset at this point, it won't be read: + // a pending exception will be thrown when this method returns false + return false; + } + + p.setDataPosition(0); + return true; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel p, int flags) { + p.writeInt(type); + p.writeLong(integerVal); + p.writeDouble(doubleVal); + p.writeString(stringVal); + } + + public static final Parcelable.Creator CREATOR = new Creator<>() { + @Override + public StubDef createFromParcel(Parcel p) { + StubDef d = new StubDef(); + d.type = p.readInt(); + d.integerVal = p.readLong(); + d.doubleVal = p.readDouble(); + d.stringVal = p.readString(); + return d; + } + + @Override + public StubDef[] newArray(int size) { + return new StubDef[size]; + } + }; +} diff --git a/core/java/com/android/internal/gmscompat/client/GmsCompatClientService.java b/core/java/com/android/internal/gmscompat/client/GmsCompatClientService.java new file mode 100644 index 0000000000000..3948e209d0d3b --- /dev/null +++ b/core/java/com/android/internal/gmscompat/client/GmsCompatClientService.java @@ -0,0 +1,41 @@ +package com.android.internal.gmscompat.client; + +import android.app.Service; +import android.content.Intent; +import android.os.Binder; +import android.os.IBinder; +import android.util.Log; + +/** + * The sole purpose of this service is to bypass some background activity restrictions (e.g. background + * service starts). GmsCompat app is always allowed to bypass those restrictions because it has a + * power optimization exemption. GmsCompat app temporarily binds to apps that host this service when + * that's required, which bring those apps to the foreground state. + *

+ * Privileged GmsCore achieves this by sending a privileged broadcast with + * BroadcastOptions#setTemporaryAppAllowlist() option set. + *

+ * A declaration of this service is added to AndroidManifest of all GMS components that use GmsCompat + * and to their clients during package parsing. + */ +public class GmsCompatClientService extends Service { + private static final String TAG = GmsCompatClientService.class.getSimpleName(); + + private final Binder dummyBinder = new Binder(); + + @Override + public void onCreate() { + Log.d(TAG, "onCreate"); + } + + @Override + public IBinder onBind(Intent intent) { + Log.d(TAG, "onBind"); + return dummyBinder; + } + + @Override + public void onDestroy() { + Log.d(TAG, "onDestroy"); + } +} diff --git a/core/java/com/android/internal/gmscompat/dynamite/GmsDynamiteClientHooks.java b/core/java/com/android/internal/gmscompat/dynamite/GmsDynamiteClientHooks.java new file mode 100644 index 0000000000000..f87224c34e818 --- /dev/null +++ b/core/java/com/android/internal/gmscompat/dynamite/GmsDynamiteClientHooks.java @@ -0,0 +1,108 @@ +package com.android.internal.gmscompat.dynamite; + +import android.app.compat.gms.GmsCompat; +import android.content.res.ApkAssets; +import android.content.res.loader.AssetsProvider; + +import com.android.internal.gmscompat.fileservice.GmsCoreFileServerClientHooks; + +import java.io.File; +import java.io.FileDescriptor; +import java.io.IOException; +import java.util.regex.Pattern; + +import dalvik.system.DelegateLastClassLoader; + +public final class GmsDynamiteClientHooks { + static final String TAG = "GmsCompat/DynamiteClient"; + private static final boolean DEBUG = false; + + // written last in the init sequence, "volatile" to publish all the preceding writes + private static volatile boolean enabled; + + public static boolean enabled() { + return enabled; + } + + // ContentResolver#acquireProvider(Uri) + public static void maybeInit(String auth) { + if (!"com.google.android.gms.chimera".equals(auth)) { + return; + } + synchronized (GmsDynamiteClientHooks.class) { + if (enabled()) { + return; + } + if (!GmsCompat.isClientOfGmsCore()) { + return; + } + + DelegateLastClassLoader.modifyClassLoaderPathHook = GmsDynamiteClientHooks::maybeModifyClassLoaderPath; + enabled = true; + } + } + + // ApkAssets#loadFromPath(String, int, AssetsProvider) + public static ApkAssets loadAssetsFromPath(String path, int flags, AssetsProvider assets) throws IOException { + if (!GmsCoreFileServerClientHooks.isInGmsCoreDeDataDirAndShouldCacheFds(path)) { + return null; + } + FileDescriptor fd = GmsCoreFileServerClientHooks.openCachedFile(path); + if (fd == null) { + throw new IOException("unable to open " + path); + } + // no need to dup the fd, ApkAssets does it itself + return ApkAssets.loadFromFd(fd, path, flags, assets); + } + + // Replaces file paths of Dynamite modules with "/gmscompat_fd" file descriptor references + // DelegateLastClassLoader#maybeModifyClassLoaderPath(String, Boolean) + public static String maybeModifyClassLoaderPath(String path, Boolean nativeLibsPathB) { + if (path == null) { + return null; + } + if (!enabled) { // libcore code doesn't have access to this field + return path; + } + boolean nativeLibsPath = nativeLibsPathB.booleanValue(); + String[] pathParts = path.split(Pattern.quote(File.pathSeparator)); + boolean modified = false; + + for (int i = 0; i < pathParts.length; ++i) { + String pathPart = pathParts[i]; + if (!GmsCoreFileServerClientHooks.isInGmsCoreDeDataDirAndShouldCacheFds(pathPart)) { + continue; + } + // defined in bionic/linker/linker_utils.cpp kZipFileSeparator + final String zipFileSeparator = "!/"; + + String filePath; + String nativeLibRelPath; + if (nativeLibsPath) { + int idx = pathPart.indexOf(zipFileSeparator); + filePath = pathPart.substring(0, idx); + nativeLibRelPath = pathPart.substring(idx + zipFileSeparator.length()); + } else { + filePath = pathPart; + nativeLibRelPath = null; + } + FileDescriptor fd = GmsCoreFileServerClientHooks.openCachedFile(filePath); + if (fd == null) { + throw new IllegalStateException("unable to open " + filePath); + } + String fdFilePath = "/gmscompat_fd_" + fd.getInt$(); + + pathParts[i] = nativeLibsPath ? + fdFilePath + zipFileSeparator + nativeLibRelPath : + fdFilePath; + + modified = true; + } + if (!modified) { + return path; + } + return String.join(File.pathSeparator, pathParts); + } + + private GmsDynamiteClientHooks() {} +} diff --git a/core/java/com/android/internal/gmscompat/fileservice/FileProxyService.java b/core/java/com/android/internal/gmscompat/fileservice/FileProxyService.java new file mode 100644 index 0000000000000..f890702365656 --- /dev/null +++ b/core/java/com/android/internal/gmscompat/fileservice/FileProxyService.java @@ -0,0 +1,94 @@ +package com.android.internal.gmscompat.fileservice; + +import android.app.compat.gms.GmsCompat; +import android.content.Context; +import android.os.Binder; +import android.os.ParcelFileDescriptor; +import android.system.ErrnoException; +import android.system.Os; +import android.system.OsConstants; +import android.util.Log; + +import java.io.File; +import java.io.FileDescriptor; +import java.io.IOException; + +public final class FileProxyService extends IFileProxyService.Stub { + public static final String TAG = "GmsCompat/FileProxyService"; + + private final String deDataPrefix; // device-encrypted data dir + private final String ceDataPrefix; // credential-encrypted data dir + + public FileProxyService(Context context) { + try { + deDataPrefix = context.createDeviceProtectedStorageContext().getDataDir().getCanonicalPath() + "/"; + ceDataPrefix = context.getDataDir().getCanonicalPath() + "/"; + } catch (IOException e) { + throw new IllegalArgumentException(e); + } + Log.d(TAG, "allowed locations: " + deDataPrefix + " and " + ceDataPrefix); + } + + @Override + public ParcelFileDescriptor openFile(String rawPath) { + try { + String path = sanitizeFilePath(rawPath); + if (path != null) { + FileDescriptor fd = Os.open(path, OsConstants.O_RDONLY | OsConstants.O_CLOEXEC, 0); + Log.d(TAG, "Opened " + rawPath + " for caller UID " + Binder.getCallingUid()); + return new ParcelFileDescriptor(fd); + } + } catch (IOException | ErrnoException e) { + Log.d(TAG, "openFile failed for " + rawPath, e); + } catch (Throwable t) { + GmsCompat.appContext().getMainThreadHandler().post(() -> { + throw new IllegalStateException(t); + }); + } + // don't forward exceptions to the untrusted caller to minimize the information leaks + return null; + } + + private String sanitizeFilePath(String rawPath) throws IOException, ErrnoException { + // Normalize path for security checks + String path = new File(rawPath).getCanonicalPath(); + + String allowedPrefix = null; + if (path.startsWith(deDataPrefix)) { + allowedPrefix = deDataPrefix; + } else if (path.startsWith(ceDataPrefix)) { + allowedPrefix = ceDataPrefix; + } + + if (allowedPrefix == null) { + Log.d(TAG, "Path " + rawPath + " is not in an allowed location, realpath is " + path); + return null; + } + + // Make sure that all path components below chimeraRoot are world-accessible + { + // Check full path first to simplify checks of its parents + int mode = Os.stat(path).st_mode; + + boolean valid = OsConstants.S_ISREG(mode) && (mode & OsConstants.S_IROTH) != 0; + if (!valid) { + Log.d(TAG, "Path " + path + " is not a world-readable regular file"); + return null; + } + } + for (int i = allowedPrefix.length(), m = path.length(); i < m; ++i) { + if (path.charAt(i) != '/') { + continue; + } + String dirPath = path.substring(0, i); + int mode = Os.stat(dirPath).st_mode; + + boolean valid = OsConstants.S_ISDIR(mode) && (mode & OsConstants.S_IXOTH) != 0; + if (!valid) { + Log.d(TAG, "Node " + dirPath + " in path " + path + " is not a world-readable directory"); + return null; + } + } + return path; + } +} diff --git a/core/java/com/android/internal/gmscompat/fileservice/GmsCoreFileServerClientHooks.java b/core/java/com/android/internal/gmscompat/fileservice/GmsCoreFileServerClientHooks.java new file mode 100644 index 0000000000000..44ee34a96dd7b --- /dev/null +++ b/core/java/com/android/internal/gmscompat/fileservice/GmsCoreFileServerClientHooks.java @@ -0,0 +1,213 @@ +package com.android.internal.gmscompat.fileservice; + +import android.annotation.Nullable; +import android.app.compat.gms.GmsCompat; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.ext.PackageId; +import android.os.IBinder; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.util.ArrayMap; +import android.util.Log; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.gmscompat.GmsCompatApp; + +import java.io.File; +import java.io.FileDescriptor; +import java.util.ArrayList; + +import libcore.io.IoBridge; +import libcore.io.IoUtils; + +import static android.system.OsConstants.O_ACCMODE; +import static android.system.OsConstants.O_RDONLY; + +public class GmsCoreFileServerClientHooks { + private static final String TAG = "GmsCoreFileServerClientHooks"; + private static final boolean DEBUG = false; + + private static boolean isEnabled; + private static String gmsCoreDeDataPrefix; + private static String gmsCoreCeDataPrefix; + private static volatile IFileProxyService fileProxyService; + private static ArrayMap pfdCache; + private static ArrayList pastCachedFds; + + public static boolean isEnabled() { + return isEnabled; + } + + public static void init() { + Context context = GmsCompat.appContext(); + ApplicationInfo gmsCoreAppInfo; + try { + gmsCoreAppInfo = context.getPackageManager().getApplicationInfoAsUser( + PackageId.GMS_CORE_NAME, 0, context.getUserId()); + } catch (PackageManager.NameNotFoundException e) { + throw new IllegalStateException(e); + } + gmsCoreDeDataPrefix = gmsCoreAppInfo.deviceProtectedDataDir + "/"; + gmsCoreCeDataPrefix = gmsCoreAppInfo.credentialProtectedDataDir + "/"; + pfdCache = new ArrayMap<>(20); + pastCachedFds = new ArrayList<>(); + + File.lastModifiedHook = GmsCoreFileServerClientHooks::getFileLastModified; + IoBridge.openFdInterceptor = new IoBridge.OpenFdInterceptor() { + @Override + public FileDescriptor maybeInterceptOpenFd(String path, int flags) { + if (isInGmsCoreDataDir(path) && shouldProxyOpen(flags)) { + return openFreshFileDescriptor(path); + } + return null; + } + }; + isEnabled = true; + } + + public static long getFileLastModified(File file) { + final String path = file.getPath(); + + if (isInGmsCoreDataDir(path)) { + if (shouldCacheForLastModified(path)) { + FileDescriptor fd = openCachedFile(path); + return fd != null ? getFileLastModified(fd) : 0L; + } + + ParcelFileDescriptor pfd = openFreshParcelFileDescriptor(path); + if (pfd != null) { + try { + return getFileLastModified(pfd.getFileDescriptor()); + } finally { + IoUtils.closeQuietly(pfd); + } + } + } + return 0L; + } + + private static long getFileLastModified(FileDescriptor fd) { + return new File("/proc/self/fd/" + fd.getInt$()).lastModified(); + } + + public static boolean isInGmsCoreDeDataDirAndShouldCacheFds(String path) { + return path.startsWith(gmsCoreDeDataPrefix); + } + + private static boolean shouldCacheForLastModified(String path) { + // Use cache for Dynamite modules which are served as .apk files + return isInGmsCoreDeDataDirAndShouldCacheFds(path) && isApkContainerPath(path); + } + + private static boolean isApkContainerPath(String path) { + return path.endsWith(".apk"); + } + + public static boolean isInGmsCoreDataDir(String path) { + return path.startsWith(gmsCoreDeDataPrefix) || path.startsWith(gmsCoreCeDataPrefix); + } + + public static FileDescriptor openParcelFileDescriptorHook(String path, int flags) { + if (!isInGmsCoreDataDir(path) || !shouldProxyOpen(flags)) { + return null; + } + + return openFreshFileDescriptor(path); + } + + // Returned file descriptor should never be closed because it may be accessed at any time by the native code + @Nullable + public static FileDescriptor openCachedFile(String path) { + if (DEBUG) { + Log.d(TAG, "cached path " + path, new Throwable()); + } + try { + ArrayMap cache = pfdCache; + // this lock isn't contended, favor simplicity, not making the critical section shorter + synchronized (cache) { + ParcelFileDescriptor pfd = cache.get(path); + if (pfd == null) { + pfd = getFileProxyService().openFile(path); + if (pfd == null) { + return null; + } + // ParcelFileDescriptor owns the underlying file descriptor + cache.put(path, pfd); + } + return pfd.getFileDescriptor(); + } + } catch (RemoteException e) { + // FileProxyService never forwards exceptions to minimize the information leaks, + // this is a very rare "binder died" exception + throw e.rethrowAsRuntimeException(); + } + } + + private static boolean shouldProxyOpen(int flags) { + return (flags & O_ACCMODE) == O_RDONLY; + } + + @Nullable + private static FileDescriptor openFreshFileDescriptor(String path) { + ParcelFileDescriptor pfd = openFreshParcelFileDescriptor(path); + if (pfd == null) { + return null; + } + + FileDescriptor fd = new FileDescriptor(); + fd.setInt$(pfd.detachFd()); + return fd; + } + + @Nullable + private static ParcelFileDescriptor openFreshParcelFileDescriptor(String path) { + if (DEBUG) { + Log.d(TAG, "fresh path " + path, new Throwable()); + } + try { + IFileProxyService service; + synchronized (pfdCache) { + service = getFileProxyService(); + } + return service.openFile(path); + } catch (RemoteException e) { + // FileProxyService never forwards exceptions to minimize the information leaks, + // this is a very rare "binder died" exception + throw e.rethrowAsRuntimeException(); + } + } + + @GuardedBy("pfdCache") + private static IFileProxyService getFileProxyService() { + IFileProxyService cache = fileProxyService; + if (cache != null) { + return cache; + } + try { + IFileProxyService service = GmsCompatApp.iClientOfGmsCore2Gca().getGmsCoreFileProxyService(); + fileProxyService = service; + IBinder.DeathRecipient serviceDeathCallback = () -> { + fileProxyService = null; + Log.d(TAG, "FileProxyService died"); + synchronized (pfdCache) { + // It's not safe to close cached file descriptors, they might still be in use + // at this point. Simply clearing the cache would make cached ParcelFileDescriptors + // collectable by GC, which would close the underlying file descriptors via + // ParcelFileDescriptor#finalize() + // + // pastCachedFds list is effectively a file descriptor leak, but it's small and + // rare. File descriptor count limit (RLIMIT_NOFILE) is set to 32768 as of Android 15. + pastCachedFds.addAll(pfdCache.values()); + pfdCache.clear(); + } + }; + service.asBinder().linkToDeath(serviceDeathCallback, 0); + return service; + } catch (RemoteException e) { + Log.e(TAG, "unable to obtain FileProxyService", e); + throw e.rethrowAsRuntimeException(); + } + } +} diff --git a/core/java/com/android/internal/gmscompat/fileservice/IFileProxyService.aidl b/core/java/com/android/internal/gmscompat/fileservice/IFileProxyService.aidl new file mode 100644 index 0000000000000..df3622fd3a576 --- /dev/null +++ b/core/java/com/android/internal/gmscompat/fileservice/IFileProxyService.aidl @@ -0,0 +1,5 @@ +package com.android.internal.gmscompat.fileservice; + +interface IFileProxyService { + ParcelFileDescriptor openFile(String path); +} diff --git a/core/java/com/android/internal/gmscompat/flags/GmsFlag.java b/core/java/com/android/internal/gmscompat/flags/GmsFlag.java new file mode 100644 index 0000000000000..1aaaec7043509 --- /dev/null +++ b/core/java/com/android/internal/gmscompat/flags/GmsFlag.java @@ -0,0 +1,292 @@ +/* + * Copyright (C) 2022 GrapheneOS + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.android.internal.gmscompat.flags; + +import android.annotation.Nullable; +import android.app.compat.gms.GmsCompat; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.ArrayMap; +import android.util.Base64; +import android.util.Log; + +import com.android.internal.gmscompat.GmsInfo; + +import java.util.Map; + +public class GmsFlag implements Parcelable { + private static final String TAG = "GmsFlag"; + + public String name; + + public static final int TYPE_BOOL = 0; + public static final int TYPE_INT = 1; + public static final int TYPE_FLOAT = 2; + public static final int TYPE_STRING = 3; + public static final int TYPE_BYTES = 4; + public byte type; + + public static final int ACTION_SET = 0; + public static final int ACTION_APPEND = 1; // only for TYPE_STRING + public byte action; + + public boolean boolArg; + public long integerArg; + public double floatArg; + public String stringArg; + public byte[] bytesArg; + + public byte permissionCheckMode; + public static final int PERMISSION_CHECK_MODE_NONE_OF = 0; + public static final int PERMISSION_CHECK_MODE_NOT_ALL_OF = 1; + public static final int PERMISSION_CHECK_MODE_ALL_OF = 2; + public @Nullable String[] permissions; + + public static final String NAMESPACE_GSERVICES = "gservices"; + // Gservices content provider is hosted by GmsCore since Android 15. It was hosted by GSF before 15. + public static final String GSERVICES_CONTENT_PROVIDER_AUTHORITY = "com.google.android.gsf.gservices"; + + public static final String GSERVICES_URI = "content://" + + GSERVICES_CONTENT_PROVIDER_AUTHORITY + "/prefix"; + + public static final String PHENOTYPE_URI_PREFIX = "content://" + + GmsInfo.PACKAGE_GMS_CORE + ".phenotype/"; + + public GmsFlag() {} + + public GmsFlag(String name) { + this.name = name; + } + + private boolean permissionsMatch() { + String[] perms = permissions; + if (perms == null) { + return true; + } + + int numOfGrantedPermissions = 0; + + for (String perm : perms) { + if (GmsCompat.hasPermission(perm)) { + ++numOfGrantedPermissions; + } + } + + switch (permissionCheckMode) { + case PERMISSION_CHECK_MODE_NONE_OF: + return numOfGrantedPermissions == 0; + case PERMISSION_CHECK_MODE_NOT_ALL_OF: + return numOfGrantedPermissions != perms.length; + case PERMISSION_CHECK_MODE_ALL_OF: + return numOfGrantedPermissions == perms.length; + default: + return false; + } + } + + public void applyToGservicesMap(ArrayMap map) { + if (!shouldOverride()) { + return; + } + + if (type != TYPE_STRING) { + // all Gservices flags are Strings + throw new IllegalStateException(); + } + + maybeOverrideString(map); + } + + private static final int PHENOTYPE_BASE64_FLAGS = Base64.NO_PADDING | Base64.NO_WRAP; + + public void applyToPhenotypeMap(Map map) { + if (!shouldOverride()) { + return; + } + + String s; + switch (type) { + case TYPE_BOOL: + s = boolArg ? "1" : "0"; + break; + case TYPE_INT: + s = Long.toString(integerArg); + break; + case TYPE_FLOAT: + s = Double.toString(floatArg); + break; + case TYPE_STRING: + maybeOverrideString(map); + return; + case TYPE_BYTES: + s = Base64.encodeToString(bytesArg, PHENOTYPE_BASE64_FLAGS); + break; + default: + return; + } + + map.put(name, s); + } + + public String typeAsString() { + int type = this.type; + return switch (type) { + case TYPE_BOOL -> "boolean"; + case TYPE_INT -> "long"; + case TYPE_FLOAT -> "double"; + case TYPE_STRING -> "string"; + case TYPE_BYTES -> "bytes"; + default -> throw new IllegalStateException(Integer.toString(type)); + }; + } + + public String valueAsString() { + int type = this.type; + return switch (type) { + case TYPE_BOOL -> Boolean.toString(boolArg); + case TYPE_INT -> Long.toString(integerArg); + case TYPE_FLOAT -> Double.toString(floatArg); + case TYPE_STRING -> stringArg; + case TYPE_BYTES -> Base64.encodeToString(bytesArg, PHENOTYPE_BASE64_FLAGS); + default -> throw new IllegalStateException(Integer.toString(type)); + }; + } + + public boolean shouldOverride() { + if (!permissionsMatch()) { + return false; + } + + return true; + } + + // method names and types match columns in phenotype.db database, tables Flags and FlagOverrides + + public int boolVal(int orig) { + if (type != TYPE_BOOL) { + logTypeMismatch(); + return orig; + } + return boolArg ? 1 : 0; + } + + public long intVal(long orig) { + if (type != TYPE_INT) { + logTypeMismatch(); + return orig; + } + return integerArg; + } + + public double floatVal(double orig) { + if (type != TYPE_FLOAT) { + logTypeMismatch(); + return orig; + } + return floatArg; + } + + public void maybeOverrideString(Map map) { + if (type != TYPE_STRING) { + logTypeMismatch(); + return; + } + + if (action == ACTION_SET) { + map.put(name, stringArg); + return; + } + + if (action == ACTION_APPEND) { + if (!map.containsKey(name)) { + Log.d(TAG, name + " is not present in the map, skipping ACTION_APPEND"); + return; + } + + Object orig = map.get(name); + + if (!(orig instanceof String)) { + Log.w(TAG, "original value of " + name + " is not a string, skipping ACTION_APPEND. Value: " + orig); + return; + } + + map.put(name, (String) orig + stringArg); + return; + } + + Log.d(TAG, "unknown action " + action + " for " + name); + } + + public byte[] extensionVal(byte[] orig) { + if (type != TYPE_BYTES) { + logTypeMismatch(); + return orig; + } + return bytesArg; + } + + public void initAsSetString(String v) { + type = GmsFlag.TYPE_STRING; + action = GmsFlag.ACTION_SET; + stringArg = v; + } + + private void logTypeMismatch() { + Log.e(TAG, "type mismatch for key " + name, new Throwable()); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel p, int flags) { + p.writeString(name); + p.writeByte(type); + p.writeByte(permissionCheckMode); + p.writeStringArray(permissions); + p.writeByte(action); + p.writeBoolean(boolArg); + p.writeLong(integerArg); + p.writeDouble(floatArg); + p.writeString(stringArg); + p.writeByteArray(bytesArg); + } + + public static final Parcelable.Creator CREATOR = new Creator<>() { + @Override + public GmsFlag createFromParcel(Parcel p) { + GmsFlag f = new GmsFlag(); + f.name = p.readString(); + f.type = p.readByte(); + f.permissionCheckMode = p.readByte(); + f.permissions = p.readStringArray(); + f.action = p.readByte(); + f.boolArg = p.readBoolean(); + f.integerArg = p.readLong(); + f.floatArg = p.readDouble(); + f.stringArg = p.readString(); + f.bytesArg = p.createByteArray(); + return f; + } + + @Override + public GmsFlag[] newArray(int size) { + return new GmsFlag[size]; + } + }; + + public static void writeMapEntry(ArrayMap map, int idx, Parcel dst) { + // map key is GmsFlag.name, do not write it twice + map.valueAt(idx).writeToParcel(dst, 0); + } + + public static void readMapEntry(Parcel p, ArrayMap dst) { + GmsFlag f = GmsFlag.CREATOR.createFromParcel(p); + dst.append(f.name, f); + } +} diff --git a/core/java/com/android/internal/gmscompat/flags/GmsFlagOverrides.java b/core/java/com/android/internal/gmscompat/flags/GmsFlagOverrides.java new file mode 100644 index 0000000000000..2c2fa6595d1a8 --- /dev/null +++ b/core/java/com/android/internal/gmscompat/flags/GmsFlagOverrides.java @@ -0,0 +1,59 @@ +package com.android.internal.gmscompat.flags; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.ext.PackageId; +import android.util.Log; + +import com.android.internal.gmscompat.GmsCompatConfig; +import com.android.internal.gmscompat.GmsHooks; + +import java.util.Arrays; + +public class GmsFlagOverrides { + private static final String TAG = "GmsFlagOverrides"; + + public static void init(Context ctx) { + Arrays.asListHook = (Object[] arr) -> { + if (arr.length == 2 && "com.google.android.apps.internal.mobdog".equals(arr[0]) && "com.google.android.apps.mobileutilities".equals(arr[1])) { + String[] replacement = new String[] { (String) arr[0], (String) arr[1], PackageId.GMS_CORE_NAME, }; + Log.d(TAG, "Arrays.asListHook: replaced " + Arrays.toString(arr) + " with " + Arrays.toString(replacement)); + return replacement; + } + return arr; + }; + + var receiver = new BroadcastReceiver() { + private boolean receivedPhenotypeCommittedBroadcast; + + @Override + public void onReceive(Context context, Intent intent) { + if (PhenotypeFlags.ACTION_COMMITTED.equals(intent.getAction())) { + if (receivedPhenotypeCommittedBroadcast) { + return; + } + receivedPhenotypeCommittedBroadcast = true; + } + Log.d(TAG, "received " + intent); + applyOverrides(); + } + }; + // Most phenotype flags and all Gservices flags are stored on user-encrypted storage, + // i.e. they can't be updated while the device is in Direct Boot state + var filter = new IntentFilter(Intent.ACTION_USER_UNLOCKED); + // In some cases phenotype service isn't ready to accept overrides at user_unlocked time and + // at process init time. It's always ready by the time phenotype ACTION_COMMITTED is sent. + filter.addAction(PhenotypeFlags.ACTION_COMMITTED); + ctx.registerReceiver(receiver, filter, Context.RECEIVER_NOT_EXPORTED); + + applyOverrides(); + } + + public static void applyOverrides() { + GmsCompatConfig config = GmsHooks.config(); + GservicesFlags.applyOverrides(config); + PhenotypeFlags.applyOverrides(config); + } +} diff --git a/core/java/com/android/internal/gmscompat/flags/GservicesFlags.java b/core/java/com/android/internal/gmscompat/flags/GservicesFlags.java new file mode 100644 index 0000000000000..e5583d07eb86c --- /dev/null +++ b/core/java/com/android/internal/gmscompat/flags/GservicesFlags.java @@ -0,0 +1,34 @@ +package com.android.internal.gmscompat.flags; + +import android.app.compat.gms.GmsCompat; +import android.content.Intent; +import android.ext.PackageId; +import android.util.ArrayMap; +import android.util.Log; + +import com.android.internal.gmscompat.GmsCompatConfig; + +public class GservicesFlags { + private static final String TAG = "GmcGservicesFlags"; + + public static void applyOverrides(GmsCompatConfig config) { + ArrayMap gservicesFlags = config.gservicesFlags; + if (gservicesFlags == null) { + return; + } + + var overriddenFlags = new ArrayMap(gservicesFlags.size()); + for (int i = 0; i < gservicesFlags.size(); ++i) { + GmsFlag flag = gservicesFlags.valueAt(i); + flag.applyToGservicesMap(overriddenFlags); + } + + if (!overriddenFlags.isEmpty()) { + var intent = new Intent("com.google.gservices.intent.action.GSERVICES_OVERRIDE"); + intent.setPackage(PackageId.GMS_CORE_NAME); + overriddenFlags.forEach(intent::putExtra); + Log.d(TAG, "sending GSERVICES_OVERRIDE broadcast, extras: " + intent.getExtras().toStringDeep()); + GmsCompat.appContext().sendBroadcast(intent); + } + } +} diff --git a/core/java/com/android/internal/gmscompat/flags/PhenotypeFlags.java b/core/java/com/android/internal/gmscompat/flags/PhenotypeFlags.java new file mode 100644 index 0000000000000..bdfe62dbfa0e8 --- /dev/null +++ b/core/java/com/android/internal/gmscompat/flags/PhenotypeFlags.java @@ -0,0 +1,54 @@ +package com.android.internal.gmscompat.flags; + +import android.app.compat.gms.GmsCompat; +import android.content.Intent; +import android.ext.PackageId; +import android.util.ArrayMap; +import android.util.Log; + +import com.android.internal.gmscompat.GmsCompatConfig; + +import java.util.ArrayList; +import java.util.Collection; + +public class PhenotypeFlags { + private static final String TAG = "GmcPhenotypeFlags"; + + public static final String ACTION_COMMITTED = "com.google.android.gms.phenotype.COMMITTED"; + + public static void applyOverrides(GmsCompatConfig config) { + ArrayMap> packageFlagMap = config.flags; + for (int packageIdx = 0; packageIdx < packageFlagMap.size(); ++packageIdx) { + Collection configFlags = packageFlagMap.valueAt(packageIdx).values(); + var overridenFlags = new ArrayList(configFlags.size()); + for (GmsFlag flag : configFlags) { + if (flag.shouldOverride()) { + overridenFlags.add(flag); + } + } + int numFlags = overridenFlags.size(); + String[] flagNames = new String[numFlags]; + String[] flagValues = new String[numFlags]; + String[] flagTypes = new String[numFlags]; + for (int i = 0; i < numFlags; ++i) { + GmsFlag flag = overridenFlags.get(i); + flagNames[i] = flag.name; + flagValues[i] = flag.valueAsString(); + flagTypes[i] = flag.typeAsString(); + } + + String flagPackageName = packageFlagMap.keyAt(packageIdx); + + var intent = new Intent("com.google.android.gms.phenotype.FLAG_OVERRIDE"); + intent.setPackage(PackageId.GMS_CORE_NAME); + intent.putExtra("package", flagPackageName); + intent.putExtra("user", "*"); + intent.putExtra("flags", flagNames); + intent.putExtra("values", flagValues); + intent.putExtra("types", flagTypes); + Log.d(TAG, "sending FLAG_OVERRIDE broadcast for flagPackage " + flagPackageName + + ", extras: " + intent.getExtras().toStringDeep()); + GmsCompat.appContext().sendBroadcast(intent); + } + } +} diff --git a/core/java/com/android/internal/gmscompat/gcarriersettings/GCarrierSettingsApp.java b/core/java/com/android/internal/gmscompat/gcarriersettings/GCarrierSettingsApp.java new file mode 100644 index 0000000000000..663c93af576e2 --- /dev/null +++ b/core/java/com/android/internal/gmscompat/gcarriersettings/GCarrierSettingsApp.java @@ -0,0 +1,76 @@ +package com.android.internal.gmscompat.gcarriersettings; + +import android.content.Context; +import android.service.carrier.CarrierIdentifier; +import android.telephony.TelephonyManager; + +import java.util.Objects; + +// A set of hooks that are needed to obtain output of Google's CarrierSettings app for arbitrary +// CarrierIds. That output is used for testing CarrierConfig2 app. +public class GCarrierSettingsApp { + public static final String PKG_NAME = "com.google.android.carrier"; + + public static final int PHONE_SLOT_IDX_FOR_CARRIER_ID_OVERRIDE = 50; + public static final int SUB_ID_FOR_CARRIER_ID_OVERRIDE = 90; + // a separate value is need to prevent caching inside GCarrierSettings from interfering with + // the results + public static final int SUB_ID_FOR_CARRIER_SERVICE_CALL = 91; + + private static int isTestingModeEnabled; + + static ThreadLocal carrierIdOverride; + + public static void init() { + carrierIdOverride = new ThreadLocal<>(); + } + + public static int maybeOverrideSlotIndex(int subId) { + if (subId == SUB_ID_FOR_CARRIER_ID_OVERRIDE) { + return PHONE_SLOT_IDX_FOR_CARRIER_ID_OVERRIDE; + } + return -1; + } + + public static int[] maybeOverrideSubIds(int slotIndex) { + if (slotIndex == PHONE_SLOT_IDX_FOR_CARRIER_ID_OVERRIDE) { + return new int[] { SUB_ID_FOR_CARRIER_ID_OVERRIDE }; + } + return null; + } + + public static TelephonyManager maybeOverrideCreateTelephonyManager(Context ctx, int subId) { + if (subId == SUB_ID_FOR_CARRIER_ID_OVERRIDE) { + CarrierIdentifier override = carrierIdOverride.get(); + Objects.requireNonNull(override); + return new GCSTelephonyManager(ctx, SUB_ID_FOR_CARRIER_ID_OVERRIDE, override); + } + + return null; + } + + public static class GCSTelephonyManager extends TelephonyManager { + private final CarrierIdentifier carrierIdOverride; + + public GCSTelephonyManager(Context context, int subId, CarrierIdentifier carrierIdOverride) { + super(context, subId); + this.carrierIdOverride = carrierIdOverride; + } + + @Override public String getSimOperator() { + return carrierIdOverride.getMcc() + carrierIdOverride.getMnc(); + } + + @Override public String getSimOperatorName() { + return carrierIdOverride.getSpn(); + } + + @Override public String getSubscriberId() { + return carrierIdOverride.getImsi(); + } + + @Override public String getGroupIdLevel1() { + return carrierIdOverride.getGid1(); + } + } +} diff --git a/core/java/com/android/internal/gmscompat/gcarriersettings/ICarrierConfigsLoader.aidl b/core/java/com/android/internal/gmscompat/gcarriersettings/ICarrierConfigsLoader.aidl new file mode 100644 index 0000000000000..c28bde2351c5c --- /dev/null +++ b/core/java/com/android/internal/gmscompat/gcarriersettings/ICarrierConfigsLoader.aidl @@ -0,0 +1,8 @@ +package com.android.internal.gmscompat.gcarriersettings; + +import android.os.Bundle; +import android.service.carrier.CarrierIdentifier; + +interface ICarrierConfigsLoader { + Bundle getConfigs(in CarrierIdentifier carrierId); +} diff --git a/core/java/com/android/internal/gmscompat/gcarriersettings/TestCarrierConfigService.java b/core/java/com/android/internal/gmscompat/gcarriersettings/TestCarrierConfigService.java new file mode 100644 index 0000000000000..7b4431232a70b --- /dev/null +++ b/core/java/com/android/internal/gmscompat/gcarriersettings/TestCarrierConfigService.java @@ -0,0 +1,160 @@ +package com.android.internal.gmscompat.gcarriersettings; + +import android.annotation.Nullable; +import android.app.Service; +import android.content.ComponentName; +import android.content.ContentValues; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Binder; +import android.os.Bundle; +import android.os.IBinder; +import android.os.PersistableBundle; +import android.os.RemoteException; +import android.os.ResultReceiver; +import android.service.carrier.CarrierIdentifier; +import android.service.carrier.CarrierService; +import android.service.carrier.CarrierService.ICarrierServiceWrapper; +import android.service.carrier.IApnSourceService; +import android.service.carrier.ICarrierService; +import android.util.Log; + +import com.android.internal.util.Preconditions; + +import java.util.ArrayList; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +// This service is only used for testing the CarrierConfig2 app. +// It allows CarrierConfig2 to get carrier configs from Google's CarrierSettings app for arbitrary +// CarrierIds. +public class TestCarrierConfigService extends Service { + + private final Executor bgExecutor = Executors.newSingleThreadExecutor(); + + private Future carrierServiceF; + private Future apnServiceF; + + private final ArrayList serviceConnections = new ArrayList<>(); + + public static final String KEY_CARRIER_SERVICE_RESULT = "carrier_service_result"; + public static final String KEY_APN_SERVICE_RESULT = "apn_service_result"; + + private final Binder binder = new ICarrierConfigsLoader.Stub() { + private ICarrierService carrierService; + private IApnSourceService apnService; + + private void maybeWaitForServices() { + synchronized (this) { + if (carrierService == null) { + try { + carrierService = ICarrierService.Stub.asInterface(carrierServiceF.get()); + apnService = IApnSourceService.Stub.asInterface(apnServiceF.get()); + } catch (ExecutionException|InterruptedException e) { + throw new IllegalStateException(e); + } + } + } + } + + @Override + public Bundle getConfigs(CarrierIdentifier carrierId) throws RemoteException { + maybeWaitForServices(); + + var carrierServiceResultF = new CompletableFuture(); + var resultReceiver = new ResultReceiver(null) { + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + Preconditions.checkArgument(resultCode == ICarrierServiceWrapper.RESULT_OK); + carrierServiceResultF.complete(resultData); + } + }; + carrierService.getCarrierConfig(GCarrierSettingsApp.SUB_ID_FOR_CARRIER_SERVICE_CALL, carrierId, resultReceiver); + + GCarrierSettingsApp.carrierIdOverride.set(carrierId); + ContentValues[] apns = apnService.getApns(GCarrierSettingsApp.SUB_ID_FOR_CARRIER_ID_OVERRIDE); + + var result = new Bundle(); + result.putParcelableArray(KEY_APN_SERVICE_RESULT, apns); + + try { + Bundle b = carrierServiceResultF.get(); + PersistableBundle pb = b.getParcelable(ICarrierServiceWrapper.KEY_CONFIG_BUNDLE); + result.putParcelable(KEY_CARRIER_SERVICE_RESULT, pb); + } catch (InterruptedException|ExecutionException e) { + throw new IllegalStateException(e); + } + + return result; + } + }; + + @Override + public void onCreate() { + var csIntent = new Intent(CarrierService.CARRIER_SERVICE_INTERFACE); + csIntent.setPackage(GCarrierSettingsApp.PKG_NAME); + carrierServiceF = bind(csIntent); + + var apnServiceIntent = new Intent(); + apnServiceIntent.setComponent(ComponentName.createRelative(GCarrierSettingsApp.PKG_NAME, ".ApnSourceService")); + apnServiceF = bind(apnServiceIntent); + } + + @Nullable + @Override + public IBinder onBind(Intent intent) { + return binder; + } + + CompletableFuture bind(Intent intent) { + String TAG = "TestCConfigService.bind"; + + var future = new CompletableFuture(); + + var sc = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + Log.d(TAG, "onServiceConnected " + name); + future.complete(service); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + Log.d(TAG, "onServiceDisconnected " + name); + } + + @Override + public void onBindingDied(ComponentName name) { + throw new IllegalStateException("onBindingDied " + name); + } + + @Override + public void onNullBinding(ComponentName name) { + throw new IllegalStateException("onNullBinding " + name); + } + }; + + int bindFlags = Context.BIND_AUTO_CREATE | Context.BIND_ABOVE_CLIENT; + boolean res = bindService(intent, bindFlags, bgExecutor, sc); + serviceConnections.add(sc); + + if (!res) { + throw new IllegalStateException("unable to bind to " + intent); + } + + return future; + } + + @Override + public void onDestroy() { + super.onDestroy(); + + for (ServiceConnection c : serviceConnections) { + unbindService(c); + } + } +} diff --git a/core/java/com/android/internal/gmscompat/sysservice/GmcPackageManager.java b/core/java/com/android/internal/gmscompat/sysservice/GmcPackageManager.java new file mode 100644 index 0000000000000..3fe16a1a21a4e --- /dev/null +++ b/core/java/com/android/internal/gmscompat/sysservice/GmcPackageManager.java @@ -0,0 +1,702 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.gmscompat.sysservice; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.app.ActivityThread; +import android.app.Application; +import android.app.ApplicationPackageManager; +import android.app.compat.gms.GmsCompat; +import android.content.ComponentName; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.IPackageDataObserver; +import android.content.pm.IPackageDeleteObserver; +import android.content.pm.IPackageInstaller; +import android.content.pm.IPackageManager; +import android.content.pm.InstallSourceInfo; +import android.content.pm.PackageInfo; +import android.content.pm.PackageInstaller; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.content.pm.SharedLibraryInfo; +import android.content.pm.VersionedPackage; +import android.ext.PackageId; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.Process; +import android.os.RemoteException; +import android.os.UserHandle; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.Log; +import android.util.PackageUtils; + +import com.android.internal.gmscompat.GmsHooks; +import com.android.internal.gmscompat.GmsInfo; +import com.android.internal.gmscompat.PlayStoreHooks; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +@SuppressLint("WrongConstant") // lint doesn't like "flags & ~" expressions +public class GmcPackageManager extends ApplicationPackageManager { + private static final String TAG = GmcPackageManager.class.getSimpleName(); + + public GmcPackageManager(Context context, IPackageManager pm) { + super(context, pm); + } + + public static void init(Context ctx) { + initPseudoDisabledPackages(); + initForceDisabledComponents(ctx); + if (GmsCompat.isPlayStore()) { + ArraySet hiddenPkgs = HIDDEN_PACKAGES; + + if (Application.getProcessName().equals(PackageId.PLAY_STORE_NAME)) { + // PackageInstaller.abandonSession() calls are conditionally stubbed out to prevent + // Play Store from destroying sessions that are waiting for confirmation from the + // user. + // + // To avoid having too many pending sessions, clean them up when the main Play Store + // process starts up, before any of its code is executed. + PackageInstaller installerWrapper = ctx.getPackageManager().getPackageInstaller(); + IPackageInstaller installer = installerWrapper.getIPackageInstaller(); + + for (PackageInstaller.SessionInfo si : installerWrapper.getAllSessions()) { + try { + // PackageInstaller.abandonSession() is conditionally stubbed out, call + // the binder method directly + installer.abandonSession(si.sessionId); + } catch (RemoteException | SecurityException e) { + // confusingly, SecurityException is thrown when session is already racily + // abandoned + Log.e(TAG, "", e); + } + Log.d(TAG, "abandoned session " + si.sessionId); + } + } + } + } + + public static void maybeAdjustPackageInfo(PackageInfo pi) { + ApplicationInfo ai = pi.applicationInfo; + if (ai != null) { + maybeAdjustApplicationInfo(ai); + } + } + + public static void maybeAdjustApplicationInfo(ApplicationInfo ai) { + String packageName = ai.packageName; + + if (GmsInfo.PACKAGE_GMS_CORE.equals(packageName)) { + // Checked before accessing com.google.android.gms.phenotype content provider + // in com.google.android.libraries.phenotype.client + // .PhenotypeClientHelper#validateContentProvider() -> isGmsCorePreinstalled() + // PhenotypeFlags will always return their default values if these flags aren't set. + // + // Also need to be set to allow updates of GmsCore through Play Store without a + // logged-in Google account + if (GmsCompat.isGmsCore() || GmsCompat.isClientOfGmsCore(ai)) { + ai.flags |= ApplicationInfo.FLAG_SYSTEM | ApplicationInfo.FLAG_UPDATED_SYSTEM_APP; + } + } + + if (!ai.enabled) { + if (shouldHideDisabledState(packageName)) { + ai.enabled = true; + } + } + } + + @Override + public void deletePackage(String packageName, IPackageDeleteObserver observer, int flags) { + if (GmsCompat.isPlayStore()) { + PlayStoreHooks.deletePackage(this, packageName, observer, flags); + return; + } + + super.deletePackage(packageName, observer, flags); + } + + @Override + public void freeStorageAndNotify(String volumeUuid, long idealStorageSize, IPackageDataObserver observer) { + if (GmsCompat.isPlayStore()) { + PlayStoreHooks.freeStorageAndNotify(volumeUuid, idealStorageSize, observer); + return; + } + + super.freeStorageAndNotify(volumeUuid, idealStorageSize, observer); + } + + @Override + public void setApplicationEnabledSetting(String packageName, int newState, int flags) { + if (GmsCompat.isPlayStore()) { + if (isPseudoDisabledPackage(packageName)) { + try { + // check whether this package is actually absent + super.getApplicationInfoAsUser(packageName, ApplicationInfoFlags.of(0L), getUserId()); + } catch (NameNotFoundException e) { + // package state tracking happens in the same process that tries to enable + // the package, no need to sync this across all processes, at least for now + GmsCompat.appContext().getMainThreadHandler().post(() -> { + PlayStoreHooks.InternalBroadcastReceiver.removePseudoDisabledPackage(GmsCompat.appContext(), packageName); + PlayStoreHooks.updatePackageState(packageName, Intent.ACTION_PACKAGE_CHANGED, Intent.ACTION_PACKAGE_REMOVED); + }); + return; + } + } + PlayStoreHooks.setApplicationEnabledSetting(packageName, newState); + return; + } + + try { + super.setApplicationEnabledSetting(packageName, newState, flags); + } catch (SecurityException e) { + Log.d(TAG, "", e); + } + } + + @Override + public boolean hasSystemFeature(String name) { + switch (name) { + // checked before accessing privileged UwbManager + case "android.hardware.uwb": + return false; + } + + return super.hasSystemFeature(name); + } + + // requires privileged OBSERVE_GRANT_REVOKE_PERMISSIONS permission + @Override + public void addOnPermissionsChangeListener(OnPermissionsChangedListener listener) { + synchronized (onPermissionsChangedListeners) { + onPermissionsChangedListeners.add(listener); + } + } + + @Override + public void removeOnPermissionsChangeListener(OnPermissionsChangedListener listener) { + synchronized (onPermissionsChangedListeners) { + onPermissionsChangedListeners.remove(listener); + } + } + + public static void notifyPermissionsChangeListeners() { + Log.d("GmcPackageManager", "notifyPermissionsChangeListeners"); + int myUid = Process.myUid(); + synchronized (onPermissionsChangedListeners) { + for (OnPermissionsChangedListener l : onPermissionsChangedListeners) { + l.onPermissionsChanged(myUid); + } + } + } + + private static final ArrayList onPermissionsChangedListeners = + new ArrayList<>(); + + // MATCH_ANY_USER flag requires privileged INTERACT_ACROSS_USERS permission + + private static PackageInfoFlags filterFlags(PackageInfoFlags flags) { + long v = flags.getValue(); + + if ((v & MATCH_ANY_USER) != 0) { + return PackageInfoFlags.of(v & ~MATCH_ANY_USER); + } + + return flags; + } + + @Override + public @NonNull List getSharedLibraries(PackageInfoFlags flags) { + return super.getSharedLibraries(filterFlags(flags)); + } + + private static final ArraySet HIDDEN_PACKAGES = new ArraySet<>(new String[] { + "app.attestation.auditor", + }); + + private static void throwIfHidden(String pkgName) throws NameNotFoundException { + if (HIDDEN_PACKAGES.contains(pkgName)) { + throw new NameNotFoundException(); + } + } + + @Override + public PackageInfo getPackageInfo(VersionedPackage versionedPackage, PackageInfoFlags flags) throws NameNotFoundException { + throwIfHidden(versionedPackage.getPackageName()); + flags = filterFlags(flags); + try { + PackageInfo pi = super.getPackageInfo(versionedPackage, flags); + maybeAdjustPackageInfo(pi); + return pi; + } catch (NameNotFoundException e) { + return makePseudoDisabledPackageInfoOrThrow(versionedPackage.getPackageName(), flags); + } + } + + @Override + public PackageInfo getPackageInfoAsUser(String packageName, PackageInfoFlags flags, int userId) throws NameNotFoundException { + throwIfHidden(packageName); + flags = filterFlags(flags); + try { + PackageInfo pi = super.getPackageInfoAsUser(packageName, flags, userId); + maybeAdjustPackageInfo(pi); + return pi; + } catch (NameNotFoundException e) { + return makePseudoDisabledPackageInfoOrThrow(packageName, flags); + } + } + + @Override + public ApplicationInfo getApplicationInfoAsUser(String packageName, ApplicationInfoFlags flags, int userId) throws NameNotFoundException { + try { + ApplicationInfo ai = super.getApplicationInfoAsUser(packageName, flags, userId); + maybeAdjustApplicationInfo(ai); + return ai; + } catch (NameNotFoundException e) { + return makePseudoDisabledApplicationInfoOrThrow(packageName, flags); + } + } + + @Override + public List getInstalledApplicationsAsUser(ApplicationInfoFlags flags, int userId) { + List ret = super.getInstalledApplicationsAsUser(flags, userId); + List res = new ArrayList<>(ret.size()); + + ArraySet pseudoDisabledPackages = clonePseudoDisabledPackages(); + + for (ApplicationInfo ai : ret) { + String pkgName = ai.packageName; + if (HIDDEN_PACKAGES.contains(pkgName)) { + continue; + } + pseudoDisabledPackages.remove(pkgName); + maybeAdjustApplicationInfo(ai); + res.add(ai); + } + + for (String pkg : pseudoDisabledPackages) { + ApplicationInfo ai = maybeMakePseudoDisabledApplicationInfo(pkg, flags); + if (ai != null) { + res.add(ai); + } + } + + return res; + } + + @Override + public List getInstalledPackagesAsUser(PackageInfoFlags flags, int userId) { + flags = filterFlags(flags); + List ret = super.getInstalledPackagesAsUser(flags, userId); + List res = new ArrayList<>(ret.size()); + + ArraySet pseudoDisabledPackages = clonePseudoDisabledPackages(); + + for (PackageInfo pi : ret) { + String pkgName = pi.packageName; + if (HIDDEN_PACKAGES.contains(pkgName)) { + continue; + } + pseudoDisabledPackages.remove(pkgName); + maybeAdjustPackageInfo(pi); + res.add(pi); + } + + for (String pkg : pseudoDisabledPackages) { + PackageInfo pi = maybeMakePseudoDisabledPackageInfo(pkg, flags); + if (pi != null) { + res.add(pi); + } + } + + return res; + } + + @Override + public String[] getPackagesForUid(int uid) { + int userId = UserHandle.getUserId(uid); + int myUserId = UserHandle.myUserId(); + + if (userId != myUserId) { + if (userId != 0) { + throw new IllegalArgumentException("uid from unexpected userId: " + uid); + } + // querying uids from other userIds requires a privileged permission + uid = UserHandle.getUid(myUserId, UserHandle.getAppId(uid)); + } + + return super.getPackagesForUid(uid); + } + + @SuppressLint("SwitchIntDef") + @Override + public int getApplicationEnabledSetting(String packageName) { + try { + int res = super.getApplicationEnabledSetting(packageName); + + switch (res) { + case COMPONENT_ENABLED_STATE_DISABLED: + case COMPONENT_ENABLED_STATE_DISABLED_USER: + case COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED: + if (shouldHideDisabledState(packageName)) { + res = COMPONENT_ENABLED_STATE_DEFAULT; + } + } + + return res; + } catch (Exception e) { + if (isPseudoDisabledPackage(packageName)) { + return COMPONENT_ENABLED_STATE_DISABLED_USER; + } + throw e; + } + } + + @Override + public String getInstallerPackageName(String packageName) { + try { + return super.getInstallerPackageName(packageName); + } catch (Exception e) { + if (isPseudoDisabledPackage(packageName)) { + return PackageId.PLAY_STORE_NAME; + } + throw e; + } + } + + @NonNull + @Override + public InstallSourceInfo getInstallSourceInfo(String packageName) throws NameNotFoundException { + InstallSourceInfo res; + try { + res = super.getInstallSourceInfo(packageName); + } catch (NameNotFoundException e) { + if (isPseudoDisabledPackage(packageName)) { + String installer = PackageId.PLAY_STORE_NAME; + var isi = new InstallSourceInfo(installer, null, null, installer); + return isi; + } + throw e; + } + + switch (packageName) { + case PackageId.ANDROID_AUTO_NAME: + case PackageId.PIXEL_HEALTH_NAME: + case PackageId.GMS_CORE_NAME: + // These packages need to be exempted from updates via Play Store to prevent breaking the + // compatibility layer. + // + // Play Store respects the value of InstallSourceInfo#getUpdateOwnerPackageName(): + // packages that have non-Play Store update owners are not updated by Play Store + ContentResolver cr = GmsCompat.appContext().getContentResolver(); + String updateOwnerPackage = PlayStoreHooks.isInstallAllowed(packageName, cr) ? + // Play Store tries to use installer session preapporaval when update ownership is + // set and Play Store is not the update owner. Installer session preapproval is + // disabled on GrapheneOS, which leads to installation failure. As a workaround, + // unconditionally return to Play Store that it's already the update owner. OS + // will handle update ownership change confirmation itself. + PackageId.PLAY_STORE_NAME : + PackageUtils.getFirstPartyAppSourcePackageName(GmsCompat.appContext()); + res = new InstallSourceInfo( + res.getInitiatingPackageName(), + res.getInitiatingPackageSigningInfo(), + res.getOriginatingPackageName(), + res.getInstallingPackageName(), + updateOwnerPackage, + res.getPackageSource() + ); + break; + } + + return res; + } + + private PackageInfo makePseudoDisabledPackageInfoOrThrow(String pkgName, PackageInfoFlags flags) throws NameNotFoundException { + if (!isPseudoDisabledPackage(pkgName)) { + throw new NameNotFoundException(); + } + PackageInfo pi = maybeMakePseudoDisabledPackageInfo(pkgName, flags); + if (pi == null) { + throw new NameNotFoundException(); + } + return pi; + } + + private ApplicationInfo makePseudoDisabledApplicationInfoOrThrow(String pkgName, ApplicationInfoFlags flags) throws NameNotFoundException { + if (!isPseudoDisabledPackage(pkgName)) { + throw new NameNotFoundException(); + } + ApplicationInfo ai = maybeMakePseudoDisabledApplicationInfo(pkgName, flags); + if (ai == null) { + throw new NameNotFoundException(); + } + return ai; + } + + @Nullable + private PackageInfo maybeMakePseudoDisabledPackageInfo(String pkgName, PackageInfoFlags flags) { + PackageInfo pi; + try { + pi = super.getPackageInfoAsUser(selfPkgName(), flags, getUserId()); + } catch (NameNotFoundException e) { + return null; + } + pi = cloneViaParcel(pi, PackageInfo.CREATOR); + pi.packageName = pkgName; + writePseudoDisabledAppInfo(pkgName, pi.applicationInfo); + pi.setLongVersionCode(Integer.MAX_VALUE); + return pi; + } + + @Nullable + private ApplicationInfo maybeMakePseudoDisabledApplicationInfo(String pkgName, ApplicationInfoFlags flags) { + ApplicationInfo ai; + try { + ai = super.getApplicationInfoAsUser(selfPkgName(), flags, getUserId()); + } catch (NameNotFoundException e) { + return null; + } + ai = cloneViaParcel(ai, ApplicationInfo.CREATOR); + writePseudoDisabledAppInfo(pkgName, ai); + return ai; + } + + private static T cloneViaParcel(T obj, Parcelable.Creator creator) { + Parcel p = Parcel.obtain(); + obj.writeToParcel(p, 0); + p.setDataPosition(0); + T res = creator.createFromParcel(p); + p.recycle(); + return res; + } + + private static void writePseudoDisabledAppInfo(String pkgName, ApplicationInfo ai) { + ai.packageName = pkgName; + ai.enabled = false; + ai.longVersionCode = Integer.MAX_VALUE; + ai.versionCode = Integer.MAX_VALUE; + } + + private static String selfPkgName() { + return GmsCompat.appContext().getPackageName(); + } + + // Pseudo-disabled PackageInfo/ApplicationInfo is used to prevent Play Store from auto-installing + // optional packages, such as "Play Services for AR". It's returned only when the package is + // not installed. + // When Play Store tries to enable a pseudo-disabled package, it receives a callback that + // the package was uninstalled. This allows the user to install a pseudo-disabled package + // by pressing the "Enable" button, which reveals the "Install" button. + + // important to have it static: there are multiple instances of enclosing class in the same process + private static final ArraySet pseudoDisabledPackages = new ArraySet<>(); + + private static void initPseudoDisabledPackages() { + if (GmsCompat.isPlayStore()) { + // "Play Services for AR" + pseudoDisabledPackages.add("com.google.ar.core"); + // "Device configuration" + pseudoDisabledPackages.add("android.autoinstalls.config.google.nexus"); + } + + if (GmsCompat.isAndroidAuto()) { + pseudoDisabledPackages.add(PackageId.G_SEARCH_APP_NAME); + pseudoDisabledPackages.add("com.google.android.apps.maps"); + pseudoDisabledPackages.add("com.google.android.tts"); + } + } + + private static boolean isPseudoDisabledPackage(String pkgName) { + synchronized (pseudoDisabledPackages) { + return pseudoDisabledPackages.contains(pkgName); + } + } + + private static ArraySet clonePseudoDisabledPackages() { + synchronized (pseudoDisabledPackages) { + return new ArraySet<>(pseudoDisabledPackages); + } + } + + public static boolean removePseudoDisabledPackage(String pkgName) { + synchronized (pseudoDisabledPackages) { + return pseudoDisabledPackages.remove(pkgName); + } + } + + private static boolean shouldHideDisabledState(String pkgName) { + if (!GmsCompat.isPlayStore()) { + return false; + } + + switch (pkgName) { + case GmsInfo.PACKAGE_GMS_CORE: + return false; + default: + return true; + } + } + + private static ArraySet componentsWithForcedEnabledSetting; + + private static void initForceDisabledComponents(Context ctx) { + final String pkgName = ctx.getPackageName(); + ArrayMap forcedCes = GmsHooks.config().forceComponentEnabledSettingsMap.get(pkgName); + + if (forcedCes == null) { + return; + } + + final int cnt = forcedCes.size(); + + var components = new ArraySet(cnt); + var settings = new ArrayList(cnt); + for (int i = 0; i < cnt; ++i) { + var name = new ComponentName(ctx, forcedCes.keyAt(i)); + components.add(name); + int state = forcedCes.valueAt(i).intValue(); + var ces = new ComponentEnabledSetting(name, state, DONT_KILL_APP | SKIP_IF_MISSING); + settings.add(ces); + } + + componentsWithForcedEnabledSetting = components; + + // Don't repeat setComponentEnabledSettings() in all processes + boolean shouldUpdate; + if (GmsCompat.isGmsCore()) { + shouldUpdate = GmsHooks.inPersistentGmsCoreProcess; + } else if (GmsCompat.isPlayStore()) { + shouldUpdate = GmsInfo.PACKAGE_PLAY_STORE.equals(Application.getProcessName()); + } else { + shouldUpdate = true; + } + + if (shouldUpdate) { + try { + ActivityThread.getPackageManager().setComponentEnabledSettings(settings, ctx.getUserId(), pkgName); + } catch (Exception e) { + Log.d(TAG, "", e); + } + } + } + + private static boolean isSetComponentEnabledSettingAllowed(@Nullable ComponentName cn, int newState, int flags) { + if (cn == null) { + return true; + } + + ArraySet set = componentsWithForcedEnabledSetting; + if (set != null && set.contains(cn)) { + Log.d(TAG, "skipped setComponentEnabledSetting for " + cn + ", newState " + newState + + ", flags " + flags); + return false; + } + + return true; + } + + @Override + public void setComponentEnabledSetting(ComponentName componentName, + int newState, int flags) { + if (!isSetComponentEnabledSettingAllowed(componentName, newState, flags)) { + return; + } + + try { + super.setComponentEnabledSetting(componentName, newState, flags); + } catch (SecurityException e) { + Log.d(TAG, "", e); + } + } + + @Override + public void setComponentEnabledSettings(List settings) { + settings = settings.stream() + .filter(s -> isSetComponentEnabledSettingAllowed(s.getComponentName(), + s.getEnabledState(), s.getEnabledFlags())) + .collect(Collectors.toUnmodifiableList()); + if (settings.isEmpty()) { + return; + } + + try { + super.setComponentEnabledSettings(settings); + } catch (SecurityException e) { + Log.d(TAG, "", e); + } + } + + /** @see android.app.ContextImpl#createPackageContext */ + @Nullable + public static Context maybeOverrideGsfPackageContext(String packageName) { + if (!GmsCompat.isGmsCore()) { + return null; + } + + if (!PackageId.GSF_NAME.equals(packageName)) { + return null; + } + + // On first launch, GmsCore attempts to migrate GSF databases into itself. GSF is a + // hasCode=false package since Android 15 and is not needed for fresh installs of GmsCore. + // If GSF is absent, GmsCore crashes when it tries to create package context for GSF as + // part of database migration. To prevent this crash, return GmsCore app context instead + // of GSF package context, which turns database migration into a no-op. + + Context ctx = GmsCompat.appContext(); + PackageManager pkgManager = ctx.getPackageManager(); + + try { + pkgManager.getApplicationInfo(packageName, 0); + } catch (PackageManager.NameNotFoundException e) { + Log.d(TAG, "replacing GSF package context with GmsCore app context", new Throwable()); + return ctx; + } + + try { + PackageInfo pi = pkgManager.getPackageInfo(PackageId.GMS_CORE_NAME, 0); + if (pi.sharedUserId == null) { + // GmsCore has left the GSF sharedUid but GSF is still present + Log.d(TAG, "maybeReplaceGsfPackageName: sharedUserId is null, ignoring GSF", new Throwable()); + return ctx; + } + return null; + } catch (NameNotFoundException e) { + throw new IllegalStateException(e); + } + } + + @NonNull + @Override + public List queryBroadcastReceiversAsUser(@NonNull Intent intent, @NonNull ResolveInfoFlags flags, @NonNull UserHandle userHandle) { + if ("android.autoinstalls.config.action.PLAY_AUTO_INSTALL".equals(intent.getAction())) { + // Play Store reads list of apps to auto-install from APK that matches this filter + return Collections.emptyList(); + } + + return super.queryBroadcastReceiversAsUser(intent, flags, userHandle); + } +} diff --git a/core/java/com/android/internal/gmscompat/sysservice/GmcTelephonyManager.java b/core/java/com/android/internal/gmscompat/sysservice/GmcTelephonyManager.java new file mode 100644 index 0000000000000..0fc7d81ca810e --- /dev/null +++ b/core/java/com/android/internal/gmscompat/sysservice/GmcTelephonyManager.java @@ -0,0 +1,152 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.gmscompat.sysservice; + +import android.Manifest; +import android.annotation.CallbackExecutor; +import android.annotation.Nullable; +import android.app.compat.gms.GmsCompat; +import android.content.Context; +import android.os.WorkSource; +import android.telephony.CellInfo; +import android.telephony.ServiceState; +import android.telephony.TelephonyCallback; +import android.telephony.TelephonyManager; +import android.telephony.UiccSlotInfo; +import android.util.Log; + +import java.util.Arrays; +import java.util.BitSet; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.concurrent.Executor; +import java.util.stream.Collectors; + +@SuppressWarnings("AutoBoxing") +public class GmcTelephonyManager { + + private static final String TAG = "GmcTelephonyManager"; + + public static int[] filterTelephonyCallbackEvents(int[] eventsArray) { + Set events = Arrays.stream(eventsArray).boxed().collect(Collectors.toSet()); + + var sb = new StringBuilder(); + + if (!GmsCompat.hasPermission(Manifest.permission.READ_PHONE_STATE)) { + removeEvents(events, EVENTS_PROT_READ_PHONE_STATE, "READ_PHONE_STATE", sb); + } + + if (!GmsCompat.hasPermission(Manifest.permission.ACCESS_FINE_LOCATION)) { + removeEvents(events, EVENTS_PROT_ACCESS_FINE_LOCATION, + "ACCESS_FINE_LOCATION", sb); + } + + if (!GmsCompat.hasPermission(Manifest.permission.READ_ACTIVE_EMERGENCY_SESSION)) { + removeEvents(events, EVENTS_PROT_READ_ACTIVE_EMERGENCY_SESSION, + "READ_ACTIVE_EMERGENCY_SESSION", sb); + } + + if (!GmsCompat.hasPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE)) { + removeEvents(events, EVENTS_PROT_READ_PRIVILEGED_PHONE_STATE, + "READ_PRIVILEGED_PHONE_STATE", sb); + } + + if (!GmsCompat.hasPermission(Manifest.permission.READ_PRECISE_PHONE_STATE)) { + removeEvents(events, EVENTS_PROT_READ_PRECISE_PHONE_STATE, + "READ_PRECISE_PHONE_STATE", sb); + } + + int[] res = events.stream().mapToInt(Integer::intValue).toArray(); + + Log.d(TAG, "registering listener, events: " + Arrays.toString(res) + + (sb.length() != 0? "\nfiltered events due to missing permission\n" + sb : ""), + new Throwable()); + + return res; + } + + private static void removeEvents(Set events, int[] eventsToRemove, String permName, StringBuilder sb) { + if (events.size() == 0) { + return; + } + + boolean filtered = false; + + for (int event : eventsToRemove) { + if (!events.remove(event)) { + continue; + } + if (!filtered) { + sb.append(permName); + sb.append(": {"); + filtered = true; + } + sb.append(event); + sb.append(", "); + } + + if (filtered) { + sb.append("}\n"); + } + } + + private static final int[] EVENTS_PROT_READ_PHONE_STATE = { + TelephonyCallback.EVENT_CALL_FORWARDING_INDICATOR_CHANGED, + TelephonyCallback.EVENT_MESSAGE_WAITING_INDICATOR_CHANGED, + TelephonyCallback.EVENT_EMERGENCY_NUMBER_LIST_CHANGED, + TelephonyCallback.EVENT_LEGACY_CALL_STATE_CHANGED, + TelephonyCallback.EVENT_CALL_STATE_CHANGED, + TelephonyCallback.EVENT_ACTIVE_DATA_SUBSCRIPTION_ID_CHANGED, + TelephonyCallback.EVENT_CELL_INFO_CHANGED, + }; + + private static final int[] EVENTS_PROT_ACCESS_FINE_LOCATION = { + TelephonyCallback.EVENT_CELL_LOCATION_CHANGED, + TelephonyCallback.EVENT_CELL_INFO_CHANGED, + TelephonyCallback.EVENT_REGISTRATION_FAILURE, + TelephonyCallback.EVENT_BARRING_INFO_CHANGED, + }; + + private static final int[] EVENTS_PROT_READ_ACTIVE_EMERGENCY_SESSION = { + TelephonyCallback.EVENT_OUTGOING_EMERGENCY_CALL, + TelephonyCallback.EVENT_OUTGOING_EMERGENCY_SMS, + }; + + private static final int[] EVENTS_PROT_READ_PRIVILEGED_PHONE_STATE = { + TelephonyCallback.EVENT_SRVCC_STATE_CHANGED, + TelephonyCallback.EVENT_VOICE_ACTIVATION_STATE_CHANGED, + TelephonyCallback.EVENT_RADIO_POWER_STATE_CHANGED, + TelephonyCallback.EVENT_ALLOWED_NETWORK_TYPE_LIST_CHANGED, + TelephonyCallback.EVENT_EMERGENCY_CALLBACK_MODE_CHANGED, + }; + + private static final int[] EVENTS_PROT_READ_PRECISE_PHONE_STATE = { + TelephonyCallback.EVENT_PRECISE_DATA_CONNECTION_STATE_CHANGED, + TelephonyCallback.EVENT_DATA_CONNECTION_REAL_TIME_INFO_CHANGED, + TelephonyCallback.EVENT_PRECISE_CALL_STATE_CHANGED, + TelephonyCallback.EVENT_CALL_DISCONNECT_CAUSE_CHANGED, + TelephonyCallback.EVENT_CALL_ATTRIBUTES_CHANGED, + TelephonyCallback.EVENT_IMS_CALL_DISCONNECT_CAUSE_CHANGED, + TelephonyCallback.EVENT_REGISTRATION_FAILURE, + TelephonyCallback.EVENT_BARRING_INFO_CHANGED, + TelephonyCallback.EVENT_PHYSICAL_CHANNEL_CONFIG_CHANGED, + TelephonyCallback.EVENT_DATA_ENABLED_CHANGED, + TelephonyCallback.EVENT_LINK_CAPACITY_ESTIMATE_CHANGED, + TelephonyCallback.EVENT_MEDIA_QUALITY_STATUS_CHANGED, + }; +} diff --git a/core/java/com/android/internal/gmscompat/sysservice/GmcUserManager.java b/core/java/com/android/internal/gmscompat/sysservice/GmcUserManager.java new file mode 100644 index 0000000000000..5f8e8206fe0b9 --- /dev/null +++ b/core/java/com/android/internal/gmscompat/sysservice/GmcUserManager.java @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.gmscompat.sysservice; + +import android.annotation.Nullable; +import android.annotation.UserIdInt; +import android.content.Context; +import android.content.pm.UserInfo; +import android.os.IUserManager; +import android.os.Process; +import android.os.UserHandle; +import android.os.UserManager; + +import java.util.Collections; +import java.util.List; + +/** + * GMS tries to interact across user profiles, which requires privileged permissions. + * As a workaround, a pseudo-single-user environment is constructed by hiding non-current users + * and marking the current user as the primary ("Owner") user. + */ +public class GmcUserManager extends UserManager { + public GmcUserManager(Context context, IUserManager service) { + super(context, service); + } + + private static int getUserId() { + return UserHandle.myUserId(); + } + + private static void checkUserId(int userId) { + if (userId != getUserId() && userId != UserHandle.USER_CURRENT) { + throw new IllegalStateException("unexpected userId " + userId); + } + } + + public static UserHandle translateUserHandle(UserHandle h) { + if (UserHandle.ALL.equals(h)) { + return UserHandle.of(getUserId()); + } + + checkUserId(h.getIdentifier()); + return h; + } + + private static int getUserSerialNumber() { + // GMS has several hardcoded (userSerialNumber == 0) checks + return 0; + } + + private static String getUserType_() { + // "system" means "primary" ("Owner") user + return UserManager.USER_TYPE_FULL_SYSTEM; + } + + private static UserInfo getUserInfo() { + // obtaining UserInfo is a privileged operation (even for the current user) + UserInfo ui = new UserInfo(); + ui.id = getUserId(); + ui.serialNumber = getUserSerialNumber(); + ui.userType = getUserType_(); + ui.flags = UserInfo.FLAG_SYSTEM | UserInfo.FLAG_FULL | UserInfo.FLAG_MAIN; + return ui; + } + + @Override + public boolean isSystemUser() { + return true; + } + + @Override + public boolean isUserOfType(String userType) { + return getUserType_().equals(userType); + } + + @Override + public UserInfo getUserInfo(int userId) { + checkUserId(userId); + return getUserInfo(); + } + + @Override + public boolean hasBaseUserRestriction(String restrictionKey, UserHandle userHandle) { + // Can't ignore device policy restrictions without permission + return hasUserRestriction(restrictionKey, userHandle); + } + + @Override + public List getUsers(boolean excludeDying) { + return Collections.singletonList(getUserInfo()); + } + + @Override + public int getUserSerialNumber(@UserIdInt int userId) { + checkUserId(userId); + return getUserSerialNumber(); + } + + @Override + public @UserIdInt int getUserHandle(int userSerialNumber) { + if (userSerialNumber != getUserSerialNumber()) { + throw new IllegalStateException("unexpected userSerialNumber " + userSerialNumber); + } + return getUserId(); + } + + // ActivityManager#getCurrentUser() + public static int amGetCurrentUser() { + return getUserId(); + } + + // ActivityManager#isUserRunning(int) + public static boolean amIsUserRunning(int userId) { + checkUserId(userId); + return true; + } + + // support for managed ("work") profiles + + @Override + public List getProfiles(@UserIdInt int userId) { + checkUserId(userId); + return getUsers(); + } + + @Override + public int[] getProfileIds(@UserIdInt int userId, boolean enabledOnly) { + checkUserId(userId); + return new int[] { userId }; + } + + @Override + public UserInfo getProfileParent(int userId) { + checkUserId(userId); + return null; + } + + @Override + public boolean isRestrictedProfile() { + return false; + } + + @Override + public boolean isDemoUser() { + return false; + } + + @Nullable + @Override + protected String getProfileType(int userId) { + checkUserId(userId); + return ""; + } + + @Nullable + @Override + public UserHandle getPreviousForegroundUser() { + return null; + } + + @Nullable + @Override + public UserHandle getMainUser() { + return Process.myUserHandle(); + } + + @Override + public boolean isMainUser() { + return true; + } + + @Override + public UserHandle getBootUser() { + return Process.myUserHandle(); + } + + @Override + public boolean isAdminUser() { + return true; + } +} diff --git a/core/java/com/android/internal/gmscompat/util/CursorWrapperExt.java b/core/java/com/android/internal/gmscompat/util/CursorWrapperExt.java new file mode 100644 index 0000000000000..f7424df0db93f --- /dev/null +++ b/core/java/com/android/internal/gmscompat/util/CursorWrapperExt.java @@ -0,0 +1,67 @@ +package com.android.internal.gmscompat.util; + +import android.database.Cursor; +import android.database.CursorWrapper; + +public abstract class CursorWrapperExt extends CursorWrapper { + + protected CursorWrapperExt(Cursor orig) { + super(orig); + } + + protected abstract void onPositionChanged(); + + @Override + public boolean moveToLast() { + if (super.moveToLast()) { + onPositionChanged(); + return true; + } + return false; + } + + @Override + public boolean move(int offset) { + if (super.move(offset)) { + onPositionChanged(); + return true; + } + return false; + } + + @Override + public boolean moveToPosition(int position) { + if (super.moveToPosition(position)) { + onPositionChanged(); + return true; + } + return false; + } + + @Override + public boolean moveToNext() { + if (super.moveToNext()) { + onPositionChanged(); + return true; + } + return false; + } + + @Override + public boolean moveToFirst() { + if (super.moveToFirst()) { + onPositionChanged(); + return true; + } + return false; + } + + @Override + public boolean moveToPrevious() { + if (super.moveToPrevious()) { + onPositionChanged(); + return true; + } + return false; + } +} diff --git a/core/java/com/android/internal/gmscompat/util/GmcActivityUtils.java b/core/java/com/android/internal/gmscompat/util/GmcActivityUtils.java new file mode 100644 index 0000000000000..4e707f5c6a49b --- /dev/null +++ b/core/java/com/android/internal/gmscompat/util/GmcActivityUtils.java @@ -0,0 +1,153 @@ +package com.android.internal.gmscompat.util; + +import android.Manifest; +import android.annotation.Nullable; +import android.app.Activity; +import android.app.ActivityOptions; +import android.app.Application; +import android.app.compat.gms.GmsCompat; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothManager; +import android.content.ComponentName; +import android.content.Intent; +import android.ext.PackageId; +import android.os.Bundle; +import android.os.RemoteException; +import android.os.SystemClock; + +import com.android.internal.gmscompat.GmsCompatApp; +import com.android.internal.gmscompat.PlayStoreHooks; + +public class GmcActivityUtils implements Application.ActivityLifecycleCallbacks { + private static final String TAG = GmcActivityUtils.class.getSimpleName(); + + public static final GmcActivityUtils INSTANCE = new GmcActivityUtils(); + + @Nullable + private Activity mostRecentVisibleActivity; + + private GmcActivityUtils() {} + + @Nullable + public static Activity getMostRecentVisibleActivity() { + return INSTANCE.mostRecentVisibleActivity; + } + + @Override + public void onActivityResumed(Activity activity) { + mostRecentVisibleActivity = activity; + + String className = activity.getClass().getName(); + + if (GmsCompat.isGmsCore()) { + switch (className) { + case "com.google.android.gms.nearby.sharing.ShareSheetActivity": + handleNearbyShareActivityResume(activity, true); + break; + case "com.google.android.gms.nearby.sharing.InternalReceiveSurfaceActivity": + handleNearbyShareActivityResume(activity, false); + break; + } + } + + if (GmsCompat.isPlayStore()) { + PlayStoreHooks.onActivityResumed(activity); + } + } + + @Override + public void onActivityPaused(Activity activity) { + if (mostRecentVisibleActivity == activity) { + mostRecentVisibleActivity = null; + } + } + + @Override public void onActivityCreated(Activity activity, @Nullable Bundle savedInstanceState) {} + @Override public void onActivityStarted(Activity activity) {} + @Override public void onActivityStopped(Activity activity) {} + @Override public void onActivitySaveInstanceState(Activity activity, Bundle outState) {} + @Override public void onActivityDestroyed(Activity activity) {} + + private static long lastBtEnableRequest; + private static long lastBtDiscoverabilityRequest; + + private static void handleNearbyShareActivityResume(Activity activity, boolean isSend) { + if (GmsCompat.hasPermission(Manifest.permission.BLUETOOTH_CONNECT)) { + var bm = GmsCompat.appContext().getSystemService(BluetoothManager.class); + BluetoothAdapter adapter = bm.getAdapter(); + + final long repeatInterval = 20_000; + + long ts = SystemClock.elapsedRealtime(); + if (isSend) { + if (adapter.getState() == BluetoothAdapter.STATE_OFF) { + if (ts - lastBtEnableRequest > repeatInterval) { + var intent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); + activity.startActivity(intent); + lastBtEnableRequest = ts; + } + } + } else { + if (adapter.getScanMode() == BluetoothAdapter.SCAN_MODE_NONE) { + if (ts - lastBtDiscoverabilityRequest > repeatInterval) { + var intent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE); + activity.startActivity(intent); + lastBtDiscoverabilityRequest = ts; + } + } + } + } else { + try { + GmsCompatApp.iGms2Gca().showGmsCoreMissingPermissionForNearbyShareNotification(); + } catch (RemoteException e) { + GmsCompatApp.callFailed(e); + } + } + } + + // See https://developer.android.com/about/versions/14/behavior-changes-14#background-activity-restrictions + public static Bundle allowActivityLaunchFromPendingIntent(@Nullable Bundle orig) { + var ao = orig != null ? ActivityOptions.fromBundle(orig) : ActivityOptions.makeBasic(); + ao.setPendingIntentBackgroundActivityStartMode(ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED); + return ao.toBundle(); + } + + public static Intent overrideStartActivityIntent(Intent intent) { + ComponentName cn = intent.getComponent(); + if (cn != null && "com.google.android.permissioncontroller".equals(cn.getPackageName())) { + // PermissionController activities can't be opened by unprivileged apps. + // (Replacing absent com.google.android.permissioncontroller package with + // com.android.permissioncontroller would not help) + return null; + } + + if (BluetoothAdapter.ACTION_REQUEST_ENABLE.equals(intent.getAction())) { + if (GmsCompat.isGmsCore() && !GmsCompat.hasPermission(Manifest.permission.BLUETOOTH_CONNECT)) { + try { + GmsCompatApp.iGms2Gca().showGmsCoreMissingNearbyDevicesPermissionGeneric(); + } catch (RemoteException e) { + GmsCompatApp.callFailed(e); + } + return null; + } + } + + String pkg = cn != null ? cn.getPackageName() : intent.getPackage(); + if (pkg != null) { + boolean checkIntent = false; + switch (pkg) { + case PackageId.G_SEARCH_APP_NAME -> + checkIntent = true; + } + + if (checkIntent) { + var pm = GmsCompat.appContext().getPackageManager(); + if (pm.resolveActivity(intent, 0) == null) { + return null; + } + } + } + + return intent; + } +} diff --git a/core/java/com/android/internal/notification/SystemNotificationChannels.java b/core/java/com/android/internal/notification/SystemNotificationChannels.java index 415aaa0243a7a..fb6c73767b08a 100644 --- a/core/java/com/android/internal/notification/SystemNotificationChannels.java +++ b/core/java/com/android/internal/notification/SystemNotificationChannels.java @@ -68,6 +68,7 @@ public class SystemNotificationChannels { */ @Deprecated public static final String SYSTEM_CHANGES_DEPRECATED = "SYSTEM_CHANGES"; public static final String SYSTEM_CHANGES = "SYSTEM_CHANGES_ALERTS"; + public static final String OTHER_USERS = "OTHER_USERS"; public static final String ACCESSIBILITY_MAGNIFICATION = "ACCESSIBILITY_MAGNIFICATION"; public static final String ACCESSIBILITY_HEARING_DEVICE = "ACCESSIBILITY_HEARING_DEVICE"; public static final String ACCESSIBILITY_SECURITY_POLICY = "ACCESSIBILITY_SECURITY_POLICY"; @@ -206,6 +207,13 @@ public static void createAll(Context context) { .build()); channelsList.add(systemChanges); + NotificationChannel otherUsers = new NotificationChannel(OTHER_USERS, + context.getString(R.string.notification_channel_other_users), + NotificationManager.IMPORTANCE_DEFAULT); + otherUsers.setDescription(context.getString(R.string.notification_channel_other_users_description)); + otherUsers.setBlockable(true); + channelsList.add(otherUsers); + final NotificationChannel newFeaturePrompt = new NotificationChannel( ACCESSIBILITY_MAGNIFICATION, context.getString(R.string.notification_channel_accessibility_magnification), @@ -232,6 +240,8 @@ public static void createAll(Context context) { NotificationManager.IMPORTANCE_LOW); channelsList.add(abusiveBackgroundAppsChannel); + extraChannels(context, channelsList); + if (enableSupervisionManagerPolicyApis()) { final NotificationChannel parentalControlsChannel = new NotificationChannel( @@ -279,4 +289,37 @@ private static NotificationChannel newAccountChannel(Context context) { } private SystemNotificationChannels() {} + + public static final String MISSING_PERMISSION = "MISSING_PERMISSION"; + public static final String BACKGROUND_DEXOPT_PROGRESS = "BACKGROUND_DEXOPT"; + public static final String BACKGROUND_DEXOPT_COMPLETED = "BACKGROUND_DEXOPT_COMPLETED"; + public static final String EXPLOIT_PROTECTION = "EXPLOIT_PROTECTION"; + public static final String SYSTEM_JOURNAL = "SYSTEM_JOURNAL"; + + private static void extraChannels(Context ctx, List dest) { + channel(ctx, MISSING_PERMISSION, + R.string.notification_channel_missing_permission, + NotificationManager.IMPORTANCE_HIGH, true, dest); + channel(ctx, BACKGROUND_DEXOPT_PROGRESS, R.string.bg_dexopt_notif_ch_title, + NotificationManager.IMPORTANCE_DEFAULT, true, dest); + + channel(ctx, BACKGROUND_DEXOPT_COMPLETED, R.string.bg_dexopt_completed_notif_ch_title, + NotificationManager.IMPORTANCE_HIGH, true, dest); + + channel(ctx, EXPLOIT_PROTECTION, R.string.notif_channel_exploit_protection, + NotificationManager.IMPORTANCE_HIGH, true, dest); + + channel(ctx, SYSTEM_JOURNAL, R.string.notif_ch_system_journal, + NotificationManager.IMPORTANCE_HIGH, true, dest); + } + + private static NotificationChannel channel(Context ctx, String id, int nameRes, int importance, boolean silent, List dest) { + var c = new NotificationChannel(id, ctx.getText(nameRes), importance); + if (silent) { + c.setSound(null, null); + c.enableVibration(false); + } + dest.add(c); + return c; + } } diff --git a/core/java/com/android/internal/os/AppZygoteInit.java b/core/java/com/android/internal/os/AppZygoteInit.java index f925afc2a921d..3b9f79c227b04 100644 --- a/core/java/com/android/internal/os/AppZygoteInit.java +++ b/core/java/com/android/internal/os/AppZygoteInit.java @@ -48,7 +48,7 @@ protected ZygoteConnection createNewConnection(LocalSocket socket, String abiLis } } - private static class AppZygoteConnection extends ZygoteConnection { + static class AppZygoteConnection extends ZygoteConnection { AppZygoteConnection(LocalSocket socket, String abiList) throws IOException { super(socket, abiList); } @@ -72,11 +72,22 @@ protected boolean canPreloadApp() { @Override protected void handlePreloadApp(ApplicationInfo appInfo) { + handlePreloadAppStatic(this, appInfo); + } + + static void handlePreloadAppStatic(@android.annotation.Nullable AppZygoteConnection conn, ApplicationInfo appInfo) { Log.i(TAG, "Beginning application preload for " + appInfo.packageName); LoadedApk loadedApk = new LoadedApk(null, appInfo, null, null, false, true, false); ClassLoader loader = loadedApk.getClassLoader(); - Zygote.allowAppFilesAcrossFork(appInfo); + if (conn == null) { + com.android.internal.util.Preconditions + .checkState(ExecSpawning.isExecSpawnedProcess()); + } + + if (conn != null) { + Zygote.allowAppFilesAcrossFork(appInfo); + } if (appInfo.zygotePreloadName != null) { Class cl; @@ -91,9 +102,9 @@ protected void handlePreloadApp(ApplicationInfo appInfo) { } else { Constructor ctor = cl.getConstructor(); ZygotePreload preloadObject = (ZygotePreload) ctor.newInstance(); - Zygote.markOpenedFilesBeforePreload(); + if (conn != null) Zygote.markOpenedFilesBeforePreload(); preloadObject.doPreload(appInfo); - Zygote.allowFilesOpenedByPreload(); + if (conn != null) Zygote.allowFilesOpenedByPreload(); } } catch (ReflectiveOperationException e) { Log.e(TAG, "AppZygote application preload failed for " @@ -103,11 +114,13 @@ protected void handlePreloadApp(ApplicationInfo appInfo) { Log.i(TAG, "No zygotePreloadName attribute specified."); } - try { - DataOutputStream socketOut = getSocketOutputStream(); - socketOut.writeInt(loader != null ? 1 : 0); - } catch (IOException e) { - throw new IllegalStateException("Error writing to command socket", e); + if (conn != null) { + try { + DataOutputStream socketOut = conn.getSocketOutputStream(); + socketOut.writeInt(loader != null ? 1 : 0); + } catch (IOException e) { + throw new IllegalStateException("Error writing to command socket", e); + } } Log.i(TAG, "Application preload done"); diff --git a/core/java/com/android/internal/os/ExecSpawning.java b/core/java/com/android/internal/os/ExecSpawning.java new file mode 100644 index 0000000000000..a49c585f336cf --- /dev/null +++ b/core/java/com/android/internal/os/ExecSpawning.java @@ -0,0 +1,148 @@ +package com.android.internal.os; + +import android.content.pm.ApplicationInfo; +import android.os.ParcelFileDescriptor; +import android.util.Log; + +import com.android.internal.util.Preconditions; + +import java.io.IOException; +import java.util.Arrays; + +import static com.android.internal.util.Preconditions.checkState; +import static java.util.Objects.requireNonNull; + +public class ExecSpawning { + static final String TAG = "ExecSpawning"; + + // Zygote argument for passing commands from zygote to exec-spawned app process + static final String COMMAND_FD_ARG = "--command-fd="; + + private static boolean isExecSpawnedProcess; + private static ZygoteArguments[] commandsToReplay; + + public static boolean isExecSpawnedProcess() { + return isExecSpawnedProcess; + } + + public static boolean isReplayingZygoteCommands() { + return commandsToReplay != null; + } + + static void init(String[] argv) { + boolean logv = Log.isLoggable(TAG, Log.VERBOSE); + if (logv) Log.v(TAG, "argv: " + Arrays.toString(argv)); + + String arg = argv[argv.length - 1]; + if (!arg.startsWith(COMMAND_FD_ARG)) { + if (arg.equals("--socket-name=zygote")) { + // we're in the primary 64-bit zygote + ZygoteCommandRecorder.enable(true); + } else if (arg.equals("--enable-lazy-preload") && "--socket-name=zygote_secondary".equals(argv[argv.length - 2])) { + // we're in the 32-bit zygote + ZygoteCommandRecorder.enable(false); + } + return; + } + + if (logv) Log.v(TAG, "init"); + + isExecSpawnedProcess = true; + + byte[] serializedCmds; + { + int separator = arg.indexOf('_', COMMAND_FD_ARG.length()); + int fd = Integer.parseInt(arg, COMMAND_FD_ARG.length(), separator, 10); + int fdSize = Integer.parseInt(arg, separator + 1, arg.length(), 10); + try (var stream = new ParcelFileDescriptor.AutoCloseInputStream(ParcelFileDescriptor.adoptFd(fd))) { + serializedCmds = stream.readNBytes(fdSize); + } catch (IOException e) { + Log.e(TAG, "", e); + // fd is a memfd, using it should never fail + throw new IllegalStateException(e); + } + checkState(serializedCmds.length == fdSize); + } + checkState(commandsToReplay == null); + commandsToReplay = ZygoteCommandRecorder.deserializeCommands(serializedCmds); + } + + static Runnable replayCommands(ZygoteServer zygoteServer) { + ZygoteArguments[] commandsToReplay = ExecSpawning.commandsToReplay; + requireNonNull(commandsToReplay); + + ZygoteConnection pseudoConnection; + try { + pseudoConnection = new ZygoteConnection(null, null); + } catch (IOException e) { + // pseudo ZygoteConnection doesn't perform any IO + throw new IllegalStateException(e); + } + + boolean logv = Log.isLoggable(TAG, Log.VERBOSE); + + for (int i = 0; i < commandsToReplay.length; ++i) { + ZygoteArguments cmd = commandsToReplay[i]; + if (logv) Log.v(TAG, "replaying cmd " + (i + 1) + " / " + commandsToReplay.length + ": " + cmd); + Runnable r = pseudoConnection.processCommand(zygoteServer, false, cmd); + if (i == commandsToReplay.length - 1) { + checkState(r != null); + ExecSpawning.commandsToReplay = null; + return r; + } + if (r != null) { // some commands run in-place, without a Runnable + r.run(); + } + } + throw new IllegalStateException("unreachable"); + } + + private static boolean hasPendingZygotePreload; + + public static void addPendingZygotePreload() { + Preconditions.checkState(!hasPendingZygotePreload); + hasPendingZygotePreload = true; + } + + public static void maybeRunPendingZygotePreload(ApplicationInfo appInfo) { + if (hasPendingZygotePreload) { + hasPendingZygotePreload = false; + Log.d(TAG, "running manual ZygotePreload"); + // Note that the process is in the isolated_app SELinux domain at this point. When zygote + // spawning is used, app zygote is running in the app_zygote SELinux domain when it performs + // preloading. This shouldn't cause issues in practice since app_zygote and isolated_app + // domains have similar level of isolation. + AppZygoteInit.AppZygoteConnection.handlePreloadAppStatic(null, appInfo); + } + } + + public static boolean shouldSkipZygotePreload(ApplicationInfo appInfo, boolean isNative) { + if (isNative) { + String zygotePreloadNativeLib = appInfo.zygotePreloadNativeLib; + switch (zygotePreloadNativeLib) { + case "libmonochrome_64.so": + Log.i(TAG, "skipping zygotePreloadNativeLib: " + zygotePreloadNativeLib); + return true; + default: + Log.w(TAG, "keeping unknown zygotePreloadNativeLib: " + zygotePreloadNativeLib); + return false; + } + } else { + String zygotePreloadName = appInfo.zygotePreloadName; + switch (zygotePreloadName) { + case "org.chromium.chrome.app.TrichromeZygotePreload": + case "org.chromium.content_public.app.ZygotePreload": + case "org.mozilla.gecko.process.ZygotePreload": + // VosZygotePreload is used by https://www.v-key.com/products/v-os-mobile-app-protection/ + // Its checks don't pass for an unknown reason when exec spawning is used, but + // skipping them entirely works at least for some apps (e.g. for com.dbs.sg.dbsmbanking app) + case "vkey.android.vos.VosZygotePreload": + Log.i(TAG, "skipping Java ZygotePreload: " + zygotePreloadName); + return true; + default: + Log.w(TAG, "keeping unknown Java ZygotePreload: " + zygotePreloadName); + return false; + } + } + } +} diff --git a/core/java/com/android/internal/os/SELinuxFlags.java b/core/java/com/android/internal/os/SELinuxFlags.java new file mode 100644 index 0000000000000..d42a5af9339db --- /dev/null +++ b/core/java/com/android/internal/os/SELinuxFlags.java @@ -0,0 +1,144 @@ +package com.android.internal.os; + +import android.annotation.Nullable; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.GosPackageState; +import android.ext.BrowserUtils; +import android.ext.settings.app.AswDenyNativeDebug; +import android.ext.settings.app.AswRestrictMemoryDynCodeLoading; +import android.ext.settings.app.AswRestrictStorageDynCodeLoading; +import android.ext.settings.app.AswRestrictWebViewDynCodeLoading; +import android.os.Build; +import android.os.Process; +import android.os.SystemProperties; +import android.util.Log; + +import java.io.File; +import java.nio.file.Files; + +// Per-app per-user per-process SELinux flags which are passed to the kernel via the selinux_flags +// process attribute. +// +// This process attribute is writable only by zygote and webview_zygote SELinux domains, app_zygote +// domain is intentionally omitted since it can run untrusted app code. +public class SELinuxFlags { + public static final long DENY_EXECMEM = 1; + public static final long DENY_EXECMOD = (1 << 1); + public static final long DENY_EXECUTE_APPDOMAIN_TMPFS = (1 << 2); + public static final long DENY_EXECUTE_APP_DATA_FILE = (1 << 3); + public static final long DENY_EXECUTE_NO_TRANS_APP_DATA_FILE = (1 << 4); + public static final long DENY_EXECUTE_ASHMEM_DEVICE = (1 << 5); + public static final long DENY_EXECUTE_ASHMEM_LIBCUTILS_DEVICE = (1 << 6); + public static final long DENY_EXECUTE_PRIVAPP_DATA_FILE = (1 << 7); + public static final long DENY_PROCESS_PTRACE = (1 << 8); + + public static final long RESTRICT_MEMORY_DYN_CODE_EXEC_FLAGS = + DENY_EXECMEM + | DENY_EXECMOD + | DENY_EXECUTE_APPDOMAIN_TMPFS + | DENY_EXECUTE_ASHMEM_DEVICE + | DENY_EXECUTE_ASHMEM_LIBCUTILS_DEVICE; + + public static final long RESTRICT_STORAGE_DYN_CODE_EXEC_FLAGS = + DENY_EXECUTE_APP_DATA_FILE + | DENY_EXECUTE_NO_TRANS_APP_DATA_FILE + | DENY_EXECUTE_PRIVAPP_DATA_FILE; + + public static final long RESTRICT_DYN_CODE_EXEC_FLAGS = + RESTRICT_MEMORY_DYN_CODE_EXEC_FLAGS | RESTRICT_STORAGE_DYN_CODE_EXEC_FLAGS; + + public static final long MEMORY_DYN_CODE_EXEC_FLAGS_THAT_BREAK_WEB_JIT = DENY_EXECMEM; + + public static final long ALL_RESTRICTIONS = + RESTRICT_DYN_CODE_EXEC_FLAGS + | DENY_PROCESS_PTRACE + ; + + static long getForWebViewProcess(Context ctx, int userId, ApplicationInfo callerAppInfo, + GosPackageState callerPs) { + if (Build.IS_DEBUGGABLE || Build.IS_EMULATOR) { + if (!kernelSupportsSELinuxFlags()) { + return 0L; + } + } + + long res = ALL_RESTRICTIONS; + + if (!AswRestrictWebViewDynCodeLoading.I.get(ctx, userId, callerAppInfo, callerPs)) { + res &= ~MEMORY_DYN_CODE_EXEC_FLAGS_THAT_BREAK_WEB_JIT; + } + + return res; + } + + static long get(Context ctx, int userId, ApplicationInfo appInfo, + GosPackageState ps, boolean isIsolatedProcess) { + if (Build.IS_DEBUGGABLE || Build.IS_EMULATOR) { + if (!kernelSupportsSELinuxFlags()) { + return 0L; + } + } + + long res = ALL_RESTRICTIONS; + + if (!AswDenyNativeDebug.I.get(ctx, userId, appInfo, ps)) { + res &= ~DENY_PROCESS_PTRACE; + } + + if (!AswRestrictMemoryDynCodeLoading.I.get(ctx, userId, appInfo, ps)) { + if (BrowserUtils.isSystemBrowser(ctx, appInfo.packageName)) { + // Chromium-based browsers use JIT only in isolated processes + if (isIsolatedProcess) { + res &= ~MEMORY_DYN_CODE_EXEC_FLAGS_THAT_BREAK_WEB_JIT; + } + } else { + res &= ~RESTRICT_MEMORY_DYN_CODE_EXEC_FLAGS; + } + } + + if (!AswRestrictStorageDynCodeLoading.I.get(ctx, userId, appInfo, ps)) { + res &= ~RESTRICT_STORAGE_DYN_CODE_EXEC_FLAGS; + } + + return res; + } + + private static String getSelfProcAttrPath() { + return "/proc/self/task/" + Process.myPid() + "/attr/selinux_flags"; + } + + public static boolean isExecmemBlocked() { + return (getFlags() & DENY_EXECMEM) != 0; + } + + private static long getFlags() { + String flagsPath = getSelfProcAttrPath(); + try { + byte[] val = Files.readAllBytes(new File(flagsPath).toPath()); + return Long.parseLong(new String(val), 16); + } catch (Exception e) { + Log.d("SELinux", "", e); + return 0L; + } + } + + public static boolean isSystemAppSepolicyWeakeningAllowed() { + if (Build.IS_DEBUGGABLE) { + return SystemProperties.getBoolean("persist.sys.allow_weakening_system_app_sepolicy", false); + } + return false; + } + + private static volatile Boolean kernelSupportsSELinuxFlagsCache; + + public static boolean kernelSupportsSELinuxFlags() { + Boolean cache = kernelSupportsSELinuxFlagsCache; + if (cache == null) { + var f = new File(getSelfProcAttrPath()); + cache = Boolean.valueOf(f.exists()); + kernelSupportsSELinuxFlagsCache = cache; + } + return cache.booleanValue(); + } +} diff --git a/core/java/com/android/internal/os/Zygote.java b/core/java/com/android/internal/os/Zygote.java index 9c6b12cf6fe5c..c51230aeeb94c 100644 --- a/core/java/com/android/internal/os/Zygote.java +++ b/core/java/com/android/internal/os/Zygote.java @@ -20,12 +20,16 @@ import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.AppGlobals; import android.compat.annotation.ChangeId; import android.compat.annotation.Disabled; import android.compat.annotation.EnabledAfter; import android.content.Context; import android.content.pm.ApplicationInfo; +import android.content.pm.GosPackageState; import android.content.pm.ProcessInfo; +import android.ext.settings.app.AppSwitch; +import android.ext.settings.app.AswUseMemoryTagging; import android.net.Credentials; import android.net.LocalServerSocket; import android.net.LocalSocket; @@ -37,9 +41,11 @@ import android.os.ServiceManager; import android.os.SystemProperties; import android.os.Trace; +import android.os.UserHandle; import android.provider.DeviceConfig; import android.system.ErrnoException; import android.system.Os; +import android.util.DisplayMetrics; import android.util.Log; import com.android.internal.compat.IPlatformCompat; @@ -55,6 +61,7 @@ import java.io.DataOutputStream; import java.io.FileDescriptor; import java.io.IOException; +import java.util.concurrent.atomic.AtomicBoolean; /** @hide */ public final class Zygote { @@ -297,31 +304,11 @@ public final class Zygote { /** a prototype instance for a future List.toArray() */ static final int[][] INT_ARRAY_2D = new int[0][0]; - /** - * @hide for internal use only. - */ - public static final String PRIMARY_SOCKET_NAME = "zygote"; - - /** - * @hide for internal use only. - */ - public static final String SECONDARY_SOCKET_NAME = "zygote_secondary"; - /** * @hide for internal use only. */ public static final String NATIVE_SOCKET_NAME = "zygote_next"; - /** - * @hide for internal use only - */ - public static final String USAP_POOL_PRIMARY_SOCKET_NAME = "usap_pool_primary"; - - /** - * @hide for internal use only - */ - public static final String USAP_POOL_SECONDARY_SOCKET_NAME = "usap_pool_secondary"; - private Zygote() {} private static boolean containsInetGid(int[] gids) { @@ -373,16 +360,18 @@ private static boolean containsInetGid(int[] gids) { * @return 0 if this is the child, pid of the child * if this is the parent, or -1 on error. */ - static int forkAndSpecialize(int uid, int gid, int[] gids, int runtimeFlags, + static int forkAndSpecialize(ZygoteExtraArgs extraArgs, int uid, int gid, int[] gids, int runtimeFlags, int[][] rlimits, int mountExternal, String seInfo, String niceName, int[] fdsToClose, int[] fdsToIgnore, boolean startChildZygote, String instructionSet, String appDataDir, boolean isTopApp, String[] pkgDataInfoList, String[] allowlistedDataInfoList, boolean bindMountAppDataDirs, boolean bindMountAppStorageDirs, boolean bindMountSyspropOverrides) { + // preFork is needed in exec spawned processes too since it switches the process into + // single-threaded mode which is required for changing the SELinux context ZygoteHooks.preFork(); boolean useFifoUi = SystemProperties.getInt("sys.use_fifo_ui", 0) == 1; - int pid = nativeForkAndSpecialize( + int pid = nativeForkAndSpecialize(extraArgs.makeJniLongArray(), uid, gid, gids, runtimeFlags, rlimits, mountExternal, seInfo, niceName, fdsToClose, fdsToIgnore, startChildZygote, instructionSet, appDataDir, isTopApp, useFifoUi, pkgDataInfoList, allowlistedDataInfoList, bindMountAppDataDirs, @@ -404,7 +393,9 @@ static int forkAndSpecialize(int uid, int gid, int[] gids, int runtimeFlags, return pid; } - private static native int nativeForkAndSpecialize(int uid, int gid, int[] gids, + static native int nativeForkExec(boolean is64Bit, boolean isNativeAppLaunch, byte[] commandBuf, boolean disableHardenedMalloc, boolean enableCompatVa39Bit); + + private static native int nativeForkAndSpecialize(long[] extraLongArgs, int uid, int gid, int[] gids, int runtimeFlags, int[][] rlimits, int mountExternal, String seInfo, String niceName, int[] fdsToClose, int[] fdsToIgnore, boolean startChildZygote, String instructionSet, String appDataDir, boolean isTopApp, boolean useFifoUi, String[] pkgDataInfoList, @@ -442,13 +433,13 @@ private static native int nativeForkAndSpecialize(int uid, int gid, int[] gids, * @param bindMountSyspropOverrides True if the zygote needs to mount the override system * properties */ - private static void specializeAppProcess(int uid, int gid, int[] gids, int runtimeFlags, + private static void specializeAppProcess(ZygoteExtraArgs extraArgs, int uid, int gid, int[] gids, int runtimeFlags, int[][] rlimits, int mountExternal, String seInfo, String niceName, boolean startChildZygote, String instructionSet, String appDataDir, boolean isTopApp, String[] pkgDataInfoList, String[] allowlistedDataInfoList, boolean bindMountAppDataDirs, boolean bindMountAppStorageDirs, boolean bindMountSyspropOverrides) { - nativeSpecializeAppProcess(uid, gid, gids, runtimeFlags, rlimits, mountExternal, seInfo, + nativeSpecializeAppProcess(extraArgs.makeJniLongArray(), uid, gid, gids, runtimeFlags, rlimits, mountExternal, seInfo, niceName, startChildZygote, instructionSet, appDataDir, isTopApp, pkgDataInfoList, allowlistedDataInfoList, bindMountAppDataDirs, bindMountAppStorageDirs, bindMountSyspropOverrides); @@ -472,7 +463,7 @@ private static void specializeAppProcess(int uid, int gid, int[] gids, int runti ZygoteHooks.postForkCommon(); } - private static native void nativeSpecializeAppProcess(int uid, int gid, int[] gids, + private static native void nativeSpecializeAppProcess(long[] extraLongArgs, int uid, int gid, int[] gids, int runtimeFlags, int[][] rlimits, int mountExternal, String seInfo, String niceName, boolean startChildZygote, String instructionSet, String appDataDir, boolean isTopApp, String[] pkgDataInfoList, String[] allowlistedDataInfoList, @@ -585,14 +576,12 @@ static void allowFilesOpenedByPreload() { * - Initializing security properties * - Unmounting storage as appropriate * - Loading necessary performance profile information - * - * @param isPrimary True if this is the zygote process, false if it is zygote_secondary */ - static void initNativeState(boolean isPrimary) { - nativeInitNativeState(isPrimary); + static void initNativeState(ZygoteType type) { + nativeInitNativeState(ExecSpawning.isExecSpawnedProcess(), type.getSocketName(), type.getUsapPoolSocketName()); } - protected static native void nativeInitNativeState(boolean isPrimary); + protected static native void nativeInitNativeState(boolean isExecSpawning, String socketName, String usapPoolSocketName); /** * Returns the raw string value of a system property. @@ -727,7 +716,7 @@ private static native int nativeForkApp(int readPipeFD, * read more * @param zygoteSocket socket from which to obtain new connections when current argBuffer * one is disconnected - * @param expectedUId Uid of peer for initial requests. Subsequent requests from a different + * @param expectedUid Uid of peer for initial requests. Subsequent requests from a different * peer will cause us to return rather than perform the requested fork. * @param minUid Minimum Uid enforced for all but first fork request. The caller checks * the Uid policy for the initial request. @@ -885,7 +874,7 @@ private static Runnable childMain(@Nullable ZygoteCommandBuffer argBuffer, } } - specializeAppProcess(args.mUid, args.mGid, args.mGids, + specializeAppProcess(args.mExtraArgs, args.mUid, args.mGid, args.mGids, args.mRuntimeFlags, rlimits, args.mMountExternal, args.mSeInfo, args.mNiceName, args.mStartChildZygote, args.mInstructionSet, args.mAppDataDir, args.mIsTopApp, @@ -1180,6 +1169,8 @@ private static void callPostForkSystemServerHooks(int runtimeFlags) { @SuppressWarnings("unused") private static void callPostForkChildHooks(int runtimeFlags, boolean isSystemServer, boolean isZygote, String instructionSet) { + android.os.Binder.onZygotePostForkChild(); + android.os.BinderProxy.onZygotePostForkChild(); ZygoteHooks.postForkChild(runtimeFlags, isSystemServer, isZygote, instructionSet); } @@ -1373,6 +1364,7 @@ private static int getRequestedMemtagLevel( } private static int decideTaggingLevel( + @NonNull AtomicBoolean shouldForciblyEnableTagging, @NonNull ApplicationInfo info, @Nullable ProcessInfo processInfo, @Nullable IPlatformCompat platformCompat) { @@ -1381,6 +1373,36 @@ private static int decideTaggingLevel( // Take into account the hardware capabilities. if (nativeSupportsMemoryTagging()) { + Context ctx = AppGlobals.getInitialApplication(); + int userId = UserHandle.getUserId(info.uid); + GosPackageState ps = GosPackageState.get(info.packageName, userId); + + var si = new AppSwitch.StateInfo(); + if (AswUseMemoryTagging.I.get(ctx, userId, info, ps, si)) { + level = MEMORY_TAG_LEVEL_ASYNC; + + if (!si.isImmutable()) { + shouldForciblyEnableTagging.set(true); + // This option prevents the app from downgrading the heap memory tagging level and + // from intercepting MTE SIGSEGV signal (it's used for crashing the process + // after tag check failure). + // + // It's intended for apps that are not aware of memory tagging and do not attempt + // to actively disable or circumvent it. + // + // Apps might be incompatible with this approach for several reasons (the list + // is not complete): + // - app stores custom data in top pointer byte (it's used for pointer tagging) + // - app relies on a MTE SIGSEGV handler in its bundled runtime + // - app uses a custom memory allocator that ignores current memory tagging level + // - app manually disables tag mismatch checks + // + // '!si.isImmutable()' check ensures that this flag gets enabled only for those + // apps for which memory tagging can be manually disabled, i.e. third-party + // apps with custom native code that haven't opted-in to memory tagging + } + } + // MTE devices can not do TBI, because the Zygote process already has live MTE // allocations. Downgrade TBI to NONE. if (level == MEMORY_TAG_LEVEL_TBI) { @@ -1460,6 +1482,7 @@ private static boolean enableNativeHeapZeroInit( * for a given app. */ public static int getMemorySafetyRuntimeFlags( + @NonNull AtomicBoolean shouldForciblyEnableTagging, @NonNull ApplicationInfo info, @Nullable ProcessInfo processInfo, @Nullable String instructionSet, @@ -1476,7 +1499,7 @@ public static int getMemorySafetyRuntimeFlags( // fine as we haven't seen this configuration in practice, and we can reasonable assume // that if tagging is desired, the system server will be 64-bit. if (instructionSet == null || instructionSet.equals("arm64")) { - runtimeFlags |= decideTaggingLevel(info, processInfo, platformCompat); + runtimeFlags |= decideTaggingLevel(shouldForciblyEnableTagging, info, processInfo, platformCompat); } if (enableNativeHeapZeroInit(info, processInfo, platformCompat)) { runtimeFlags |= NATIVE_HEAP_ZERO_INIT_ENABLED; @@ -1494,9 +1517,12 @@ public static int getMemorySafetyRuntimeFlagsForSecondaryZygote( final IPlatformCompat platformCompat = IPlatformCompat.Stub.asInterface( ServiceManager.getService(Context.PLATFORM_COMPAT_SERVICE)); + var shouldForciblyEnableTagging = new AtomicBoolean(); int runtimeFlags = - getMemorySafetyRuntimeFlags( + getMemorySafetyRuntimeFlags(shouldForciblyEnableTagging, info, processInfo, null /*instructionSet*/, platformCompat); + // the value of shouldForciblyEnableTagging is intentionally ignored since it's determined + // already at an earlier point in ProcessList.startLocked() // TBI ("fake" pointer tagging) in AppZygote is controlled by a separate compat feature. if ((runtimeFlags & MEMORY_TAG_LEVEL_MASK) == MEMORY_TAG_LEVEL_TBI @@ -1511,4 +1537,13 @@ && isCompatChangeEnabled( } return runtimeFlags; } + + /** + * Used on GrapheneOS to set up runtime flags + * + * @param runtimeFlags flags to be passed to the native method + * + * @hide + */ + public static native void nativeHandleRuntimeFlags(int runtimeFlags); } diff --git a/core/java/com/android/internal/os/ZygoteArguments.java b/core/java/com/android/internal/os/ZygoteArguments.java index e84b3fc2ca833..d1b5f6bc85571 100644 --- a/core/java/com/android/internal/os/ZygoteArguments.java +++ b/core/java/com/android/internal/os/ZygoteArguments.java @@ -16,6 +16,9 @@ package com.android.internal.os; +import android.os.Parcel; +import android.os.Parcelable; + import java.io.EOFException; import java.util.ArrayList; import java.util.Arrays; @@ -55,7 +58,141 @@ * @hide */ @RavenwoodKeepPartialClass -public class ZygoteArguments { +public class ZygoteArguments implements Parcelable { + + @Override + public int describeContents() { + return 0; + } + + private static boolean checkedFieldCount; + + @Override + public void writeToParcel(Parcel p, int flags) { + if (android.os.Flags.isDevBuild() && !checkedFieldCount) { + int numFields = ZygoteArguments.class.getDeclaredFields().length; + int expectedFields = 45; + if (numFields != expectedFields) { + throw new IllegalStateException("expected " + expectedFields + " fields, got " + numFields); + } + checkedFieldCount = true; + } + p.writeInt(mUid); + p.writeBoolean(mUidSpecified); + p.writeInt(mGid); + p.writeBoolean(mGidSpecified); + p.writeIntArray(mGids); + p.writeInt(mRuntimeFlags); + p.writeParcelable(mExtraArgs, 0); + p.writeInt(mMountExternal); + p.writeBoolean(mTargetSdkVersionSpecified); + p.writeInt(mTargetSdkVersion); + p.writeString8(mNiceName); + p.writeBoolean(mCapabilitiesSpecified); + p.writeLong(mPermittedCapabilities); + p.writeLong(mEffectiveCapabilities); + p.writeBoolean(mSeInfoSpecified); + p.writeString8(mSeInfo); + p.writeBoolean(mUsapPoolEnabled); + p.writeBoolean(mUsapPoolStatusSpecified); + int rlimitsListSize = mRLimits != null ? mRLimits.size() : -1; + p.writeInt(rlimitsListSize); + if (rlimitsListSize > 0) { + for (int i = 0; i < rlimitsListSize; ++i) { + p.writeIntArray(mRLimits.get(i)); + } + } + p.writeString8(mInvokeWith); + p.writeString8(mPackageName); + p.writeString8Array(mRemainingArgs); + p.writeBoolean(mAbiListQuery); + p.writeString8(mInstructionSet); + p.writeString8(mAppDataDir); + p.writeString8(mPreloadApp); + p.writeBoolean(mPreloadDefault); + p.writeBoolean(mStartChildZygote); + p.writeBoolean(mPidQuery); + p.writeBoolean(mBootCompleted); + p.writeString8Array(mApiDenylistExemptions); + p.writeInt(mHiddenApiAccessLogSampleRate); + p.writeInt(mHiddenApiAccessStatslogSampleRate); + p.writeBoolean(mIsTopApp); + p.writeLongArray(mDisabledCompatChanges); + p.writeLongArray(mEnabledCompatChanges); + p.writeString8Array(mPkgDataInfoList); + p.writeString8Array(mAllowlistedDataInfoList); + p.writeBoolean(mBindMountAppStorageDirs); + p.writeBoolean(mBindMountAppDataDirs); + p.writeBoolean(mBindMountSyspropOverrides); + p.writeBoolean(mAppDataIsolationEnabled); + p.writeString8(mPreloadPackage); + } + + private ZygoteArguments(Parcel p) { + mUid = p.readInt(); + mUidSpecified = p.readBoolean(); + mGid = p.readInt(); + mGidSpecified = p.readBoolean(); + mGids = p.createIntArray(); + mRuntimeFlags = p.readInt(); + mExtraArgs = p.readParcelable(ZygoteExtraArgs.class.getClassLoader(), ZygoteExtraArgs.class); + mMountExternal = p.readInt(); + mTargetSdkVersionSpecified = p.readBoolean(); + mTargetSdkVersion = p.readInt(); + mNiceName = p.readString8(); + mCapabilitiesSpecified = p.readBoolean(); + mPermittedCapabilities = p.readLong(); + mEffectiveCapabilities = p.readLong(); + mSeInfoSpecified = p.readBoolean(); + mSeInfo = p.readString8(); + mUsapPoolEnabled = p.readBoolean(); + mUsapPoolStatusSpecified = p.readBoolean(); + int rlimitsListSize = p.readInt(); + if (rlimitsListSize == -1) { + mRLimits = null; + } else { + mRLimits = new ArrayList<>(rlimitsListSize); + for (int i = 0; i < rlimitsListSize; ++i) { + mRLimits.add(p.createIntArray()); + } + } + mInvokeWith = p.readString8(); + mPackageName = p.readString8(); + mRemainingArgs = p.createString8Array(); + mAbiListQuery = p.readBoolean(); + mInstructionSet = p.readString8(); + mAppDataDir = p.readString8(); + mPreloadApp = p.readString8(); + mPreloadDefault = p.readBoolean(); + mStartChildZygote = p.readBoolean(); + mPidQuery = p.readBoolean(); + mBootCompleted = p.readBoolean(); + mApiDenylistExemptions = p.createString8Array(); + mHiddenApiAccessLogSampleRate = p.readInt(); + mHiddenApiAccessStatslogSampleRate = p.readInt(); + mIsTopApp = p.readBoolean(); + mDisabledCompatChanges = p.createLongArray(); + mEnabledCompatChanges = p.createLongArray(); + mPkgDataInfoList = p.createString8Array(); + mAllowlistedDataInfoList = p.createString8Array(); + mBindMountAppStorageDirs = p.readBoolean(); + mBindMountAppDataDirs = p.readBoolean(); + mBindMountSyspropOverrides = p.readBoolean(); + mAppDataIsolationEnabled = p.readBoolean(); + mPreloadPackage = p.readString8(); + } + + public static final Parcelable.Creator CREATOR = new Creator<>() { + @Override + public ZygoteArguments createFromParcel(Parcel source) { + return new ZygoteArguments(source); + } + + @Override + public ZygoteArguments[] newArray(int size) { + return new ZygoteArguments[size]; + } + }; /** * from --setuid @@ -79,6 +216,11 @@ public class ZygoteArguments { */ int mRuntimeFlags; + /** + * From --gos-extra-args + */ + ZygoteExtraArgs mExtraArgs = ZygoteExtraArgs.DEFAULT; + /** * From --mount-external */ @@ -327,6 +469,14 @@ private void parseArgs(ZygoteCommandBuffer args, int argCount) seenRuntimeArgs = true; } else if (arg.startsWith("--runtime-flags=")) { mRuntimeFlags = Integer.parseInt(getAssignmentValue(arg)); + } else if (arg.equals(ZygoteExtraArgs.ARG_COMPLEX_COMMAND_MARKER)) { + // ignored here, this arg is needed only for isSimpleForkCommand() in + // core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp + } else if (arg.startsWith(ZygoteExtraArgs.ARG_PREFIX)) { + mExtraArgs = ZygoteExtraArgs.parse(getAssignmentValue(arg)); + if (mExtraArgs.getNativeAppLaunchCommand() != null) { + expectRuntimeArgs = false; + } } else if (arg.startsWith("--seinfo=")) { if (mSeInfoSpecified) { throw new IllegalArgumentException( @@ -609,4 +759,63 @@ static long[] parseAndMergeCompatChanges(String arg, long[] existingChanges) { } return merged; } + + @Override + public String toString() { + var b = new StringBuilder("ZygoteArguments{"); + b.append("mUid=").append(mUid); + b.append(", mUidSpecified=").append(mUidSpecified); + b.append(", mGid=").append(mGid); + b.append(", mGidSpecified=").append(mGidSpecified); + b.append(", mGids=").append(Arrays.toString(mGids)); + b.append(", mRuntimeFlags=").append(Integer.toHexString(mRuntimeFlags)); + b.append(", mExtraArgs=").append(mExtraArgs); + b.append(", mMountExternal=").append(mMountExternal); + b.append(", mTargetSdkVersionSpecified=").append(mTargetSdkVersionSpecified); + b.append(", mTargetSdkVersion=").append(mTargetSdkVersion); + b.append(", mNiceName=").append(mNiceName); + b.append(", mCapabilitiesSpecified=").append(mCapabilitiesSpecified); + b.append(", mPermittedCapabilities=").append(mPermittedCapabilities); + b.append(", mEffectiveCapabilities=").append(mEffectiveCapabilities); + b.append(", mSeInfoSpecified=").append(mSeInfoSpecified); + b.append(", mSeInfo=").append(mSeInfo); + b.append(", mUsapPoolEnabled=").append(mUsapPoolEnabled); + b.append(", mUsapPoolStatusSpecified=").append(mUsapPoolStatusSpecified); + if (mRLimits == null) { + b.append(", mRLimits=null"); + } else { + b.append(", mRLimits=["); + for (int i = 0; i < mRLimits.size(); i++) { + b.append(Arrays.toString(mRLimits.get(i))); + if (i < mRLimits.size() - 1) b.append(", "); + } + b.append("]"); + } + b.append(", mInvokeWith=").append(mInvokeWith); + b.append(", mPackageName=").append(mPackageName); + b.append(", mRemainingArgs=").append(Arrays.toString(mRemainingArgs)); + b.append(", mAbiListQuery=").append(mAbiListQuery); + b.append(", mInstructionSet=").append(mInstructionSet); + b.append(", mAppDataDir=").append(mAppDataDir); + b.append(", mPreloadApp=").append(mPreloadApp); + b.append(", mPreloadDefault=").append(mPreloadDefault); + b.append(", mStartChildZygote=").append(mStartChildZygote); + b.append(", mPidQuery=").append(mPidQuery); + b.append(", mBootCompleted=").append(mBootCompleted); + b.append(", mApiDenylistExemptions=").append(Arrays.toString(mApiDenylistExemptions)); + b.append(", mHiddenApiAccessLogSampleRate=").append(mHiddenApiAccessLogSampleRate); + b.append(", mHiddenApiAccessStatslogSampleRate=").append(mHiddenApiAccessStatslogSampleRate); + b.append(", mIsTopApp=").append(mIsTopApp); + b.append(", mDisabledCompatChanges=").append(Arrays.toString(mDisabledCompatChanges)); + b.append(", mEnabledCompatChanges=").append(Arrays.toString(mEnabledCompatChanges)); + b.append(", mPkgDataInfoList=").append(Arrays.toString(mPkgDataInfoList)); + b.append(", mAllowlistedDataInfoList=").append(Arrays.toString(mAllowlistedDataInfoList)); + b.append(", mBindMountAppStorageDirs=").append(mBindMountAppStorageDirs); + b.append(", mBindMountAppDataDirs=").append(mBindMountAppDataDirs); + b.append(", mBindMountSyspropOverrides=").append(mBindMountSyspropOverrides); + b.append(", mAppDataIsolationEnabled=").append(mAppDataIsolationEnabled); + b.append(", mPreloadPackage=").append(mPreloadPackage); + b.append('}'); + return b.toString(); + } } diff --git a/core/java/com/android/internal/os/ZygoteCommandRecorder.java b/core/java/com/android/internal/os/ZygoteCommandRecorder.java new file mode 100644 index 0000000000000..a94e4a0ba45f1 --- /dev/null +++ b/core/java/com/android/internal/os/ZygoteCommandRecorder.java @@ -0,0 +1,94 @@ +package com.android.internal.os; + +import android.os.Parcel; +import android.util.Log; + +import java.util.LinkedHashMap; +import java.util.Objects; + +import static com.android.internal.util.Preconditions.checkState; + +// Records zygote commands that have to be replayed in exec-spawned zygote as part of exec spawning +class ZygoteCommandRecorder { + private static final String TAG = "ZygoteCommandRecorder"; + private static boolean isEnabled; + private static boolean is64bit; + + enum CommandType { + BootCompleted, + ApiDenylistExemptions, + HiddenApiAccessLogSampleRate, + } + + // intentionally using an insertion-ordered map + private static LinkedHashMap capturedCommands; + + static void enable(boolean is64bit) { + Log.d(TAG, "enabled, is64bit: " + is64bit); + checkState(!isEnabled); + isEnabled = true; + ZygoteCommandRecorder.is64bit = is64bit; + capturedCommands = new LinkedHashMap<>(); + } + + static boolean is64bit() { + return is64bit; + } + + static void maybeAddReplayCommand(CommandType type, ZygoteArguments cmd) { + if (!isEnabled) { + return; + } + Objects.requireNonNull(cmd); + + if (ExecSpawning.isExecSpawnedProcess()) { + return; + } + + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "maybeAddReplayCommand: added " + type + ": " + cmd, new Throwable()); + } + + capturedCommands.put(type, cmd); + } + + static byte[] addFinalCommandAndSerialize(ZygoteArguments finalCmd) { + checkState(isEnabled); + int numReplayCmds = capturedCommands.size(); + + if (Log.isLoggable(TAG, Log.VERBOSE)) { + Log.v(TAG, "addFinalCommandAndSerialize: " + finalCmd); + } + + var p = Parcel.obtain(); + byte[] serializedCommands; + try { + p.writeInt(numReplayCmds); + for (ZygoteArguments cmd : capturedCommands.values()) { + cmd.writeToParcel(p, 0); + } + finalCmd.writeToParcel(p, 0); + serializedCommands = p.marshall(); + } finally { + p.recycle(); + } + return serializedCommands; + } + + static ZygoteArguments[] deserializeCommands(byte[] data) { + Parcel p = Parcel.obtain(); + try { + p.unmarshall(data, 0, data.length); + p.setDataPosition(0); + int numReplayCmds = p.readInt(); + ZygoteArguments[] commands = new ZygoteArguments[numReplayCmds + 1]; + for (int i = 0; i < numReplayCmds; ++i) { + commands[i] = ZygoteArguments.CREATOR.createFromParcel(p); + } + commands[numReplayCmds] = ZygoteArguments.CREATOR.createFromParcel(p); + return commands; + } finally { + p.recycle(); + } + } +} diff --git a/core/java/com/android/internal/os/ZygoteConnection.java b/core/java/com/android/internal/os/ZygoteConnection.java index 6fa5a27b66306..bf37ef6309179 100644 --- a/core/java/com/android/internal/os/ZygoteConnection.java +++ b/core/java/com/android/internal/os/ZygoteConnection.java @@ -22,7 +22,9 @@ import static com.android.internal.os.ZygoteConnectionConstants.CONNECTION_TIMEOUT_MILLIS; import static com.android.internal.os.ZygoteConnectionConstants.WRAPPED_PID_TIMEOUT_MILLIS; +import static com.android.internal.util.Preconditions.checkState; +import android.annotation.Nullable; import android.compat.annotation.UnsupportedAppUsage; import android.content.pm.ApplicationInfo; import android.net.Credentials; @@ -35,6 +37,8 @@ import android.system.StructPollfd; import android.util.Log; +import com.android.internal.os.ZygoteCommandRecorder.CommandType; + import dalvik.system.VMRuntime; import dalvik.system.ZygoteHooks; @@ -45,8 +49,10 @@ import java.io.DataOutputStream; import java.io.FileDescriptor; import java.io.IOException; +import java.io.OutputStream; import java.nio.charset.StandardCharsets; import java.util.Base64; +import java.util.Objects; import java.util.concurrent.TimeUnit; /** @@ -82,6 +88,12 @@ class ZygoteConnection { mSocket = socket; this.abiList = abiList; + if (ExecSpawning.isReplayingZygoteCommands()) { + mSocketOutStream = new DataOutputStream(OutputStream.nullOutputStream()); + peer = null; + return; + } + mSocketOutStream = new DataOutputStream(socket.getOutputStream()); mSocket.setSoTimeout(CONNECTION_TIMEOUT_MILLIS); @@ -117,18 +129,27 @@ FileDescriptor getFileDescriptor() { * If the client closes the socket, an {@code EOF} condition is set, which callers can test * for by calling {@code ZygoteConnection.isClosedByPeer}. */ - Runnable processCommand(ZygoteServer zygoteServer, boolean multipleOK) { + Runnable processCommand(ZygoteServer zygoteServer, boolean multipleOK, @Nullable ZygoteArguments command) { ZygoteArguments parsedArgs; - try (ZygoteCommandBuffer argBuffer = new ZygoteCommandBuffer(mSocket)) { + boolean isReplayingZygoteCommands = ExecSpawning.isReplayingZygoteCommands(); + + if (!isReplayingZygoteCommands) { + checkState(command == null); + } + + try (ZygoteCommandBuffer argBuffer = + command != null ? null : + new ZygoteCommandBuffer(mSocket)) { while (true) { try { - parsedArgs = ZygoteArguments.getInstance(argBuffer); + parsedArgs = command != null ? command : ZygoteArguments.getInstance(argBuffer); // Keep argBuffer around, since we need it to fork. } catch (IOException ex) { throw new IllegalStateException("IOException on command socket", ex); } if (parsedArgs == null) { + checkState(!isReplayingZygoteCommands); isEof = true; return null; } @@ -138,16 +159,19 @@ Runnable processCommand(ZygoteServer zygoteServer, boolean multipleOK) { FileDescriptor serverPipeFd = null; if (parsedArgs.mBootCompleted) { + ZygoteCommandRecorder.maybeAddReplayCommand(CommandType.BootCompleted, parsedArgs); handleBootCompleted(); return null; } if (parsedArgs.mAbiListQuery) { + checkState(!isReplayingZygoteCommands); handleAbiListQuery(); return null; } if (parsedArgs.mPidQuery) { + checkState(!isReplayingZygoteCommands); handlePidQuery(); return null; } @@ -161,11 +185,13 @@ Runnable processCommand(ZygoteServer zygoteServer, boolean multipleOK) { } if (parsedArgs.mPreloadDefault) { + checkState(!isReplayingZygoteCommands); handlePreload(); return null; } if (canPreloadApp() && parsedArgs.mPreloadApp != null) { + checkState(!isReplayingZygoteCommands); byte[] rawParcelData = Base64.getDecoder().decode(parsedArgs.mPreloadApp); Parcel appInfoParcel = Parcel.obtain(); appInfoParcel.unmarshall(rawParcelData, 0, rawParcelData.length); @@ -189,11 +215,13 @@ Runnable processCommand(ZygoteServer zygoteServer, boolean multipleOK) { + Long.toHexString(parsedArgs.mEffectiveCapabilities)); } - Zygote.applyUidSecurityPolicy(parsedArgs, peer); - Zygote.applyInvokeWithSecurityPolicy(parsedArgs, peer); + if (!isReplayingZygoteCommands) { + Zygote.applyUidSecurityPolicy(parsedArgs, peer); + Zygote.applyInvokeWithSecurityPolicy(parsedArgs, peer); - Zygote.applyDebuggerSystemProperty(parsedArgs); - Zygote.applyInvokeWithSystemProperty(parsedArgs); + Zygote.applyDebuggerSystemProperty(parsedArgs); + Zygote.applyInvokeWithSystemProperty(parsedArgs); + } int[][] rlimits = null; @@ -204,6 +232,7 @@ Runnable processCommand(ZygoteServer zygoteServer, boolean multipleOK) { int[] fdsToIgnore = null; if (parsedArgs.mInvokeWith != null) { + checkState(!isReplayingZygoteCommands); try { FileDescriptor[] pipeFds = Os.pipe2(O_CLOEXEC); childPipeFd = pipeFds[1]; @@ -230,36 +259,65 @@ Runnable processCommand(ZygoteServer zygoteServer, boolean multipleOK) { int [] fdsToClose = { -1, -1 }; - FileDescriptor fd = mSocket.getFileDescriptor(); + if (!isReplayingZygoteCommands) { // ZygoteServer sockets are null in exec spawned zygote + FileDescriptor fd = mSocket.getFileDescriptor(); - if (fd != null) { - fdsToClose[0] = fd.getInt$(); - } + if (fd != null) { + fdsToClose[0] = fd.getInt$(); + } - FileDescriptor zygoteFd = zygoteServer.getZygoteSocketFileDescriptor(); + FileDescriptor zygoteFd = zygoteServer.getZygoteSocketFileDescriptor(); - if (zygoteFd != null) { - fdsToClose[1] = zygoteFd.getInt$(); + if (zygoteFd != null) { + fdsToClose[1] = zygoteFd.getInt$(); + } } - if (parsedArgs.mInvokeWith != null || parsedArgs.mStartChildZygote + boolean shouldUseExecSpawning = !isReplayingZygoteCommands + && parsedArgs.mExtraArgs.shouldUseExecSpawning() + && parsedArgs.mInvokeWith == null + && !parsedArgs.mStartChildZygote; + if (shouldUseExecSpawning || isReplayingZygoteCommands || parsedArgs.mInvokeWith != null || parsedArgs.mStartChildZygote || !multipleOK || peer.getUid() != Process.SYSTEM_UID) { - Log.w(TAG, "Resorting to Java fork code; multipleOK = " + multipleOK - + (parsedArgs.mInvokeWith != null ? "; invokeWith used" : "")); - // Continue using old code for now. TODO: Handle these cases in the other path. - pid = Zygote.forkAndSpecialize(parsedArgs.mUid, parsedArgs.mGid, - parsedArgs.mGids, parsedArgs.mRuntimeFlags, rlimits, - parsedArgs.mMountExternal, parsedArgs.mSeInfo, parsedArgs.mNiceName, - fdsToClose, fdsToIgnore, parsedArgs.mStartChildZygote, - parsedArgs.mInstructionSet, parsedArgs.mAppDataDir, - parsedArgs.mIsTopApp, parsedArgs.mPkgDataInfoList, - parsedArgs.mAllowlistedDataInfoList, parsedArgs.mBindMountAppDataDirs, - parsedArgs.mBindMountAppStorageDirs, - parsedArgs.mBindMountSyspropOverrides); + + if (shouldUseExecSpawning) { + final byte[] commands; + byte[] nativeAppLaunchCmd = parsedArgs.mExtraArgs.getNativeAppLaunchCommand(); + if (nativeAppLaunchCmd != null) { + commands = nativeAppLaunchCmd; + } else { + commands = ZygoteCommandRecorder.addFinalCommandAndSerialize(parsedArgs); + } + pid = Zygote.nativeForkExec( + ZygoteCommandRecorder.is64bit(), + nativeAppLaunchCmd != null, + commands, + parsedArgs.mExtraArgs.hasFlag(ZygoteExtraArgs.Flag.DISABLE_HARDENED_MALLOC), + parsedArgs.mExtraArgs.hasFlag(ZygoteExtraArgs.Flag.ENABLE_COMPAT_VA_39_BIT)); + } else { + if (isReplayingZygoteCommands) { + if (parsedArgs.mExtraArgs.hasFlag(ZygoteExtraArgs.Flag.MANUALLY_RUN_ZYGOTE_PRELOAD)) { + ExecSpawning.addPendingZygotePreload(); + } + } + Log.d(TAG, "Resorting to Java fork code; isReplayingZygoteCommands: " + isReplayingZygoteCommands + " multipleOK = " + multipleOK + + (parsedArgs.mInvokeWith != null ? "; invokeWith used" : "")); + // Continue using old code for now. TODO: Handle these cases in the other path. + pid = Zygote.forkAndSpecialize(parsedArgs.mExtraArgs, parsedArgs.mUid, parsedArgs.mGid, + parsedArgs.mGids, parsedArgs.mRuntimeFlags, rlimits, + parsedArgs.mMountExternal, parsedArgs.mSeInfo, parsedArgs.mNiceName, + fdsToClose, fdsToIgnore, parsedArgs.mStartChildZygote, + parsedArgs.mInstructionSet, parsedArgs.mAppDataDir, + parsedArgs.mIsTopApp, parsedArgs.mPkgDataInfoList, + parsedArgs.mAllowlistedDataInfoList, parsedArgs.mBindMountAppDataDirs, + parsedArgs.mBindMountAppStorageDirs, + parsedArgs.mBindMountSyspropOverrides); + } try { if (pid == 0) { // in child + checkState(!shouldUseExecSpawning); // should never be reached zygoteServer.setForkChild(); zygoteServer.closeServerSocket(); @@ -269,6 +327,7 @@ Runnable processCommand(ZygoteServer zygoteServer, boolean multipleOK) { return handleChildProc(parsedArgs, childPipeFd, parsedArgs.mStartChildZygote); } else { + checkState(!isReplayingZygoteCommands); // In the parent. A pid < 0 indicates a failure and will be handled in // handleParentProc. IoUtils.closeQuietly(childPipeFd); @@ -281,6 +340,7 @@ Runnable processCommand(ZygoteServer zygoteServer, boolean multipleOK) { IoUtils.closeQuietly(serverPipeFd); } } else { + Objects.requireNonNull(argBuffer); ZygoteHooks.preFork(); Runnable result = Zygote.forkSimpleApps(argBuffer, zygoteServer.getZygoteSocketFileDescriptor(), @@ -302,14 +362,17 @@ Runnable processCommand(ZygoteServer zygoteServer, boolean multipleOK) { } // Handle anything that may need a ZygoteCommandBuffer after we've released ours. if (parsedArgs.mUsapPoolStatusSpecified) { + checkState(!isReplayingZygoteCommands); return handleUsapPoolStatusChange(zygoteServer, parsedArgs.mUsapPoolEnabled); } if (parsedArgs.mApiDenylistExemptions != null) { + ZygoteCommandRecorder.maybeAddReplayCommand(CommandType.ApiDenylistExemptions, parsedArgs); return handleApiDenylistExemptions(zygoteServer, parsedArgs.mApiDenylistExemptions); } if (parsedArgs.mHiddenApiAccessLogSampleRate != -1 || parsedArgs.mHiddenApiAccessStatslogSampleRate != -1) { + ZygoteCommandRecorder.maybeAddReplayCommand(CommandType.HiddenApiAccessLogSampleRate, parsedArgs); return handleHiddenApiAccessLogSampleRate(zygoteServer, parsedArgs.mHiddenApiAccessLogSampleRate, parsedArgs.mHiddenApiAccessStatslogSampleRate); @@ -319,6 +382,7 @@ Runnable processCommand(ZygoteServer zygoteServer, boolean multipleOK) { private void handleAbiListQuery() { try { + Log.d(TAG, "handleAbiListQuery"); final byte[] abiListBytes = abiList.getBytes(StandardCharsets.US_ASCII); mSocketOutStream.writeInt(abiListBytes.length); mSocketOutStream.write(abiListBytes); @@ -339,6 +403,7 @@ private void handlePidQuery() { } private void handleBootCompleted() { + Log.d(TAG, "handleBootCompleted"); try { mSocketOutStream.writeInt(0); } catch (IOException ioe) { @@ -483,11 +548,15 @@ protected void handlePreloadApp(ApplicationInfo aInfo) { */ @UnsupportedAppUsage void closeSocket() { - try { - mSocket.close(); - } catch (IOException ex) { - Log.e(TAG, "Exception while closing command " - + "socket in parent", ex); + if (ExecSpawning.isReplayingZygoteCommands()) { + checkState(mSocket == null); + } else { + try { + mSocket.close(); + } catch (IOException ex) { + Log.e(TAG, "Exception while closing command " + + "socket in parent", ex); + } } } diff --git a/core/java/com/android/internal/os/ZygoteExtraArgs.java b/core/java/com/android/internal/os/ZygoteExtraArgs.java new file mode 100644 index 0000000000000..4a8b74a7ddee2 --- /dev/null +++ b/core/java/com/android/internal/os/ZygoteExtraArgs.java @@ -0,0 +1,202 @@ +package com.android.internal.os; + +import android.annotation.IntDef; +import android.annotation.Nullable; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.GosPackageState; +import android.ext.settings.app.AswUseExecSpawning; +import android.ext.settings.app.AswUseExtendedVaSpace; +import android.ext.settings.app.AswUseHardenedMalloc; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.ZygoteSelectionMode; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.HexFormat; + +public class ZygoteExtraArgs implements Parcelable { + private long selinuxFlags; + private int flags; + @Nullable private byte[] nativeAppLaunchCommand; + + public static final String ARG_PREFIX = "--gos-extra-args="; + // checked by isSimpleForkCommand() in core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp + public static final String ARG_COMPLEX_COMMAND_MARKER = "--is-complex-zygote-command"; + + public static final ZygoteExtraArgs DEFAULT = new ZygoteExtraArgs(); + + private ZygoteExtraArgs() {} + + // keep in sync with ExtraArgsFlag in core/jni/com_android_internal_os_Zygote.cpp + public interface Flag { + // hardened_malloc is always disabled when PREFER_COMPAT_ZYGOTE is set + int DISABLE_HARDENED_MALLOC = 1; + // 39-bit is available only on arm64 and is always enabled when PREFER_COMPAT_ZYGOTE is set + int ENABLE_COMPAT_VA_39_BIT = 1 << 1; + int FORCIBLY_ENABLE_MEMORY_TAGGING = 1 << 2; + int USE_ZYGOTE_SPAWNING = 1 << 3; + int PREFER_COMPAT_ZYGOTE = 1 << 4; + int MANUALLY_RUN_ZYGOTE_PRELOAD = 1 << 5; + + @IntDef(flag = true, value = { + DISABLE_HARDENED_MALLOC, + ENABLE_COMPAT_VA_39_BIT, + FORCIBLY_ENABLE_MEMORY_TAGGING, + PREFER_COMPAT_ZYGOTE, + MANUALLY_RUN_ZYGOTE_PRELOAD, + }) + @Retention(RetentionPolicy.SOURCE) + @interface Enum {} + } + + public static ZygoteExtraArgs create(Context ctx, int userId, ApplicationInfo appInfo, + boolean shouldForciblyEnableMemoryTagging, + GosPackageState ps, + boolean isIsolatedProcess) { + var res = new ZygoteExtraArgs(); + res.selinuxFlags = SELinuxFlags.get(ctx, userId, appInfo, ps, isIsolatedProcess); + boolean useZygoteSpawning = !AswUseExecSpawning.I.get(ctx, userId, appInfo, ps); + if (useZygoteSpawning) { + res.setFlag(Flag.USE_ZYGOTE_SPAWNING, true); + if (!AswUseHardenedMalloc.I.get(ctx, userId, appInfo, ps)) { + res.setFlag(Flag.PREFER_COMPAT_ZYGOTE, true); + } + } else { + res.setFlag(Flag.DISABLE_HARDENED_MALLOC, !AswUseHardenedMalloc.I.get(ctx, userId, appInfo, ps)); + res.setFlag(Flag.ENABLE_COMPAT_VA_39_BIT, !AswUseExtendedVaSpace.I.get(ctx, userId, appInfo, ps)); + } + res.setFlag(Flag.FORCIBLY_ENABLE_MEMORY_TAGGING, shouldForciblyEnableMemoryTagging); + return res; + } + + public static ZygoteExtraArgs createForWebviewZygote() { + return DEFAULT; + } + + public static ZygoteExtraArgs createForWebviewProcess(Context ctx, int userId, + ApplicationInfo callerAppInfo, GosPackageState callerPs) { + var res = new ZygoteExtraArgs(); + res.selinuxFlags = SELinuxFlags.getForWebViewProcess(ctx, userId, callerAppInfo, callerPs); + res.setFlag(Flag.USE_ZYGOTE_SPAWNING, !AswUseExecSpawning.I.get(ctx, userId, callerAppInfo, callerPs)); + return res; + } + + public ZygoteSelectionMode getZygoteSelectionMode() { + return hasFlag(Flag.PREFER_COMPAT_ZYGOTE) ? + ZygoteSelectionMode.PreferCompatZygote : + ZygoteSelectionMode.Regular; + } + + public boolean hasFlag(@Flag.Enum int flag) { + return (this.flags & flag) == flag; + } + + public void setFlag(int flag, boolean value) { + if (value) { + this.flags |= flag; + } else { + this.flags &= ~flag; + } + } + + public long getSelinuxFlags() { + return selinuxFlags; + } + + @Nullable + public byte[] getNativeAppLaunchCommand() { + return nativeAppLaunchCommand; + } + + public void setNativeAppLaunchCommand(byte[] value) { + nativeAppLaunchCommand = value; + } + + public boolean shouldUseExecSpawning() { + return !hasFlag(Flag.USE_ZYGOTE_SPAWNING); + } + + static ZygoteExtraArgs parse(String argValue) { + byte[] serialized = HexFormat.of().parseHex(argValue); + Parcel p = Parcel.obtain(); + try { + p.unmarshall(serialized, 0, serialized.length); + p.setDataPosition(0); + return new ZygoteExtraArgs(p); + } finally { + p.recycle(); + } + } + + public void toZygoteArgList(ArrayList argsForZygote) { + if (shouldUseExecSpawning()) { + argsForZygote.add(ARG_COMPLEX_COMMAND_MARKER); + } + byte[] serialized; + Parcel p = Parcel.obtain(); + try { + writeToParcel(p, 0); + serialized = p.marshall(); + } finally { + p.recycle(); + } + argsForZygote.add(ARG_PREFIX + HexFormat.of().formatHex(serialized)); + } + + // this field is intentionally not serialized, it's stored here only for passing its value + // throughout the system_server codepath + private ApplicationInfo preloadAppInfo; + + public void setPreloadAppInfo(ApplicationInfo appInfo) { + preloadAppInfo = appInfo; + } + + public ApplicationInfo getPreloadAppInfo() { + return preloadAppInfo; + } + + // keep in sync with ExtraArgs struct in core/jni/com_android_internal_os_Zygote.cpp + private static final int IDX_SELINUX_FLAGS = 0; + private static final int IDX_FLAGS = 1; + private static final int ARR_LEN = 2; + + public long[] makeJniLongArray() { + long[] res = new long[ARR_LEN]; + res[IDX_SELINUX_FLAGS] = selinuxFlags; + res[IDX_FLAGS] = flags; + return res; + } + + @Override + public void writeToParcel(Parcel p, int parcelFlags) { + p.writeLong(this.selinuxFlags); + p.writeInt(this.flags); + p.writeByteArray(this.nativeAppLaunchCommand); + } + + ZygoteExtraArgs(Parcel p) { + selinuxFlags = p.readLong(); + flags = p.readInt(); + nativeAppLaunchCommand = p.createByteArray(); + } + + public static final Parcelable.Creator CREATOR = new Creator<>() { + @Override + public ZygoteExtraArgs createFromParcel(Parcel p) { + return new ZygoteExtraArgs(p); + } + + @Override + public ZygoteExtraArgs[] newArray(int size) { + return new ZygoteExtraArgs[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } +} diff --git a/core/java/com/android/internal/os/ZygoteInit.java b/core/java/com/android/internal/os/ZygoteInit.java index ccd664d12bc0e..495b8c0081d67 100644 --- a/core/java/com/android/internal/os/ZygoteInit.java +++ b/core/java/com/android/internal/os/ZygoteInit.java @@ -20,6 +20,7 @@ import static android.system.OsConstants.S_IRWXG; import static android.system.OsConstants.S_IRWXO; +import static com.android.internal.os.ExecSpawning.COMMAND_FD_ARG; import static com.android.internal.util.FrameworkStatsLog.BOOT_TIME_EVENT_ELAPSED_TIME__EVENT__SECONDARY_ZYGOTE_INIT_START; import static com.android.internal.util.FrameworkStatsLog.BOOT_TIME_EVENT_ELAPSED_TIME__EVENT__ZYGOTE_INIT_START; @@ -125,23 +126,31 @@ public class ZygoteInit { */ private static ClassLoader sCachedSystemServerClassLoader = null; - static void preload(TimingsTraceLog bootTimingsTraceLog) { + static void preload(TimingsTraceLog bootTimingsTraceLog, boolean fullPreload) { Log.d(TAG, "begin preload"); bootTimingsTraceLog.traceBegin("BeginPreload"); - beginPreload(); + beginPreload(fullPreload); bootTimingsTraceLog.traceEnd(); // BeginPreload - bootTimingsTraceLog.traceBegin("PreloadClasses"); - preloadClasses(); - bootTimingsTraceLog.traceEnd(); // PreloadClasses - bootTimingsTraceLog.traceBegin("CacheNonBootClasspathClassLoaders"); - cacheNonBootClasspathClassLoaders(); - bootTimingsTraceLog.traceEnd(); // CacheNonBootClasspathClassLoaders - bootTimingsTraceLog.traceBegin("PreloadResources"); - Resources.preloadResources(); - bootTimingsTraceLog.traceEnd(); // PreloadResources - Trace.traceBegin(Trace.TRACE_TAG_DALVIK, "PreloadAppProcessHALs"); - nativePreloadAppProcessHALs(); - Trace.traceEnd(Trace.TRACE_TAG_DALVIK); + if (fullPreload) { + bootTimingsTraceLog.traceBegin("PreloadClasses"); + preloadClasses(); + bootTimingsTraceLog.traceEnd(); // PreloadClasses + } + if (fullPreload) { + bootTimingsTraceLog.traceBegin("CacheNonBootClasspathClassLoaders"); + cacheNonBootClasspathClassLoaders(); + bootTimingsTraceLog.traceEnd(); // CacheNonBootClasspathClassLoaders + } + if (fullPreload) { + bootTimingsTraceLog.traceBegin("PreloadResources"); + Resources.preloadResources(); + bootTimingsTraceLog.traceEnd(); // PreloadResources + } + if (fullPreload) { + Trace.traceBegin(Trace.TRACE_TAG_DALVIK, "PreloadAppProcessHALs"); + nativePreloadAppProcessHALs(); + Trace.traceEnd(Trace.TRACE_TAG_DALVIK); + } Trace.traceBegin(Trace.TRACE_TAG_DALVIK, "PreloadGraphicsDriver"); maybePreloadGraphicsDriver(); Trace.traceEnd(Trace.TRACE_TAG_DALVIK); @@ -151,7 +160,7 @@ static void preload(TimingsTraceLog bootTimingsTraceLog) { // TODO: remove the try/catch and the flag read as soon as the flag is ramped and 25Q2 // starts building from source. - if (preloadHttpengineInZygote()) { + if (fullPreload && preloadHttpengineInZygote()) { try { HttpEngine.preload(); } catch (NoSuchMethodError e){ @@ -165,16 +174,30 @@ static void preload(TimingsTraceLog bootTimingsTraceLog) { Log.d(TAG, "HttpEngine.preload() threw " + e); } } - // Ask the WebViewFactory to do any initialization that must run in the zygote process, - // for memory sharing purposes. - WebViewFactory.prepareWebViewInZygote(); - endPreload(); - warmUpJcaProviders(); + if (fullPreload) { + // Ask the WebViewFactory to do any initialization that must run in the zygote process, + // for memory sharing purposes. + WebViewFactory.prepareWebViewInZygote(); + } + endPreload(fullPreload); + warmUpJcaProviders(fullPreload); + + if (!fullPreload) { + // run the privileged DisplayMetrics class initializer while the process is running in + // the zygote SELinux context + // noinspection unused + int val = android.util.DisplayMetrics.DENSITY_DEVICE_STABLE; + } + Log.d(TAG, "end preload"); sPreloadComplete = true; } + static void preload(TimingsTraceLog bootTimingsTraceLog) { + preload(bootTimingsTraceLog, true); + } + static void lazyPreload() { Preconditions.checkState(!sPreloadComplete); Log.i(TAG, "Lazily preloading resources."); @@ -182,14 +205,14 @@ static void lazyPreload() { preload(new TimingsTraceLog("ZygoteInitTiming_lazy", Trace.TRACE_TAG_DALVIK)); } - private static void beginPreload() { + private static void beginPreload(boolean fullPreload) { Log.i(TAG, "Calling ZygoteHooks.beginPreload()"); - ZygoteHooks.onBeginPreload(); + ZygoteHooks.onBeginPreload(fullPreload); } - private static void endPreload() { - ZygoteHooks.onEndPreload(); + private static void endPreload(boolean fullPreload) { + ZygoteHooks.onEndPreload(fullPreload); Log.i(TAG, "Called ZygoteHooks.endPreload()"); } @@ -242,7 +265,7 @@ private static void preloadCompatConfig() { * By doing it here we avoid that each app does it when requesting a service from the provider * for the first time. */ - private static void warmUpJcaProviders() { + private static void warmUpJcaProviders(boolean fullPreload) { long startTime = SystemClock.uptimeMillis(); Trace.traceBegin( Trace.TRACE_TAG_DALVIK, "Starting installation of AndroidKeyStoreProvider"); @@ -252,15 +275,17 @@ private static void warmUpJcaProviders() { + (SystemClock.uptimeMillis() - startTime) + "ms."); Trace.traceEnd(Trace.TRACE_TAG_DALVIK); - startTime = SystemClock.uptimeMillis(); - Trace.traceBegin( - Trace.TRACE_TAG_DALVIK, "Starting warm up of JCA providers"); - for (Provider p : Security.getProviders()) { - p.warmUpServiceProvision(); + if (fullPreload) { + startTime = SystemClock.uptimeMillis(); + Trace.traceBegin( + Trace.TRACE_TAG_DALVIK, "Starting warm up of JCA providers"); + for (Provider p : Security.getProviders()) { + p.warmUpServiceProvision(); + } + Log.i(TAG, "Warmed up JCA providers in " + + (SystemClock.uptimeMillis() - startTime) + "ms."); + Trace.traceEnd(Trace.TRACE_TAG_DALVIK); } - Log.i(TAG, "Warmed up JCA providers in " - + (SystemClock.uptimeMillis() - startTime) + "ms."); - Trace.traceEnd(Trace.TRACE_TAG_DALVIK); } private static boolean isExperimentEnabled(String experiment) { @@ -822,24 +847,36 @@ private static Runnable forkSystemServer(String abiList, String socketName, */ @UnsupportedAppUsage public static void main(String[] argv) { + try { + ExecSpawning.init(argv); + } catch (Throwable e) { + Log.e(TAG, "ExecSpawning init failed", e); + throw e; + } + ZygoteServer zygoteServer = null; + + boolean isExecSpawning = ExecSpawning.isExecSpawnedProcess(); + // Mark zygote start. This ensures that thread creation will throw // an error. ZygoteHooks.startZygoteNoThreadCreation(); - // Zygote goes into its own process group. - try { - Os.setpgid(0, 0); - } catch (ErrnoException ex) { - throw new RuntimeException("Failed to setpgid(0,0)", ex); + if (!isExecSpawning) { + // Zygote goes into its own process group. + try { + Os.setpgid(0, 0); + } catch (ErrnoException ex) { + throw new RuntimeException("Failed to setpgid(0,0)", ex); + } } Runnable caller; try { // Store now for StatsLogging later. final long startTime = SystemClock.elapsedRealtime(); - final boolean isRuntimeRestarted = "1".equals( + final boolean isRuntimeRestarted = !isExecSpawning && "1".equals( SystemProperties.get("sys.boot_completed")); String bootTimeTag = Process.is64Bit() ? "Zygote64Timing" : "Zygote32Timing"; @@ -861,18 +898,22 @@ public static void main(String[] argv) { abiList = argv[i].substring(ABI_LIST_ARG.length()); } else if (argv[i].startsWith(SOCKET_NAME_ARG)) { zygoteSocketName = argv[i].substring(SOCKET_NAME_ARG.length()); + } else if (argv[i].startsWith(COMMAND_FD_ARG)) { + Preconditions.checkState(isExecSpawning); + // handled by ExecSpawning.init() + continue; } else { throw new RuntimeException("Unknown command line argument: " + argv[i]); } } - final boolean isPrimaryZygote = zygoteSocketName.equals(Zygote.PRIMARY_SOCKET_NAME); - if (!isRuntimeRestarted) { - if (isPrimaryZygote) { + if (!isExecSpawning && !isRuntimeRestarted) { + ZygoteType zygoteType = ZygoteType.fromSocketName(zygoteSocketName); + if (zygoteType == ZygoteType.Primary) { FrameworkStatsLog.write(FrameworkStatsLog.BOOT_TIME_EVENT_ELAPSED_TIME_REPORTED, BOOT_TIME_EVENT_ELAPSED_TIME__EVENT__ZYGOTE_INIT_START, startTime); - } else if (zygoteSocketName.equals(Zygote.SECONDARY_SOCKET_NAME)) { + } else if (zygoteType == ZygoteType.Secondary) { FrameworkStatsLog.write(FrameworkStatsLog.BOOT_TIME_EVENT_ELAPSED_TIME_REPORTED, BOOT_TIME_EVENT_ELAPSED_TIME__EVENT__SECONDARY_ZYGOTE_INIT_START, startTime); @@ -889,7 +930,7 @@ public static void main(String[] argv) { bootTimingsTraceLog.traceBegin("ZygotePreload"); EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_START, SystemClock.uptimeMillis()); - preload(bootTimingsTraceLog); + preload(bootTimingsTraceLog, !isExecSpawning); EventLog.writeEvent(LOG_BOOT_PROGRESS_PRELOAD_END, SystemClock.uptimeMillis()); bootTimingsTraceLog.traceEnd(); // ZygotePreload @@ -902,11 +943,13 @@ public static void main(String[] argv) { bootTimingsTraceLog.traceEnd(); // ZygoteInit - Zygote.initNativeState(isPrimaryZygote); + final ZygoteType zygoteType = ZygoteType.fromSocketName(zygoteSocketName); + + Zygote.initNativeState(zygoteType); ZygoteHooks.stopZygoteNoThreadCreation(); - zygoteServer = new ZygoteServer(isPrimaryZygote); + zygoteServer = isExecSpawning ? new ZygoteServer() : new ZygoteServer(zygoteType); if (startSystemServer) { Runnable r = forkSystemServer(abiList, zygoteSocketName, zygoteServer); @@ -919,13 +962,13 @@ public static void main(String[] argv) { } } - Log.i(TAG, "Accepting command socket connections"); + if (!isExecSpawning) Log.i(TAG, "Accepting command socket connections"); // The select loop returns early in the child process after a fork and // loops forever in the zygote. caller = zygoteServer.runSelectLoop(abiList); } catch (Throwable ex) { - Log.e(TAG, "System zygote died with fatal exception", ex); + Log.e(TAG, (isExecSpawning ? "Exec spawned process" : "System zygote died") + " with fatal exception", ex); throw ex; } finally { if (zygoteServer != null) { @@ -951,9 +994,9 @@ private static boolean hasSecondZygote(String abiList) { } private static void waitForSecondaryZygote(String socketName) { - String otherZygoteName = Zygote.PRIMARY_SOCKET_NAME.equals(socketName) - ? Zygote.SECONDARY_SOCKET_NAME : Zygote.PRIMARY_SOCKET_NAME; - ZygoteProcess.waitForConnectionToZygote(otherZygoteName); + ZygoteType otherZygoteType = ZygoteType.Primary.getSocketName().equals(socketName) + ? ZygoteType.Secondary : ZygoteType.Primary; + ZygoteProcess.waitForConnectionToZygote(otherZygoteType); } static boolean isPreloadComplete() { diff --git a/core/java/com/android/internal/os/ZygoteServer.java b/core/java/com/android/internal/os/ZygoteServer.java index f8598f2f471a6..646228615095c 100644 --- a/core/java/com/android/internal/os/ZygoteServer.java +++ b/core/java/com/android/internal/os/ZygoteServer.java @@ -146,23 +146,12 @@ private enum UsapPoolRefillAction { /** * Initialize the Zygote server with the Zygote server socket, USAP pool server socket, and USAP * pool event FD. - * - * @param isPrimaryZygote If this is the primary Zygote or not. */ - ZygoteServer(boolean isPrimaryZygote) { + ZygoteServer(ZygoteType type) { mUsapPoolEventFD = Zygote.getUsapPoolEventFD(); - if (isPrimaryZygote) { - mZygoteSocket = Zygote.createManagedSocketFromInitSocket(Zygote.PRIMARY_SOCKET_NAME); - mUsapPoolSocket = - Zygote.createManagedSocketFromInitSocket( - Zygote.USAP_POOL_PRIMARY_SOCKET_NAME); - } else { - mZygoteSocket = Zygote.createManagedSocketFromInitSocket(Zygote.SECONDARY_SOCKET_NAME); - mUsapPoolSocket = - Zygote.createManagedSocketFromInitSocket( - Zygote.USAP_POOL_SECONDARY_SOCKET_NAME); - } + mZygoteSocket = Zygote.createManagedSocketFromInitSocket(type.getSocketName()); + mUsapPoolSocket = Zygote.createManagedSocketFromInitSocket(type.getUsapPoolSocketName()); mUsapPoolSupported = true; fetchUsapPoolPolicyProps(); @@ -392,6 +381,10 @@ private void resetUsapRefillState() { * @param abiList list of ABIs supported by this zygote. */ Runnable runSelectLoop(String abiList) { + if (ExecSpawning.isReplayingZygoteCommands()) { + return ExecSpawning.replayCommands(this); + } + ArrayList socketFDs = new ArrayList<>(); ArrayList peers = new ArrayList<>(); @@ -518,7 +511,7 @@ Runnable runSelectLoop(String abiList) { boolean multipleForksOK = !isUsapPoolEnabled() && ZygoteHooks.isIndefiniteThreadSuspensionSafe(); final Runnable command = - connection.processCommand(this, multipleForksOK); + connection.processCommand(this, multipleForksOK, null); // TODO (chriswailes): Is this extra check necessary? if (mIsForkChild) { diff --git a/core/java/com/android/internal/os/ZygoteType.java b/core/java/com/android/internal/os/ZygoteType.java new file mode 100644 index 0000000000000..823c151411936 --- /dev/null +++ b/core/java/com/android/internal/os/ZygoteType.java @@ -0,0 +1,35 @@ +package com.android.internal.os; + +/** + * @hide + */ +public enum ZygoteType { + Primary("zygote", "usap_pool_primary"), + Compat("zygote_compat", "usap_pool_compat"), + Secondary("zygote_secondary", "usap_pool_secondary"); + + private final String socketName; + private final String usapPoolSocketName; + + ZygoteType(String socketName, String usapPoolSocketName) { + this.socketName = socketName; + this.usapPoolSocketName = usapPoolSocketName; + } + + public String getSocketName() { + return socketName; + } + + public String getUsapPoolSocketName() { + return usapPoolSocketName; + } + + public static ZygoteType fromSocketName(String socketName) { + for (var t : ZygoteType.values()) { + if (socketName.equals(t.socketName)) { + return t; + } + } + throw new IllegalArgumentException(socketName); + } +} diff --git a/core/java/com/android/internal/pm/parsing/PackageParser2.java b/core/java/com/android/internal/pm/parsing/PackageParser2.java index 8542728765985..1f4ab65874fb5 100644 --- a/core/java/com/android/internal/pm/parsing/PackageParser2.java +++ b/core/java/com/android/internal/pm/parsing/PackageParser2.java @@ -192,8 +192,10 @@ public abstract static class Callback implements ParsingPackageUtils.Callback { public final ParsingPackage startParsingPackage(@NonNull String packageName, @NonNull String baseCodePath, @NonNull String codePath, @NonNull TypedArray manifestArray, boolean isCoreApp) { - return PackageImpl.forParsing(packageName, baseCodePath, codePath, manifestArray, + var res = PackageImpl.forParsing(packageName, baseCodePath, codePath, manifestArray, isCoreApp, Callback.this); + res.initPackageParsingHooks(); + return res; } /** diff --git a/core/java/com/android/internal/pm/parsing/pkg/PackageExtDefault.java b/core/java/com/android/internal/pm/parsing/pkg/PackageExtDefault.java new file mode 100644 index 0000000000000..09ddc8f0ec074 --- /dev/null +++ b/core/java/com/android/internal/pm/parsing/pkg/PackageExtDefault.java @@ -0,0 +1,13 @@ +package com.android.internal.pm.parsing.pkg; + +import android.ext.AppInfoExt; + +/** @hide */ +public class PackageExtDefault implements PackageExtIface { + public static final PackageExtDefault INSTANCE = new PackageExtDefault(); + + @Override + public AppInfoExt toAppInfoExt(PackageImpl pkg) { + return AppInfoExt.DEFAULT; + } +} diff --git a/core/java/com/android/internal/pm/parsing/pkg/PackageExtIface.java b/core/java/com/android/internal/pm/parsing/pkg/PackageExtIface.java new file mode 100644 index 0000000000000..c09bd69b01d65 --- /dev/null +++ b/core/java/com/android/internal/pm/parsing/pkg/PackageExtIface.java @@ -0,0 +1,8 @@ +package com.android.internal.pm.parsing.pkg; + +import android.ext.AppInfoExt; + +/** @hide */ +public interface PackageExtIface { + AppInfoExt toAppInfoExt(PackageImpl pkg); +} diff --git a/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java b/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java index e1b6adf12ddd8..08eeaa36fa817 100644 --- a/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java +++ b/core/java/com/android/internal/pm/parsing/pkg/PackageImpl.java @@ -80,6 +80,7 @@ import com.android.internal.pm.pkg.component.ParsedServiceImpl; import com.android.internal.pm.pkg.component.ParsedUsesPermission; import com.android.internal.pm.pkg.component.ParsedUsesPermissionImpl; +import com.android.internal.pm.pkg.parsing.PackageParsingHooks; import com.android.internal.pm.pkg.parsing.ParsingPackage; import com.android.internal.pm.pkg.parsing.ParsingPackageHidden; import com.android.internal.pm.pkg.parsing.ParsingPackageUtils; @@ -102,8 +103,10 @@ import java.util.Comparator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.UUID; +import java.util.function.Function; /** * Extensions to {@link PackageImpl} including fields/state contained in the system server @@ -647,6 +650,10 @@ public PackageImpl addProtectedBroadcast(String protectedBroadcast) { @Override public PackageImpl addProvider(ParsedProvider parsedProvider) { + if (getPackageParsingHooks().shouldSkipProvider(parsedProvider)) { + return this; + } + this.providers = CollectionUtils.add(this.providers, parsedProvider); addMimeGroupsFromComponent(parsedProvider); return this; @@ -2666,6 +2673,7 @@ public PackageImpl sortServices() { public ApplicationInfo toAppInfoWithoutStateWithoutFlags() { ApplicationInfo appInfo = new ApplicationInfo(); + appInfo.setExt(ext.toAppInfoExt(this)); // Lines that are commented below are state related and should not be assigned here. // They are left in as placeholders, since there is no good backwards compatible way to @@ -2830,6 +2838,7 @@ public PackageImpl(@NonNull String packageName, @NonNull String baseApkPath, @Override public PackageImpl hideAsParsed() { assignDerivedFields(); + assignDerivedFields2(); return this; } @@ -3027,9 +3036,42 @@ public PackageImpl setPackageName(@NonNull String packageName) { ComponentMutateUtils.setPackageName(receivers.get(index), this.packageName); } + final String legacyVanadiumPkgName = "org.chromium.chrome"; + final String realVanadiumPkgName = "app.vanadium.browser"; + + final boolean isLegacyVanadiumPkgName = legacyVanadiumPkgName.equals(packageName); + + if (isLegacyVanadiumPkgName) { + if (!getManifestPackageName().equals(realVanadiumPkgName)) { + throw new IllegalStateException("unexpected manifestPackageName: " + + "expected " + realVanadiumPkgName + ", got " + getManifestPackageName()); + } + } + int providersSize = providers.size(); for (int index = 0; index < providersSize; index++) { - ComponentMutateUtils.setPackageName(providers.get(index), this.packageName); + ParsedProvider provider = providers.get(index); + ComponentMutateUtils.setPackageName(provider, this.packageName); + + if (isLegacyVanadiumPkgName) { + // method name "getAuthority" is misleading: it may return multiple + // ';'-separated authorities + final String separator = ";"; + String[] authorities = provider.getAuthority().split(separator); + + for (int i = 0; i < authorities.length; ++i) { + String authority = authorities[i]; + if (!authority.startsWith(realVanadiumPkgName)) { + // all Chromium ContentProvider authorities are prefixed with manifest package name + throw new IllegalStateException("unexpected Vanadium ContentProvider authority " + authority); + } + + authorities[i] = authority.replaceFirst(realVanadiumPkgName, legacyVanadiumPkgName); + } + + String renamedAuthorities = String.join(separator, authorities); + ComponentMutateUtils.setAuthority(provider, renamedAuthorities); + } } int servicesSize = services.size(); @@ -4022,4 +4064,35 @@ private static class Booleans2 { private static final long UPDATABLE_SYSTEM = 1L << 2; private static final long HAS_PCC_COMPONENTS = 1L << 3; } + + private PackageParsingHooks packageParsingHooks = PackageParsingHooks.DEFAULT; + + public static Function packageParsingHooksSupplier; + + @Override + public void initPackageParsingHooks() { + var supplier = packageParsingHooksSupplier; + packageParsingHooks = supplier != null ? supplier.apply(getPackageName()) : PackageParsingHooks.DEFAULT; + } + + @Override + public PackageParsingHooks getPackageParsingHooks() { + return packageParsingHooks; + } + + private PackageExtIface ext = PackageExtDefault.INSTANCE; + + @Override + public void setPackageExt(@Nullable PackageExtIface ext) { + this.ext = ext; + } + + @Nullable + @Override + public PackageExtIface ext() { + return ext; + } + + public long cachedCompatConfigVersionCode; + public Object cachedCompatConfig; } diff --git a/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java b/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java index 9ab973095b5e4..a93d76da0536b 100644 --- a/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java +++ b/core/java/com/android/internal/pm/pkg/component/ParsedServiceUtils.java @@ -204,6 +204,8 @@ public static ParseResult parseService(String[] separateProcesses service.setExported(hasIntentFilters); } + pkg.getPackageParsingHooks().amendParsedService(service); + return input.success(service); } } diff --git a/core/java/com/android/internal/pm/pkg/parsing/PackageParsingHooks.java b/core/java/com/android/internal/pm/pkg/parsing/PackageParsingHooks.java new file mode 100644 index 0000000000000..750fb0ff3a568 --- /dev/null +++ b/core/java/com/android/internal/pm/pkg/parsing/PackageParsingHooks.java @@ -0,0 +1,71 @@ +package com.android.internal.pm.pkg.parsing; + +import android.annotation.Nullable; +import android.content.pm.PackageManager; + +import com.android.internal.pm.pkg.component.ParsedPermission; +import com.android.internal.pm.pkg.component.ParsedProvider; +import com.android.internal.pm.pkg.component.ParsedService; +import com.android.internal.pm.pkg.component.ParsedServiceImpl; +import com.android.internal.pm.pkg.component.ParsedUsesPermission; +import com.android.internal.pm.pkg.component.ParsedUsesPermissionImpl; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class PackageParsingHooks { + public static final PackageParsingHooks DEFAULT = new PackageParsingHooks(); + + public boolean shouldSkipPermissionDefinition(ParsedPermission p) { + return false; + } + + public boolean shouldSkipUsesPermission(ParsedUsesPermission p) { + return false; + } + + public boolean shouldSkipProvider(ParsedProvider p) { + return false; + } + + @Nullable + public List addUsesPermissions() { + return null; + } + + protected static List createUsesPerms(String... perms) { + int l = perms.length; + var res = new ArrayList(l); + for (int i = 0; i < l; ++i) { + res.add(new ParsedUsesPermissionImpl(perms[i], 0, 0, Collections.emptySet(), Collections.emptySet())); + } + return res; + } + + public void amendParsedService(ParsedServiceImpl s) { + + } + + public List addServices(ParsingPackage pkg) { + return null; + } + + // supported return values: + // PackageManager.COMPONENT_ENABLED_STATE_DISABLED + // PackageManager.COMPONENT_ENABLED_STATE_ENABLED + // PackageManager.COMPONENT_ENABLED_STATE_DEFAULT (skip override) + public int overrideDefaultPackageEnabledState() { + return PackageManager.COMPONENT_ENABLED_STATE_DEFAULT; + } + + public static ParsedServiceImpl createService(ParsingPackage pkg, String className) { + var s = new ParsedServiceImpl(); + s.setPackageName(pkg.getPackageName()); + s.setName(className); + s.setProcessName(pkg.getProcessName()); + s.setDirectBootAware(pkg.isPartiallyDirectBootAware()); + s.setExported(true); + return s; + } +} diff --git a/core/java/com/android/internal/pm/pkg/parsing/ParsingPackage.java b/core/java/com/android/internal/pm/pkg/parsing/ParsingPackage.java index a23604de68901..3beb7174220a3 100644 --- a/core/java/com/android/internal/pm/pkg/parsing/ParsingPackage.java +++ b/core/java/com/android/internal/pm/pkg/parsing/ParsingPackage.java @@ -31,6 +31,7 @@ import android.util.SparseArray; import android.util.SparseIntArray; +import com.android.internal.pm.parsing.pkg.PackageExtIface; import com.android.internal.pm.parsing.pkg.ParsedPackage; import com.android.internal.pm.pkg.component.ParsedActivity; import com.android.internal.pm.pkg.component.ParsedAllowComponentAccessPolicy; @@ -630,4 +631,16 @@ ParsingPackage setResetEnabledSettingsOnAppDataCleared( * Returns the intent matching flags. */ int getIntentMatchingFlags(); + + boolean isPartiallyDirectBootAware(); + + void initPackageParsingHooks(); + + default PackageParsingHooks getPackageParsingHooks() { + return PackageParsingHooks.DEFAULT; + } + + void setPackageExt(@Nullable PackageExtIface ext); + + boolean isDeclaredHavingCode(); } diff --git a/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java b/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java index 4e27a18435eb9..699072c44a873 100644 --- a/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java +++ b/core/java/com/android/internal/pm/pkg/parsing/ParsingPackageUtils.java @@ -67,6 +67,7 @@ import android.content.res.Resources; import android.content.res.TypedArray; import android.content.res.XmlResourceParser; +import android.ext.PackageId; import android.net.Uri; import android.os.Build; import android.os.Bundle; @@ -148,6 +149,7 @@ import java.util.Objects; import java.util.Set; import java.util.StringTokenizer; +import java.util.function.Function; /** * TODO(b/135203078): Differentiate between parse_ methods and some add_ method for whether it @@ -655,9 +657,22 @@ private ParseResult parseBaseApk(ParseInput input, File apkFile, pkg.setVolumeUuid(volumeUuid); + PackageExtInitIface pkgExtInit = null; + PackageExtInitSupplier pkgExtInitSupplier = packageExtInitSupplier; + if (pkgExtInitSupplier != null) { + pkgExtInit = pkgExtInitSupplier.invoke(input, pkg, (flags & PARSE_IS_SYSTEM_DIR) != 0); + if (pkgExtInit != null) { + pkgExtInit.run(); + } + } + if ((flags & PARSE_COLLECT_CERTIFICATES) != 0) { - final ParseResult ret = - getSigningDetails(input, pkg, false /*skipVerify*/); + // skip reparsing certificates if they were already parsed by PackageExtInit + ParseResult ret = pkgExtInit != null ? + pkgExtInit.getSigningDetailsParseResult() : null; + if (ret == null) { + ret = parseSigningDetails(input, pkg); + } if (ret.isError()) { return input.error(ret); } @@ -673,6 +688,22 @@ private ParseResult parseBaseApk(ParseInput input, File apkFile, } } + public interface PackageExtInitSupplier { + PackageExtInitIface invoke(ParseInput input, ParsingPackage pkg, boolean isSystem); + } + + @Nullable + public static PackageExtInitSupplier packageExtInitSupplier; + + public interface PackageExtInitIface { + void run(); + ParseResult getSigningDetailsParseResult(); + } + + public static ParseResult parseSigningDetails(ParseInput input, ParsingPackage pkg) { + return getSigningDetails(input, pkg, false /*skipVerify*/); + } + private ParseResult parseSplitApk(ParseInput input, ParsingPackage pkg, int splitIndex, AssetManager assets, int flags) { final String apkPath = pkg.getSplitCodePaths()[splitIndex]; @@ -1022,6 +1053,25 @@ private ParseResult validateBaseApkTags(ParseInput input, Parsin ); } + List extraUsesPerms = pkg.getPackageParsingHooks().addUsesPermissions(); + + if (extraUsesPerms != null) { + for (ParsedUsesPermission p : extraUsesPerms) { + String name = p.getName(); + if (pkg.getUsesPermissionMapping().containsKey(name)) { + Slog.w(TAG, "PackageParsingHooks.addUsesPermissions() " + + "tried to add duplicate uses-permission " + name + + " to pkg " + pkg.getPackageName()); + continue; + } + pkg.addUsesPermission(p); + } + } + + if (pkg.isDeclaredHavingCode() && !pkg.getUsesPermissionMapping().containsKey(android.Manifest.permission.OTHER_SENSORS)) { + pkg.addImplicitPermission(android.Manifest.permission.OTHER_SENSORS); + } + convertCompatPermissions(pkg); convertSplitPermissions(pkg); @@ -1138,6 +1188,12 @@ private static ParseResult parseSharedUser(ParseInput input, if (PackageManager.ENABLE_SHARED_UID_MIGRATION) { int max = anInteger(0, R.styleable.AndroidManifest_sharedUserMaxSdkVersion, sa); leaving = (max != 0) && (max < Build.VERSION.RESOURCES_SDK_INT); + + if (android.ext.PackageId.GMS_CORE_NAME.equals(pkg.getPackageName())) { + // Ignore sharedUid for fresh installs of GmsCore. On existing installs, sharedUid + // is needed for access to GSF. + leaving = true; + } } return input.success(pkg @@ -1321,7 +1377,9 @@ private static ParseResult parsePermission(ParseInput input, } ParsedPermission permission = result.getResult(); if (permission != null) { - pkg.addPermission(permission); + if (!pkg.getPackageParsingHooks().shouldSkipPermissionDefinition(permission)) { + pkg.addPermission(permission); + } } return input.success(pkg); } @@ -1523,8 +1581,10 @@ private ParseResult parseUsesPermission(ParseInput input, } if (!found) { - pkg.addUsesPermission( - new ParsedUsesPermissionImpl(name, usesPermissionFlags, purposeStringResource, purposes, generalPurposes)); + var p = new ParsedUsesPermissionImpl(name, usesPermissionFlags, purposeStringResource, purposes, generalPurposes); + if (!pkg.getPackageParsingHooks().shouldSkipUsesPermission(p)) { + pkg.addUsesPermission(p); + } } return success; } finally { @@ -2385,6 +2445,22 @@ private ParseResult parseBaseApplication(ParseInput input, if (hasReceiverOrder) { pkg.sortReceivers(); } + + List extraServices = pkg.getPackageParsingHooks().addServices(pkg); + if (extraServices != null) { + for (var s : extraServices) { + hasServiceOrder |= (s.getOrder() != 0); + pkg.addService(s); + } + } + + if (gmsCompatClientServiceSupplier != null) { + ParsedService gmsCompatClientSvc = gmsCompatClientServiceSupplier.apply(pkg); + if (gmsCompatClientSvc != null) { + pkg.addService(gmsCompatClientSvc); + } + } + if (hasServiceOrder) { pkg.sortServices(); } @@ -2394,6 +2470,8 @@ private ParseResult parseBaseApplication(ParseInput input, return input.success(pkg); } + public static Function gmsCompatClientServiceSupplier; + // Must be run after the entire {@link ApplicationInfo} has been fully processed and after // every activity info has had a chance to set it from its attributes. private void afterParseBaseApplication(ParsingPackage pkg) { @@ -2494,6 +2572,20 @@ private void parseBaseAppBasicFlags(ParsingPackage pkg, TypedArray sa) { // CHECKSTYLE:on //@formatter:on + + var hooks = pkg.getPackageParsingHooks(); + int enabledOverride = hooks.overrideDefaultPackageEnabledState(); + if (enabledOverride == PackageManager.COMPONENT_ENABLED_STATE_DISABLED) { + pkg.setEnabled(false); + } else if (enabledOverride == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) { + pkg.setEnabled(true); + } + + if (PackageId.GSF_NAME.equals(pkg.getPackageName())) { + // GSF is a hasCode=false package on GMS Android 15+. GrapheneOS doesn't include GSF as a + // preinstalled app, so pre-35 GSF remains after update from pre-Android 15. + pkg.setDeclaredHavingCode(false); + } } /** diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java index 3c9d05d7bfe33..f87072bbcf9bd 100644 --- a/core/java/com/android/internal/policy/PhoneWindow.java +++ b/core/java/com/android/internal/policy/PhoneWindow.java @@ -472,6 +472,10 @@ public static boolean isEdgeToEdgeEnforced(ApplicationInfo info, boolean local, * whether the app declares style to opt out. */ public static boolean isOptOutEdgeToEdgeEnabled(ApplicationInfo info, boolean local) { + if (info.isSystemApp()) { + return true; + } + final boolean disabled = local // Calling this doesn't require a permission. ? CompatChanges.isChangeEnabled(DISABLE_OPT_OUT_EDGE_TO_EDGE) diff --git a/core/java/com/android/internal/util/DumpUtils.java b/core/java/com/android/internal/util/DumpUtils.java index d1ff034ff0015..00b77ac98d23f 100644 --- a/core/java/com/android/internal/util/DumpUtils.java +++ b/core/java/com/android/internal/util/DumpUtils.java @@ -96,11 +96,17 @@ private static void logMessage(PrintWriter pw, String msg) { @android.ravenwood.annotation.RavenwoodThrow( blockedBy = android.permission.PermissionManager.class) public static boolean checkDumpPermission(Context context, String tag, PrintWriter pw) { + return checkDumpPermission(context, tag, pw, true); + } + + public static boolean checkDumpPermission(Context context, String tag, PrintWriter pw, boolean log) { if (context.checkCallingOrSelfPermission(android.Manifest.permission.DUMP) != PackageManager.PERMISSION_GRANTED) { - logMessage(pw, "Permission Denial: can't dump " + tag + " from from pid=" - + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid() - + " due to missing android.permission.DUMP permission"); + if (log) { + logMessage(pw, "Permission Denial: can't dump " + tag + " from from pid=" + + Binder.getCallingPid() + ", uid=" + Binder.getCallingUid() + + " due to missing android.permission.DUMP permission"); + } return false; } else { return true; diff --git a/core/java/com/android/internal/util/GoogleCameraUtils.java b/core/java/com/android/internal/util/GoogleCameraUtils.java new file mode 100644 index 0000000000000..ff8dfcab960c5 --- /dev/null +++ b/core/java/com/android/internal/util/GoogleCameraUtils.java @@ -0,0 +1,16 @@ +package com.android.internal.util; + +import android.content.Context; +import android.content.res.Resources; +import android.ext.PackageId; + +import com.android.internal.R; + +public class GoogleCameraUtils { + public static final String PACKAGE_NAME = PackageId.G_CAMERA_NAME; + + public static boolean isCustomSeInfoNeededForAccessToAccelerators(Context ctx) { + Resources res = ctx.getResources(); + return res.getBoolean(R.bool.config_GoogleCamera_needs_seinfo_for_access_to_accelerators); + } +} diff --git a/core/java/com/android/internal/widget/ILockSettings.aidl b/core/java/com/android/internal/widget/ILockSettings.aidl index 3c11fce117aea..5abca2d4f6ec5 100644 --- a/core/java/com/android/internal/widget/ILockSettings.aidl +++ b/core/java/com/android/internal/widget/ILockSettings.aidl @@ -29,6 +29,7 @@ import android.security.keystore.recovery.RecoveryCertPath; import com.android.internal.widget.ICheckCredentialProgressCallback; import com.android.internal.widget.IWeakEscrowTokenActivatedListener; import com.android.internal.widget.IWeakEscrowTokenRemovedListener; +import com.android.internal.widget.LockDomain; import com.android.internal.widget.LockscreenCredential; import com.android.internal.widget.VerifyCredentialResponse; @@ -49,18 +50,18 @@ interface ILockSettings { long getLong(in String key, in long defaultValue, in int userId); @UnsupportedAppUsage String getString(in String key, in String defaultValue, in int userId); - boolean setLockCredential(in LockscreenCredential credential, in LockscreenCredential savedCredential, int userId); + boolean setLockCredential(in LockscreenCredential credential, in LockscreenCredential savedCredential, in LockDomain lockDomain, int userId); void resetKeyStore(int userId); void prepareToVerifyCredential(int userId); - VerifyCredentialResponse checkCredential(in LockscreenCredential credential, int userId, + VerifyCredentialResponse checkCredential(in LockscreenCredential credential, in LockDomain lockDomain, int userId, in ICheckCredentialProgressCallback progressCallback); - VerifyCredentialResponse verifyCredential(in LockscreenCredential credential, int userId, int flags); + VerifyCredentialResponse verifyCredential(in LockscreenCredential credential, in LockDomain lockDomain, int userId, int flags); VerifyCredentialResponse verifyTiedProfileChallenge(in LockscreenCredential credential, int userId, int flags); VerifyCredentialResponse verifyGatekeeperPasswordHandle(long gatekeeperPasswordHandle, long challenge, int userId); void removeGatekeeperPasswordHandle(long gatekeeperPasswordHandle); - int getCredentialType(int userId); - int getPinLength(int userId); - boolean refreshStoredPinLength(int userId); + int getCredentialType(int userId, in LockDomain lockDomain); + int getPinLength(int userId, in LockDomain lockDomain); + boolean refreshStoredPinLength(int userId, in LockDomain lockDomain); byte[] getHashFactor(in LockscreenCredential currentCredential, int userId); void setSeparateProfileChallengeEnabled(int userId, boolean enabled, in LockscreenCredential managedUserPassword); boolean isUseOneLockSettingEnabled(int userId); @@ -73,7 +74,7 @@ interface ILockSettings { void userPresent(int userId); int getStrongAuthForUser(int userId); boolean hasPendingEscrowToken(int userId); - ParcelDuration getLockoutEndTime(int userId); + ParcelDuration getLockoutEndTime(int userId, in LockDomain lockDomain); // Keystore RecoveryController methods. // {@code ServiceSpecificException} may be thrown to signal an error, which caller can @@ -114,4 +115,6 @@ interface ILockSettings { boolean isWeakEscrowTokenValid(long handle, in byte[] token, int userId); void unlockUserKeyIfUnsecured(int userId); boolean writeRepairModeCredential(int userId); + void setDuressCredentials(in LockscreenCredential ownerCredential, in LockscreenCredential duressPin, in LockscreenCredential duressPassword); + boolean hasDuressCredentials(in LockscreenCredential ownerCredential); } diff --git a/core/java/com/android/internal/widget/LockDomain.aidl b/core/java/com/android/internal/widget/LockDomain.aidl new file mode 100644 index 0000000000000..6ee4d59134263 --- /dev/null +++ b/core/java/com/android/internal/widget/LockDomain.aidl @@ -0,0 +1,3 @@ +package com.android.internal.widget; + +parcelable LockDomain; diff --git a/core/java/com/android/internal/widget/LockDomain.java b/core/java/com/android/internal/widget/LockDomain.java new file mode 100644 index 0000000000000..46f7e9f02e95f --- /dev/null +++ b/core/java/com/android/internal/widget/LockDomain.java @@ -0,0 +1,40 @@ +package com.android.internal.widget; + +import android.os.Parcel; +import android.os.Parcelable; + +/** + * Using this type as an argument for methods that require secondary handling is safer and more + * future proof given that we rebase on top of upstream. + * + * Consider that if we add secondary handling to a method by overloading with a boolean + * isPrimary, then upstream could add a conflicting overload. + * + * Also consider that if we have foo(boolean) and its overload foo(boolean, boolean), adding an + * additional boolean argument, isPrimary, to both could result in future callers silently calling + * the wrong method. + */ +public enum LockDomain implements Parcelable { + Primary, Secondary; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@androidx.annotation.NonNull Parcel dest, int flags) { + dest.writeInt(ordinal()); + } + + public static final Parcelable.Creator CREATOR + = new Parcelable.Creator<>() { + public LockDomain createFromParcel(Parcel in) { + return LockDomain.values()[in.readInt()]; + } + + public LockDomain[] newArray(int size) { + return new LockDomain[size]; + } + }; +} \ No newline at end of file diff --git a/core/java/com/android/internal/widget/LockPatternChecker.java b/core/java/com/android/internal/widget/LockPatternChecker.java index c73658cf110be..b2dfcaeab1831 100644 --- a/core/java/com/android/internal/widget/LockPatternChecker.java +++ b/core/java/com/android/internal/widget/LockPatternChecker.java @@ -1,6 +1,7 @@ package com.android.internal.widget; import static com.android.internal.widget.flags.Flags.runCheckCredentialWithHigherPriority; +import static com.android.internal.widget.LockDomain.Primary; import android.annotation.NonNull; import android.os.AsyncTask; @@ -89,6 +90,13 @@ protected void onCancelled() { return task; } + public static AsyncTask checkCredential(LockPatternUtils utils, + final LockscreenCredential credential, + final int userId, + final OnCheckCallback callback) { + return checkCredential(utils, Primary, credential, userId, callback); + } + /** * Checks a lockscreen credential asynchronously. * @@ -98,6 +106,7 @@ protected void onCancelled() { * @param callback The callback to be invoked with the check result. */ public static AsyncTask checkCredential(final LockPatternUtils utils, + final LockDomain lockDomain, final LockscreenCredential credential, final int userId, final OnCheckCallback callback) { @@ -118,7 +127,8 @@ protected VerifyCredentialResponse doInBackground(Void... args) { + "priority display", e); } } - return utils.checkCredential(credentialCopy, userId, callback::onEarlyMatched); + return utils.checkCredential(credentialCopy, lockDomain, userId, + callback::onEarlyMatched); } finally { if (runCheckCredentialWithHigherPriority() && originalPriority != INVALID_PRIORITY) { diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java index d7b9da7ea4048..15f5f0b7c2e0d 100644 --- a/core/java/com/android/internal/widget/LockPatternUtils.java +++ b/core/java/com/android/internal/widget/LockPatternUtils.java @@ -23,6 +23,10 @@ import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_SOMETHING; import static android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_UNSPECIFIED; import static android.security.Flags.shouldTrustManagerListenForPrimaryAuth; +import static com.android.internal.widget.LockDomain.Primary; +import static com.android.internal.widget.LockDomain.Secondary; +import static com.android.internal.widget.LockPatternUtils.ThrowIfUserNotExist.DoNotThrow; +import static com.android.internal.widget.LockPatternUtils.ThrowIfUserNotExist.DoThrow; import static com.android.internal.widget.flags.Flags.enableDefaultVisibilityForSensitiveInputs; @@ -44,6 +48,7 @@ import android.content.ContentResolver; import android.content.Context; import android.content.pm.UserInfo; +import android.content.pm.UserProperties; import android.os.Build; import android.os.Handler; import android.os.Looper; @@ -207,6 +212,8 @@ public static String credentialTypeToString(int credentialType) { Settings.Secure.LOCK_SCREEN_OWNER_INFO_ENABLED; private static final String LOCK_PIN_ENHANCED_PRIVACY = "pin_enhanced_privacy"; + private static final String LOCK_PIN_ENHANCED_PRIVACY_SECONDARY = + "pin_enhanced_privacy_secondary"; private static final String LOCK_SCREEN_DEVICE_OWNER_INFO = "lockscreen.device_owner_info"; @@ -215,8 +222,9 @@ public static String credentialTypeToString(int credentialType) { private static final String IS_TRUST_USUALLY_MANAGED = "lockscreen.istrustusuallymanaged"; public static final String AUTO_PIN_CONFIRM = "lockscreen.auto_pin_confirm"; + public static final String AUTO_PIN_CONFIRM_SECONDARY = "lockscreen.auto_pin_confirm_secondary"; - public static final String CURRENT_LSKF_BASED_PROTECTOR_ID_KEY = "sp-handle"; + public static final String CURRENT_LSKF_BASED_PROTECTOR_ID_KEY_BASE = "sp-handle"; public static final String PASSWORD_HISTORY_DELIMITER = ","; private static final String GSI_RUNNING_PROP = "ro.gsid.image_running"; @@ -379,22 +387,37 @@ private int getRequestedPasswordHistoryLength(int userId) { @UnsupportedAppUsage public void reportFailedPasswordAttempt(int userId) { + reportFailedPasswordAttempt(userId, Primary); + } + + public void reportFailedPasswordAttempt(int userId, LockDomain lockDomain) { if (isSpecialUserId(mContext, userId, /* checkDeviceSupported= */ true)) { - return; + if (lockDomain == Primary) { + return; + } + throw new SecondaryForSpecialUserException(); } - getDevicePolicyManager().reportFailedPasswordAttempt(userId); - if (!shouldTrustManagerListenForPrimaryAuth()) { + + getDevicePolicyManager().reportFailedPasswordAttempt(userId, lockDomain); + if (lockDomain == Primary && !shouldTrustManagerListenForPrimaryAuth()) { getTrustManager().reportUnlockAttempt(/* authenticated= */ false, userId); } } @UnsupportedAppUsage public void reportSuccessfulPasswordAttempt(int userId) { + reportSuccessfulPasswordAttempt(userId, Primary); + } + + public void reportSuccessfulPasswordAttempt(int userId, LockDomain lockDomain) { if (isSpecialUserId(mContext, userId, /* checkDeviceSupported= */ true)) { - return; + if (lockDomain == Primary) { + return; + } + throw new SecondaryForSpecialUserException(); } - getDevicePolicyManager().reportSuccessfulPasswordAttempt(userId); - if (!shouldTrustManagerListenForPrimaryAuth()) { + getDevicePolicyManager().reportSuccessfulPasswordAttempt(userId, lockDomain); + if (lockDomain == Primary && !shouldTrustManagerListenForPrimaryAuth()) { getTrustManager().reportUnlockAttempt(/* authenticated= */ true, userId); } } @@ -403,10 +426,19 @@ public void reportSuccessfulPasswordAttempt(int userId) { * Reports a password lockout to services that need to be aware of it. */ public void reportPasswordLockout(Duration timeout, int userId) { + reportPasswordLockout(timeout, userId, Primary); + } + + public void reportPasswordLockout(Duration timeout, int userId, LockDomain lockDomain) { + if (!checkUserSupportsBiometricSecondFactorIfSecondary(userId, lockDomain)) { + return; + } if (isSpecialUserId(mContext, userId, /* checkDeviceSupported= */ true)) { return; } - getTrustManager().reportUnlockLockout(clamp(timeout), userId); + if (lockDomain == Primary) { + getTrustManager().reportUnlockLockout(clamp(timeout), userId); + } } /** @@ -424,10 +456,17 @@ public static int clamp(Duration duration) { public int getCurrentFailedPasswordAttempts(int userId) { + return getCurrentFailedPasswordAttempts(userId, Primary); + } + + public int getCurrentFailedPasswordAttempts(int userId, LockDomain lockDomain) { if (isSpecialUserId(mContext, userId, /* checkDeviceSupported= */ true)) { - return 0; + if (lockDomain == Primary) { + return 0; + } + throw new SecondaryForSpecialUserException(); } - return getDevicePolicyManager().getCurrentFailedPasswordAttempts(userId); + return getDevicePolicyManager().getCurrentFailedPasswordAttempts(userId, lockDomain); } public int getMaximumFailedPasswordsForWipe(int userId) { @@ -465,10 +504,16 @@ public boolean writeRepairModeCredential(int userId) { @NonNull public VerifyCredentialResponse verifyCredential(@NonNull LockscreenCredential credential, int userId, @VerifyFlag int flags) { + return verifyCredential(credential, Primary, userId, flags); + } + + @NonNull + public VerifyCredentialResponse verifyCredential(@NonNull LockscreenCredential credential, + LockDomain lockDomain, int userId, @VerifyFlag int flags) { throwIfCalledOnMainThread(); try { final VerifyCredentialResponse response = getLockSettings().verifyCredential( - credential, userId, flags); + credential, lockDomain, userId, flags); if (response == null) { return VerifyCredentialResponse.OTHER_ERROR; } else { @@ -536,11 +581,16 @@ public VerifyCredentialResponse checkCredential( @NonNull LockscreenCredential credential, int userId, @Nullable CheckCredentialProgressCallback progressCallback) { + return checkCredential(credential, Primary, userId, progressCallback); + } + + public VerifyCredentialResponse checkCredential(@NonNull LockscreenCredential credential, LockDomain lockDomain, + int userId, @Nullable CheckCredentialProgressCallback progressCallback) { throwIfCalledOnMainThread(); try { final VerifyCredentialResponse response = getLockSettings() - .checkCredential(credential, userId, wrapCallback(progressCallback)); + .checkCredential(credential, lockDomain, userId, wrapCallback(progressCallback)); return requireNonNullElse(response, VerifyCredentialResponse.OTHER_ERROR); } catch (RemoteException re) { Log.e(TAG, "failed to check credential", re); @@ -636,8 +686,12 @@ public boolean checkPasswordHistory(byte[] passwordToCheck, byte[] hashFactor, i * B. PIN_LENGTH_UNAVAILABLE if it is not available or if an exception occurs */ public int getPinLength(int userId) { + return getPinLength(userId, Primary); + } + + public int getPinLength(int userId, LockDomain lockDomain) { try { - return getLockSettings().getPinLength(userId); + return getLockSettings().getPinLength(userId, lockDomain); } catch (RemoteException e) { Log.e(TAG, "Could not fetch PIN length " + e); return PIN_LENGTH_UNAVAILABLE; @@ -654,8 +708,12 @@ public int getPinLength(int userId) { * @return true/false depending on whether PIN length has been saved or not */ public boolean refreshStoredPinLength(int userId) { + return refreshStoredPinLength(userId, Primary); + } + + public boolean refreshStoredPinLength(int userId, LockDomain lockDomain) { try { - return getLockSettings().refreshStoredPinLength(userId); + return getLockSettings().refreshStoredPinLength(userId, lockDomain); } catch (RemoteException e) { Log.e(TAG, "Could not store PIN length on disk " + e); return false; @@ -669,7 +727,18 @@ public boolean refreshStoredPinLength(int userId) { */ @UnsupportedAppUsage public int getActivePasswordQuality(int userId) { - return getKeyguardStoredPasswordQuality(userId); + return getActivePasswordQuality(userId, Primary); + } + + public int getActivePasswordQuality(int userId, LockDomain lockDomain) { + return getKeyguardStoredPasswordQuality(userId, lockDomain); + } + + public boolean isBiometricSecondFactorEnabled(int userId) { + if (!checkUserSupportsBiometricSecondFactor(userId, false)) { + return false; + } + return getActivePasswordQuality(userId, Secondary) == PASSWORD_QUALITY_NUMERIC; } /** @@ -703,9 +772,22 @@ public void setLockScreenDisabled(boolean disable, int userId) { */ @UnsupportedAppUsage public boolean isLockScreenDisabled(int userId) { - if (isSecure(userId)) { + return isLockScreenDisabled(userId, Primary); + } + + public boolean isLockScreenDisabled(int userId, LockDomain lockDomain) { + if (isSecure(userId, lockDomain)) { return false; } + + if (lockDomain == Secondary) { + // When primary is !secure, the lockscreen is either enabled ("Swipe") or disabled + // ("None"). For secondary there is no situation where swipe makes sense, so always + // disable it. Don't need to implement setLockScreenDisabled for secondary as it is + // just a way for caller to set swipe/none. + return true; + } + boolean disabledByDefault = mContext.getResources().getBoolean( com.android.internal.R.bool.config_disableLockscreenByDefault); UserInfo userInfo = getUserManager().getUserInfo(userId); @@ -722,7 +804,15 @@ public boolean isLockScreenDisabled(int userId) { * @param userId user ID of the user this has effect on */ public void setAutoPinConfirm(boolean enabled, int userId) { - setBoolean(AUTO_PIN_CONFIRM, enabled, userId); + setAutoPinConfirm(enabled, userId, Primary); + } + + public void setAutoPinConfirm(boolean enabled, int userId, LockDomain lockDomain) { + if (!checkUserSupportsBiometricSecondFactorIfSecondary(userId, lockDomain)) { + return; + } + String key = lockDomain == Primary ? AUTO_PIN_CONFIRM : AUTO_PIN_CONFIRM_SECONDARY; + setBoolean(key, enabled, userId); } /** @@ -733,7 +823,15 @@ public void setAutoPinConfirm(boolean enabled, int userId) { * @return true, if the entered pin should be auto confirmed */ public boolean isAutoPinConfirmEnabled(int userId) { - return getBoolean(AUTO_PIN_CONFIRM, /* defaultValue= */ false, userId); + return isAutoPinConfirmEnabled(userId, Primary); + } + + public boolean isAutoPinConfirmEnabled(int userId, LockDomain lockDomain) { + if (!checkUserSupportsBiometricSecondFactorIfSecondary(userId, lockDomain)) { + return false; + } + String key = lockDomain == Primary ? AUTO_PIN_CONFIRM : AUTO_PIN_CONFIRM_SECONDARY; + return getBoolean(key, /* defaultValue= */ false, userId); } /** Returns if the given quality maps to an alphabetic password */ @@ -790,13 +888,25 @@ public static int pinOrPasswordQualityToCredentialType(int quality) { */ public boolean setLockCredential(@NonNull LockscreenCredential newCredential, @NonNull LockscreenCredential savedCredential, int userHandle) { + return setLockCredential(newCredential, savedCredential, Primary, userHandle); + } + + /** + * @param newCredential Can only be of type CREDENTIAL_TYPE_PIN, or CREDENTIAL_TYPE_NONE if + * lockDomain is Secondary. + * @param savedCredential Must be primary credential, even if setting secondary. + * @param lockDomain Whether setting primary or biometric second factor credential. + */ + public boolean setLockCredential(@NonNull LockscreenCredential newCredential, + @NonNull LockscreenCredential savedCredential, LockDomain lockDomain, int userHandle) { if (!hasSecureLockScreen() && newCredential.getType() != CREDENTIAL_TYPE_NONE) { throw new UnsupportedOperationException( "This operation requires the lock screen feature."); } try { - if (!getLockSettings().setLockCredential(newCredential, savedCredential, userHandle)) { + if (!getLockSettings().setLockCredential(newCredential, savedCredential, lockDomain, + userHandle)) { return false; } } catch (RemoteException e) { @@ -876,7 +986,12 @@ public static boolean isFileEncryptionEnabled() { @UnsupportedAppUsage @Deprecated public int getKeyguardStoredPasswordQuality(int userHandle) { - return credentialTypeToPasswordQuality(getCredentialTypeForUser(userHandle)); + return getKeyguardStoredPasswordQuality(userHandle, Primary); + } + + @Deprecated + public int getKeyguardStoredPasswordQuality(int userHandle, LockDomain lockDomain) { + return credentialTypeToPasswordQuality(getCredentialTypeForUser(userHandle, lockDomain)); } /** @@ -891,7 +1006,7 @@ public int getKeyguardStoredPasswordQuality(int userHandle) { */ public void setSeparateProfileChallengeEnabled(int userHandle, boolean enabled, LockscreenCredential profilePassword) { - if (!isCredentialShareableWithParent(userHandle)) { + if (!isCredentialShareableWithParent(userHandle, DoNotThrow)) { return; } try { @@ -912,7 +1027,7 @@ public void setSeparateProfileChallengeEnabled(int userHandle, boolean enabled, * credential. */ public boolean isSeparateProfileChallengeEnabled(int userHandle) { - return isCredentialShareableWithParent(userHandle) + return isCredentialShareableWithParent(userHandle, DoNotThrow) && !isUseOneLockSettingEnabled(userHandle); } @@ -929,7 +1044,7 @@ public boolean isSeparateProfileChallengeEnabled(int userHandle) { * being the state of "Use one lock" itself which matters if a credential is set again later. */ public boolean isProfileWithUnifiedChallenge(int userHandle) { - return isCredentialShareableWithParent(userHandle) + return isCredentialShareableWithParent(userHandle, DoNotThrow) && isUseOneLockSettingEnabled(userHandle); } @@ -956,13 +1071,75 @@ private boolean isManagedProfile(int userHandle) { return info != null && info.isManagedProfile(); } - private boolean isCredentialShareableWithParent(int userHandle) { + public enum ThrowIfUserNotExist { + DoThrow, DoNotThrow + } + + public boolean isCredentialShareableWithParent(int userHandle, + ThrowIfUserNotExist throwIfUserNotExist) { + UserProperties props; try { - return getUserManager().getUserProperties(UserHandle.of(userHandle)) - .isCredentialShareableWithParent(); + props = getUserManager().getUserProperties(UserHandle.of(userHandle)); } catch (IllegalArgumentException e) { + if (throwIfUserNotExist == DoThrow) { + throw e; + } return false; } + return props.isCredentialShareableWithParent(); + } + + public static class SecondaryForCredSharableUserException extends IllegalArgumentException { + public SecondaryForCredSharableUserException() { + super("Credential sharable users do not support biometric second factor"); + } + } + + public static class SecondaryForSpecialUserException extends IllegalArgumentException { + public SecondaryForSpecialUserException() { + super("Special users do not support biometric second factor"); + } + } + + /** + * @return True if user supports biometric second factor, or false if user does not exist. + * @throws IllegalArgumentException If user does not support biometric second factor. + */ + public boolean checkUserSupportsBiometricSecondFactor(int userId) { + return checkUserSupportsBiometricSecondFactor(userId, true); + } + + public boolean checkUserSupportsBiometricSecondFactor(int userId, boolean throwIfNotSupport) { + // LockSettingsService/LockSettingsStorage don't use checkDeviceSupported argument. + if (isSpecialUserId(userId)) { + if (throwIfNotSupport) { + throw new SecondaryForSpecialUserException(); + } + return false; + } + + boolean sharable; + try { + sharable = isCredentialShareableWithParent(userId, DoThrow); + } catch (IllegalArgumentException e) { + return false; + } + + if (sharable) { + if (throwIfNotSupport) { + throw new SecondaryForCredSharableUserException(); + } + return false; + } + return true; + } + + public boolean checkUserSupportsBiometricSecondFactorIfSecondary(int userId, + LockDomain lockDomain) { + if (lockDomain == Primary) { + return true; + } + return checkUserSupportsBiometricSecondFactor(userId); } /** @@ -1021,22 +1198,27 @@ private String getSalt(int userId) { /** * Retrieve the credential type of a user. */ - private final PropertyInvalidatedCache.QueryHandler mCredentialTypeQuery = - new PropertyInvalidatedCache.QueryHandler<>() { - @Override - public Integer apply(Integer userHandle) { - try { - return getLockSettings().getCredentialType(userHandle); - } catch (RemoteException re) { - Log.e(TAG, "failed to get credential type", re); - return CREDENTIAL_TYPE_NONE; - } - } - @Override - public boolean shouldBypassCache(Integer userHandle) { - return isSpecialUserId(userHandle); - } - }; + class CredendialTypeQueryHandler extends PropertyInvalidatedCache.QueryHandler { + private final LockDomain mLockDomain; + + CredendialTypeQueryHandler(LockDomain lockDomain) { + this.mLockDomain = lockDomain; + } + + @Override + public Integer apply(Integer userHandle) { + try { + return getLockSettings().getCredentialType(userHandle, mLockDomain); + } catch (RemoteException re) { + Log.e(TAG, "failed to get credential type", re); + return CREDENTIAL_TYPE_NONE; + } + } + @Override + public boolean shouldBypassCache(Integer userHandle) { + return isSpecialUserId(userHandle); + } + }; /** * The API that is cached. @@ -1046,9 +1228,17 @@ public boolean shouldBypassCache(Integer userHandle) { /** * Cache the credential type of a user. */ - private final PropertyInvalidatedCache mCredentialTypeCache = + private final PropertyInvalidatedCache mPrimaryCredentialTypeCache = + // TODO: maxEntires should be maximum number of concurrent users allowed. + new PropertyInvalidatedCache<>(4, PropertyInvalidatedCache.MODULE_SYSTEM, + CREDENTIAL_TYPE_API, CREDENTIAL_TYPE_API, + new CredendialTypeQueryHandler(Primary)); + + private final PropertyInvalidatedCache mSecondaryCredentialTypeCache = + // TODO: maxEntires should be maximum number of concurrent users allowed. new PropertyInvalidatedCache<>(4, PropertyInvalidatedCache.MODULE_SYSTEM, - CREDENTIAL_TYPE_API, CREDENTIAL_TYPE_API, mCredentialTypeQuery); + CREDENTIAL_TYPE_API, CREDENTIAL_TYPE_API + "_secondary", + new CredendialTypeQueryHandler(Secondary)); /** * Invalidate the credential type cache @@ -1065,7 +1255,12 @@ public final static void invalidateCredentialTypeCache() { * {@link #CREDENTIAL_TYPE_PASSWORD} */ public @CredentialType int getCredentialTypeForUser(int userHandle) { - return mCredentialTypeCache.query(userHandle); + return getCredentialTypeForUser(userHandle, Primary); + } + + public @CredentialType int getCredentialTypeForUser(int userHandle, LockDomain lockDomain) { + var cache = lockDomain == Primary ? mPrimaryCredentialTypeCache : mSecondaryCredentialTypeCache; + return cache.query(userHandle); } /** @@ -1074,7 +1269,11 @@ public final static void invalidateCredentialTypeCache() { */ @UnsupportedAppUsage public boolean isSecure(int userId) { - int type = getCredentialTypeForUser(userId); + return isSecure(userId, Primary); + } + + public boolean isSecure(int userId, LockDomain lockDomain) { + int type = getCredentialTypeForUser(userId, lockDomain); return type != CREDENTIAL_TYPE_NONE; } @@ -1123,7 +1322,14 @@ public boolean isVisiblePatternEverChosen(int userId) { * @return Whether enhanced pin privacy is enabled. */ public boolean isPinEnhancedPrivacyEnabled(int userId) { - boolean defaultValue = false; + return isPinEnhancedPrivacyEnabled(userId, Primary); + } + + public boolean isPinEnhancedPrivacyEnabled(int userId, LockDomain lockDomain) { + if (!checkUserSupportsBiometricSecondFactorIfSecondary(userId, lockDomain)) { + return true; + } + boolean defaultValue = true; if (enableDefaultVisibilityForSensitiveInputs()) { defaultValue = mContext.getResources() @@ -1131,30 +1337,49 @@ public boolean isPinEnhancedPrivacyEnabled(int userId) { com.android.internal.R.bool .config_lockPinEnhancedPrivacyDefault); } - return getBoolean(LOCK_PIN_ENHANCED_PRIVACY, defaultValue, userId); + String key = lockDomain == Primary ? LOCK_PIN_ENHANCED_PRIVACY : LOCK_PIN_ENHANCED_PRIVACY_SECONDARY; + return getBoolean(key, defaultValue, userId); } /** * Set whether enhanced pin privacy is enabled. */ public void setPinEnhancedPrivacyEnabled(boolean enabled, int userId) { - setBoolean(LOCK_PIN_ENHANCED_PRIVACY, enabled, userId); + setPinEnhancedPrivacyEnabled(enabled, userId, Primary); + } + + public void setPinEnhancedPrivacyEnabled(boolean enabled, int userId, LockDomain lockDomain) { + if (!checkUserSupportsBiometricSecondFactorIfSecondary(userId, lockDomain)) { + return; + } + String key = lockDomain == Primary ? LOCK_PIN_ENHANCED_PRIVACY : LOCK_PIN_ENHANCED_PRIVACY_SECONDARY; + setBoolean(key, enabled, userId); } /** * @return Whether enhanced pin privacy was ever chosen. */ public boolean isPinEnhancedPrivacyEverChosen(int userId) { - return getString(LOCK_PIN_ENHANCED_PRIVACY, userId) != null; + return isPinEnhancedPrivacyEverChosen(userId, Primary); } + public boolean isPinEnhancedPrivacyEverChosen(int userId, LockDomain lockDomain) { + if (!checkUserSupportsBiometricSecondFactorIfSecondary(userId, lockDomain)) { + return false; + } + String key = lockDomain == Primary ? LOCK_PIN_ENHANCED_PRIVACY : LOCK_PIN_ENHANCED_PRIVACY_SECONDARY; + return getString(key, userId) != null; + } + + record LockoutEndTimeQuery(int userId, LockDomain lockDomain) {} + /** Retrieve the lockout end time of a user. */ - private final PropertyInvalidatedCache.QueryHandler mLockoutEndTimeQuery = + private final PropertyInvalidatedCache.QueryHandler mLockoutEndTimeQuery = new PropertyInvalidatedCache.QueryHandler<>() { @Override - public Duration apply(Integer userId) { + public Duration apply(LockoutEndTimeQuery query) { try { - return getLockSettings().getLockoutEndTime(userId).getDuration(); + return getLockSettings().getLockoutEndTime(query.userId, query.lockDomain).getDuration(); } catch (RemoteException re) { Log.e(TAG, "failed to get lockout end time", re); return Duration.ZERO; @@ -1166,7 +1391,7 @@ public Duration apply(Integer userId) { @VisibleForTesting static final String LOCKOUT_END_TIME_API = "getLockoutEndTime"; /** Cache the lockout end time of a user. */ - private final PropertyInvalidatedCache mLockoutEndTimeCache = + private final PropertyInvalidatedCache mLockoutEndTimeCache = new PropertyInvalidatedCache<>( 4, PropertyInvalidatedCache.MODULE_SYSTEM, @@ -1189,10 +1414,15 @@ public static final void invalidateLockoutEndTimeCache() { * Duration#ZERO} if the user is currently allowed. */ public Duration getLockoutEndTime(int userId) { - Duration lockoutEndTime = mLockoutEndTimeCache.query(userId); + return getLockoutEndTime(userId, Primary); + } + + public Duration getLockoutEndTime(int userId, LockDomain lockDomain) { + var query = new LockoutEndTimeQuery(userId, lockDomain); + Duration lockoutEndTime = mLockoutEndTimeCache.query(query); if (!lockoutEndTime.isZero() && lockoutEndTime.compareTo(mTimeSinceBootSupplier.get()) <= 0) { - return mLockoutEndTimeCache.recompute(userId); + return mLockoutEndTimeCache.recompute(query); } return lockoutEndTime; } @@ -1997,4 +2227,38 @@ public RemoteLockscreenValidationResult validateRemoteLockscreen( throw e.rethrowFromSystemServer(); } } + + public static List validateDuressCredential( + @NonNull LockscreenCredential credential) { + var metrics = new PasswordMetrics(credential.getType()); + metrics.length = 4; + return PasswordMetrics.validateCredential(metrics, DevicePolicyManager.PASSWORD_COMPLEXITY_LOW, credential); + } + + public void setDuressCredentials(@NonNull LockscreenCredential ownerCredential, + @NonNull LockscreenCredential duressPin, + @NonNull LockscreenCredential duressPassword) { + try { + getLockSettings().setDuressCredentials(ownerCredential, duressPin, duressPassword); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + public void deleteDuressCredentials(@NonNull LockscreenCredential ownerCredential) { + var noCredential = LockscreenCredential.createNone(); + try { + getLockSettings().setDuressCredentials(ownerCredential, noCredential, noCredential); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + public boolean hasDuressCredentials(@NonNull LockscreenCredential ownerCredential) { + try { + return getLockSettings().hasDuressCredentials(ownerCredential); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } } diff --git a/core/java/com/android/internal/widget/WrappedLockPatternUtils.java b/core/java/com/android/internal/widget/WrappedLockPatternUtils.java new file mode 100644 index 0000000000000..dcd17bdd5bf8f --- /dev/null +++ b/core/java/com/android/internal/widget/WrappedLockPatternUtils.java @@ -0,0 +1,64 @@ +package com.android.internal.widget; + +import static com.android.internal.widget.LockDomain.Primary; +import static com.android.internal.widget.LockDomain.Secondary; + +import android.content.Context; + +public class WrappedLockPatternUtils { + private final LockPatternUtils mInner; + private final LockDomain mLockDomain; + + public WrappedLockPatternUtils(LockPatternUtils inner, LockDomain lockDomain) { + mInner = inner; + mLockDomain = lockDomain; + } + + public WrappedLockPatternUtils(Context context, LockDomain lockDomain) { + mInner = new LockPatternUtils(context); + mLockDomain = lockDomain; + } + + public LockPatternUtils getInner() { + return mInner; + } + + public LockDomain getLockDomain() { + return mLockDomain; + } + + public @LockPatternUtils.CredentialType int getCredentialTypeForUser(int userHandle) { + return mInner.getCredentialTypeForUser(userHandle, mLockDomain); + } + + public void setPinEnhancedPrivacyEnabled(boolean enabled, int userId) { + mInner.setPinEnhancedPrivacyEnabled(enabled, userId, mLockDomain); + } + + public boolean isPinEnhancedPrivacyEnabled(int userId) { + return mInner.isPinEnhancedPrivacyEnabled(userId, mLockDomain); + } + + public int getPinLength(int userId) { + // This technique allows us to avoid modifying upstream tests in cases where we can mock + // the inner LPU but not the WLPU. + if (mLockDomain == Primary) { + return mInner.getPinLength(userId); + } + return mInner.getPinLength(userId, Secondary); + } + + public int getCurrentFailedPasswordAttempts(int userId) { + if (mLockDomain == Primary) { + return mInner.getCurrentFailedPasswordAttempts(userId); + } + return mInner.getCurrentFailedPasswordAttempts(userId, Secondary); + } + + public boolean isAutoPinConfirmEnabled(int userId) { + if (mLockDomain == Primary) { + return mInner.isAutoPinConfirmEnabled(userId); + } + return mInner.isAutoPinConfirmEnabled(userId, Secondary); + } +} diff --git a/core/java/com/android/server/pm/pkg/AndroidPackage.java b/core/java/com/android/server/pm/pkg/AndroidPackage.java index 92a6ae15fb544..c80f34502b8f6 100644 --- a/core/java/com/android/server/pm/pkg/AndroidPackage.java +++ b/core/java/com/android/server/pm/pkg/AndroidPackage.java @@ -48,6 +48,7 @@ import android.util.SparseIntArray; import com.android.internal.R; +import com.android.internal.pm.parsing.pkg.PackageExtIface; import com.android.internal.pm.pkg.component.ParsedActivity; import com.android.internal.pm.pkg.component.ParsedAllowComponentAccessPolicy; import com.android.internal.pm.pkg.component.ParsedApexSystemService; @@ -1555,6 +1556,10 @@ public interface AndroidPackage { */ int getIntentMatchingFlags(); + /** @hide */ + @Immutable.Ignore + PackageExtIface ext(); + /** * Returns true if this application has any component that should run in * the Private Compute Core sandbox. diff --git a/core/jni/android_app_ActivityThread.cpp b/core/jni/android_app_ActivityThread.cpp index e25ba76cbbeb6..16a8d4656179a 100644 --- a/core/jni/android_app_ActivityThread.cpp +++ b/core/jni/android_app_ActivityThread.cpp @@ -33,7 +33,7 @@ static void android_app_ActivityThread_initZygoteChildHeapProfiling(JNIEnv* env, android_mallopt(M_INIT_ZYGOTE_CHILD_PROFILING, nullptr, 0); } -static JNINativeMethod gActivityThreadMethods[] = { +static const JNINativeMethod gActivityThreadMethods[] = { // ------------ Regular JNI ------------------ { "nPurgePendingResources", "()V", (void*) android_app_ActivityThread_purgePendingResources }, diff --git a/core/jni/android_media_ImageWriter.cpp b/core/jni/android_media_ImageWriter.cpp index b10f1bfa9af5f..dd8139a0c6e95 100644 --- a/core/jni/android_media_ImageWriter.cpp +++ b/core/jni/android_media_ImageWriter.cpp @@ -1124,7 +1124,7 @@ static jobjectArray Image_createSurfacePlanes(JNIEnv* env, jobject thiz, // ---------------------------------------------------------------------------- -static JNINativeMethod gImageWriterMethods[] = { +static const JNINativeMethod gImageWriterMethods[] = { {"nativeClassInit", "()V", (void*)ImageWriter_classInit }, {"nativeInit", "(Ljava/lang/Object;Landroid/view/Surface;IIIZIIJ)J", (void*)ImageWriter_init }, @@ -1141,7 +1141,7 @@ static JNINativeMethod gImageWriterMethods[] = { {"cancelImage", "(JLandroid/media/Image;)V", (void*)ImageWriter_cancelImage }, }; -static JNINativeMethod gImageMethods[] = { +static const JNINativeMethod gImageMethods[] = { {"nativeCreatePlanes", "(II)[Landroid/media/ImageWriter$WriterSurfaceImage$SurfacePlane;", (void*)Image_createSurfacePlanes }, {"nativeGetWidth", "()I", (void*)Image_getWidth }, diff --git a/core/jni/android_os_HidlMemory.cpp b/core/jni/android_os_HidlMemory.cpp index 69e48184c0ad3..612fc95776a56 100644 --- a/core/jni/android_os_HidlMemory.cpp +++ b/core/jni/android_os_HidlMemory.cpp @@ -50,7 +50,7 @@ static void nativeFinalize(JNIEnv* env, jobject jobj) { delete native; } -static JNINativeMethod gMethods[] = { +static const JNINativeMethod gMethods[] = { {"nativeFinalize", "()V", (void*) nativeFinalize}, }; diff --git a/core/jni/android_os_HwBinder.cpp b/core/jni/android_os_HwBinder.cpp index 8663fba31e839..572afecc9fb90 100644 --- a/core/jni/android_os_HwBinder.cpp +++ b/core/jni/android_os_HwBinder.cpp @@ -399,7 +399,7 @@ static void JHwBinder_report_sysprop_change(JNIEnv * /*env*/, jclass /*clazz*/) report_sysprop_change(); } -static JNINativeMethod gMethods[] = { +static const JNINativeMethod gMethods[] = { { "native_init", "()J", (void *)JHwBinder_native_init }, { "native_setup", "()V", (void *)JHwBinder_native_setup }, diff --git a/core/jni/android_os_HwBlob.cpp b/core/jni/android_os_HwBlob.cpp index df579af0acb44..28459e6275a7e 100644 --- a/core/jni/android_os_HwBlob.cpp +++ b/core/jni/android_os_HwBlob.cpp @@ -600,7 +600,7 @@ static jlong JHwBlob_native_handle(JNIEnv *env, jobject thiz) { return handle; } -static JNINativeMethod gMethods[] = { +static const JNINativeMethod gMethods[] = { { "native_init", "()J", (void *)JHwBlob_native_init }, { "native_setup", "(I)V", (void *)JHwBlob_native_setup }, diff --git a/core/jni/android_os_HwParcel.cpp b/core/jni/android_os_HwParcel.cpp index e264e1501842e..2873b1a15d44f 100644 --- a/core/jni/android_os_HwParcel.cpp +++ b/core/jni/android_os_HwParcel.cpp @@ -1074,7 +1074,7 @@ static void JHwParcel_native_writeBuffer( } } -static JNINativeMethod gMethods[] = { +static const JNINativeMethod gMethods[] = { { "native_init", "()J", (void *)JHwParcel_native_init }, { "native_setup", "(Z)V", (void *)JHwParcel_native_setup }, diff --git a/core/jni/android_os_HwRemoteBinder.cpp b/core/jni/android_os_HwRemoteBinder.cpp index 3b77842c6e71d..212fc6cc58b1f 100644 --- a/core/jni/android_os_HwRemoteBinder.cpp +++ b/core/jni/android_os_HwRemoteBinder.cpp @@ -454,7 +454,7 @@ static jint JHwRemoteBinder_hashCode(JNIEnv* env, jobject thiz) { return static_cast(longHash ^ (longHash >> 32)); // See Long.hashCode() } -static JNINativeMethod gMethods[] = { +static const JNINativeMethod gMethods[] = { { "native_init", "()J", (void *)JHwRemoteBinder_native_init }, { "native_setup_empty", "()V", diff --git a/core/jni/android_os_NativeZygoteProcess.cpp b/core/jni/android_os_NativeZygoteProcess.cpp index c90dd0133b340..0c53c5a7713d6 100644 --- a/core/jni/android_os_NativeZygoteProcess.cpp +++ b/core/jni/android_os_NativeZygoteProcess.cpp @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -58,7 +59,7 @@ static std::optional extract_jstring(JNIEnv* env, jstring manage return ScopedUtfChars(env, managed_string); } -static void CreateSpawnParcel(flatbuffers::FlatBufferBuilder& builder, JNIEnv* env, jint uid, +static void CreateSpawnParcel(flatbuffers::FlatBufferBuilder& builder, JNIEnv* env, jlong selinux_flags, jint uid, jint gid, const char* niceNameStr, bool is_child_zygote, const char* seInfoStr, const char* socketPathStr, SpawnPayload payload_type, flatbuffers::Offset payload) { @@ -76,7 +77,7 @@ static void CreateSpawnParcel(flatbuffers::FlatBufferBuilder& builder, JNIEnv* e std::vector secondary_groups; std::vector rlimits; auto spawnCommon = - CreateSpawnCommonDirect(builder, uid, gid, niceNameStr, priority_initial, + CreateSpawnCommonDirect(builder, selinux_flags, uid, gid, niceNameStr, priority_initial, priority_final, cap_effective, cap_permitted, cap_inheritable, cap_bound, seInfoStr, &secondary_groups, &rlimits); flatbuffers::Offset spawnUnion; @@ -136,14 +137,30 @@ bool waitUntilNativeZygoteReady() { namespace android { +static jint captureCommand(JNIEnv* env, uint8_t* buf, ssize_t size, jobjectArray outCapturedCommand) { + if (size >= INT32_MAX) { + jniThrowIOException(env, E2BIG); + return -1; + } + jbyteArray out = env->NewByteArray((jint) size); + if (out == nullptr) { + return -1; + } + env->SetByteArrayRegion(out, 0, (jint) size, (jbyte*) buf); + env->SetObjectArrayElement(outCapturedCommand, 0, out); + return 0; +} + static jint android_os_NativeZygoteProcess_startNativeProcess( - JNIEnv* env, jclass /* classObj */, jobject sockFd, jint uid, jint gid, jlong startSeq, + JNIEnv* env, jclass /* classObj */, + jobjectArray outCapturedCommand, jlong selinuxFlags, jbyteArray preloadCommand, + jobject sockFd, jint uid, jint gid, jlong startSeq, jstring packageName, jstring niceName, jint targetSdkVersion, jboolean startChildZygote, jint runtimeFlags, jstring seInfo, jboolean isTopApp) { - int fd = jniGetFDFromFileDescriptor(env, sockFd); - if (fd < 0) { - jniThrowRuntimeException(env, "Failed to get a valid file descriptor"); - return -1; + std::vector preloadCommandVec; + if (preloadCommand != nullptr) { + ScopedByteArrayRO sba(env, preloadCommand); + preloadCommandVec.insert(preloadCommandVec.end(), sba.get(), sba.get() + sba.size()); } auto packageNameStr = extract_jstring(env, packageName); auto niceNameStr = extract_jstring(env, niceName); @@ -155,15 +172,24 @@ static jint android_os_NativeZygoteProcess_startNativeProcess( flatbuffers::FlatBufferBuilder builder; auto spawnAndroidNativeCmd = - CreateSpawnAndroidNativeDirect(builder, packageNamePtr, startSeq, targetSdkVersion, + CreateSpawnAndroidNativeDirect(builder, &preloadCommandVec, packageNamePtr, startSeq, targetSdkVersion, static_cast(runtimeFlags), is_top_app); bool is_child_zygote = startChildZygote == JNI_TRUE; - CreateSpawnParcel(builder, env, uid, gid, niceNamePtr, is_child_zygote, seInfoPtr, + CreateSpawnParcel(builder, env, selinuxFlags, uid, gid, niceNamePtr, is_child_zygote, seInfoPtr, /**subspecies_data=*/nullptr, SpawnPayload_SpawnAndroidNative, spawnAndroidNativeCmd.Union()); uint8_t* buf = builder.GetBufferPointer(); ssize_t size = builder.GetSize(); + if (outCapturedCommand != nullptr) { + return captureCommand(env, buf, size, outCapturedCommand); + } + + int fd = jniGetFDFromFileDescriptor(env, sockFd); + if (fd < 0) { + jniThrowRuntimeException(env, "Failed to get a valid file descriptor"); + return -1; + } ssize_t written = write(fd, buf, size); if (written == -1 || written != size) { jniThrowIOException(env, errno); @@ -174,16 +200,11 @@ static jint android_os_NativeZygoteProcess_startNativeProcess( } static jint android_os_NativeZygoteProcess_startNativeChildZygote( - JNIEnv* env, jclass /* classObj */, jobject sockFd, jint uid, jint gid, jstring niceName, + JNIEnv* env, jclass /* classObj */, jobjectArray outCapturedCommand, jlong selinuxFlags, jobject sockFd, jint uid, jint gid, jstring niceName, jstring seInfo, jint targetSdkVersion, jint runtimeFlags, jstring serverPath, jint uidRangeStart, jint uidRangeEnd, jstring allowedLibPath, jstring librarySearchPaths, jboolean isShared, jstring zipPath, jstring nativeSharedLibPath, jstring libraryPath, jstring preloadFunc) { - int fd = jniGetFDFromFileDescriptor(env, sockFd); - if (fd < 0) { - jniThrowRuntimeException(env, "Failed to get a valid file descriptor"); - return -1; - } auto niceNameChars = extract_jstring(env, niceName); auto seInfoChars = extract_jstring(env, seInfo); auto libraryPathChars = extract_jstring(env, libraryPath); @@ -215,13 +236,24 @@ static jint android_os_NativeZygoteProcess_startNativeChildZygote( zipPathStr, nativeSharedLibPathStr, preloadFuncStr, uidRangeStart, uidRangeEnd); - CreateSpawnParcel(builder, env, uid, gid, niceNameStr, /**is_child_zygote=*/true, seInfoStr, + if (outCapturedCommand != nullptr) { + builder.Finish(spawnAndroidChildZygoteCmd); + return captureCommand(env, builder.GetBufferPointer(), builder.GetSize(), outCapturedCommand); + } + + CreateSpawnParcel(builder, env, selinuxFlags, uid, gid, niceNameStr, /**is_child_zygote=*/true, seInfoStr, serverPathStr, SpawnPayload_SpawnSubspeciesAndroidNative, spawnAndroidChildZygoteCmd.Union()); uint8_t* buf = builder.GetBufferPointer(); ssize_t size = builder.GetSize(); + int fd = jniGetFDFromFileDescriptor(env, sockFd); + if (fd < 0) { + jniThrowRuntimeException(env, "Failed to get a valid file descriptor"); + return -1; + } + ssize_t written = write(fd, buf, size); if (written == -1 || written != size) { jniThrowIOException(env, errno); @@ -261,10 +293,10 @@ static void android_os_NativeZygoteProcess_prewarmNativeZygote(JNIEnv* env, jcla static const JNINativeMethod method_table[] = { /* name, signature, funcPtr */ {"nativeStartNativeProcess", - "(Ljava/io/FileDescriptor;IIJLjava/lang/String;Ljava/lang/String;IZILjava/lang/String;Z)I", + "([[BJ[BLjava/io/FileDescriptor;IIJLjava/lang/String;Ljava/lang/String;IZILjava/lang/String;Z)I", (void*)android_os_NativeZygoteProcess_startNativeProcess}, {"nativeStartNativeChildZygote", - "(Ljava/io/FileDescriptor;IILjava/lang/String;Ljava/lang/String;II" + "([[BJLjava/io/FileDescriptor;IILjava/lang/String;Ljava/lang/String;II" "Ljava/lang/String;IILjava/lang/String;Ljava/lang/String;ZLjava/lang/String;Ljava/lang/" "String;Ljava/lang/String;" "Ljava/lang/String;)I", diff --git a/core/jni/com_android_internal_os_Zygote.cpp b/core/jni/com_android_internal_os_Zygote.cpp index 15a7ee4a0a392..bf72fbb15ebcd 100644 --- a/core/jni/com_android_internal_os_Zygote.cpp +++ b/core/jni/com_android_internal_os_Zygote.cpp @@ -39,6 +39,7 @@ #include #include #include +#include #include #include #include @@ -56,12 +57,14 @@ #include #include #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -171,6 +174,8 @@ static int gUsapPoolEventFD = -1; */ static int gSystemServerSocketFd = -1; +static bool gIsExecSpawning = false; + static constexpr int DEFAULT_DATA_DIR_PERMISSION = 0751; static constexpr const uint64_t UPPER_HALF_WORD_MASK = 0xFFFF'FFFF'0000'0000; @@ -358,6 +363,29 @@ enum RuntimeFlags : uint32_t { AUDIT_OUTGOING_TRANSACTIONS = 1 << 27, }; +namespace ExtraArgsFlag { + static const int FORCIBLY_ENABLE_MEMORY_TAGGING = 1 << 2; +} + +struct ExtraArgs { + uint64_t selinux_flags = 0; + int flags = 0; + + ExtraArgs() {} + + ExtraArgs(JNIEnv* env, jlongArray jlongArgs) { + const size_t num_jlong_args = 2; + jlong jlong_arr[num_jlong_args]; + env->GetLongArrayRegion(jlongArgs, 0, num_jlong_args, (jlong *) &jlong_arr); + selinux_flags = (uint64_t) jlong_arr[0]; + flags = (int) jlong_arr[1]; + } + + bool hasFlag(int flag) { + return flags & flag; + } +}; + enum UnsolicitedZygoteMessageTypes : uint32_t { UNSOLICITED_ZYGOTE_MESSAGE_TYPE_RESERVED = 0, UNSOLICITED_ZYGOTE_MESSAGE_TYPE_SIGCHLD = 1, @@ -915,6 +943,9 @@ static void DetachDescriptors(JNIEnv* env, } for (int fd : fds_to_close) { + if (fd == -1 && gIsExecSpawning) { + continue; + } ALOGV("Switching descriptor %d to /dev/null", fd); if (TEMP_FAILURE_RETRY(dup3(devnull_fd, fd, O_CLOEXEC)) == -1) { fail_fn(StringPrintf("Failed dup3() on descriptor %d: %s", fd, strerror(errno))); @@ -1729,130 +1760,13 @@ static void WaitUntilDirReady(const std::string& target, fail_fn_t fail_fn) { fail_fn(CREATE_ERROR("Error dir is not ready %s: %s", dir_path, strerror(errno))); } -// All public String android.os.Build constants, and the system properties they're pulled from -std::pair build_constants[] = { - std::pair("ID", "ro.build.id"), - std::pair("DISPLAY", "ro.build.display.id"), - std::pair("PRODUCT", "ro.product.name"), - std::pair("DEVICE", "ro.product.device"), - std::pair("BOARD", "ro.product.board"), - std::pair("MANUFACTURER", "ro.product.manufacturer"), - std::pair("BRAND", "ro.product.brand"), - std::pair("MODEL", "ro.product.model"), - std::pair("BOOTLOADER", "ro.bootloader"), - std::pair("HARDWARE", "ro.hardware"), - std::pair("SKU", "ro.boot.hardware.sku"), - std::pair("ODM_SKU", "ro.boot.product.hardware.sku"), - std::pair("TAGS", "ro.build.tags"), - std::pair("TYPE", "ro.build.type"), - std::pair("USER", "ro.build.user"), - std::pair("HOST", "ro.build.host"), -}; - -// All public String Build.VERSION constants, and the system properties they're pulled from -std::pair build_version_constants[] = { - std::pair("INCREMENTAL", "ro.build.version.incremental"), - std::pair("RELEASE", "ro.build.version.release"), - std::pair("RELEASE_OR_CODENAME", "ro.build.version.release_or_codename"), - std::pair("RELEASE_OR_PREVIEW_DISPLAY", "ro.build.version.release_or_preview_display"), - std::pair("BASE_OS", "ro.build.version.base_os"), - std::pair("SECURITY_PATCH", "ro.build.version.security_patch"), - std::pair("PREVIEW_SDK_FINGERPRINT", "ro.build.version.preview_sdk_fingerprint"), - std::pair("CODENAME", "ro.build.version.codename"), -}; - -static void ReloadBuildJavaConstant(JNIEnv* env, jclass build_class, const char* field_name, - const char* field_signature, const char* sysprop_name) { - const prop_info* prop_info = __system_property_find(sysprop_name); - if (prop_info == nullptr) { - return; - } - std::string new_value; - __system_property_read_callback( - prop_info, - [](void* cookie, const char* name, const char* value, unsigned serial) { - auto new_value = reinterpret_cast(cookie); - *new_value = value; - }, - &new_value); - jfieldID fieldId = env->GetStaticFieldID(build_class, field_name, field_signature); - if (strcmp(field_signature, "I") == 0) { - env->SetStaticIntField(build_class, fieldId, jint(strtol(new_value.c_str(), nullptr, 0))); - } else if (strcmp(field_signature, "Ljava/lang/String;") == 0) { - jstring string_val = env->NewStringUTF(new_value.c_str()); - env->SetStaticObjectField(build_class, fieldId, string_val); - } else if (strcmp(field_signature, "[Ljava/lang/String;") == 0) { - auto stream = std::stringstream(new_value); - std::vector items; - std::string segment; - while (std::getline(stream, segment, ',')) { - items.push_back(segment); - } - jclass string_class = env->FindClass("java/lang/String"); - jobjectArray string_arr = env->NewObjectArray(items.size(), string_class, nullptr); - for (size_t i = 0; i < items.size(); i++) { - jstring string_arr_val = env->NewStringUTF(items.at(i).c_str()); - env->SetObjectArrayElement(string_arr, i, string_arr_val); - } - env->SetStaticObjectField(build_class, fieldId, string_arr); - } else if (strcmp(field_signature, "J") == 0) { - env->SetStaticLongField(build_class, fieldId, jlong(strtoll(new_value.c_str(), nullptr, 0))); - } -} - -static void ReloadBuildJavaConstants(JNIEnv* env) { - jclass build_cls = env->FindClass("android/os/Build"); - size_t arr_size = sizeof(build_constants) / sizeof(build_constants[0]); - for (size_t i = 0; i < arr_size; i++) { - const char* field_name = build_constants[i].first; - const char* sysprop_name = build_constants[i].second; - ReloadBuildJavaConstant(env, build_cls, field_name, "Ljava/lang/String;", sysprop_name); - } - jclass build_version_cls = env->FindClass("android/os/Build$VERSION"); - arr_size = sizeof(build_version_constants) / sizeof(build_version_constants[0]); - for (size_t i = 0; i < arr_size; i++) { - const char* field_name = build_version_constants[i].first; - const char* sysprop_name = build_version_constants[i].second; - ReloadBuildJavaConstant(env, build_version_cls, field_name, "Ljava/lang/String;", sysprop_name); - } - - // Reload the public String[] constants - ReloadBuildJavaConstant(env, build_cls, "SUPPORTED_ABIS", "[Ljava/lang/String;", - "ro.product.cpu.abilist"); - ReloadBuildJavaConstant(env, build_cls, "SUPPORTED_32_BIT_ABIS", "[Ljava/lang/String;", - "ro.product.cpu.abilist32"); - ReloadBuildJavaConstant(env, build_cls, "SUPPORTED_64_BIT_ABIS", "[Ljava/lang/String;", - "ro.product.cpu.abilist64"); - ReloadBuildJavaConstant(env, build_version_cls, "ALL_CODENAMES", "[Ljava/lang/String;", - "ro.build.version.all_codenames"); - - // Reload the public int/long constants - ReloadBuildJavaConstant(env, build_cls, "TIME", "J", "ro.build.date.utc"); - ReloadBuildJavaConstant(env, build_version_cls, "PREVIEW_SDK_INT", "I", - "ro.build.version.preview_sdk"); - - // Re-derive the fingerprint - jmethodID derive_fingerprint = - env->GetStaticMethodID(build_cls, "deriveFingerprint", "()Ljava/lang/String;"); - auto new_fingerprint = (jstring)(env->CallStaticObjectMethod(build_cls, derive_fingerprint)); - jfieldID fieldId = env->GetStaticFieldID(build_cls, "FINGERPRINT", "Ljava/lang/String;"); - env->SetStaticObjectField(build_cls, fieldId, new_fingerprint); -} - static void BindMountSyspropOverride(fail_fn_t fail_fn, JNIEnv* env) { // Reload the system properties file, to ensure new values are read into memory __system_properties_zygote_reload(); - // android.os.Build constants are pulled from system properties, so they must be reloaded, too - ReloadBuildJavaConstants(env); -} -static void MountInitOverride(fail_fn_t fail_fn, JNIEnv* env) { - const char* init_etc_dir = "/system/etc/init"; - - if (TEMP_FAILURE_RETRY(mount("tmpfs", init_etc_dir, "tmpfs", MS_NOSUID | MS_NODEV | MS_NOEXEC, - "uid=0,gid=0,mode=0751")) == -1) { - fail_fn(CREATE_ERROR("Failed to mount tmpfs %s: %s", init_etc_dir, strerror(errno))); - } + // ReloadBuildJavaConstants() is not needed currently, only ro.boot.verifiedbootstate is overridden, + // which doesn't affect android.os.Build constants + // TODO: when ReloadBuildJavaConstants is needed, make it skip reloading of unaffected constants } static void BindMountStorageToLowerFs(const userid_t user_id, const uid_t uid, @@ -1910,7 +1824,7 @@ static void BindMountStorageDirs(JNIEnv* env, jobjectArray pkg_data_info_list, } // Utility routine to specialize a zygote child process. -static void SpecializeCommon(JNIEnv* env, uid_t uid, gid_t gid, jintArray gids, jint runtime_flags, +static void SpecializeCommon(JNIEnv* env, ExtraArgs& extra_args, uid_t uid, gid_t gid, jintArray gids, jint runtime_flags, jobjectArray rlimits, jlong permitted_capabilities, jlong effective_capabilities, jlong bounding_capabilities, jint mount_external, jstring managed_se_info, @@ -1928,6 +1842,19 @@ static void SpecializeCommon(JNIEnv* env, uid_t uid, gid_t gid, jintArray gids, auto instruction_set = extract_fn(managed_instruction_set); auto app_data_dir = extract_fn(managed_app_data_dir); + { + unsigned max_fds = is_system_server ? 256 * 1024 : 32 * 1024; + + rlimit rl = {}; + rl.rlim_cur = max_fds; + rl.rlim_max = max_fds; + + if (setrlimit(RLIMIT_NOFILE, &rl) == -1) { + fail_fn(CREATE_ERROR("setrlimit(RLIMIT_NOFILE, {%ld, %ld}) failed: %s", + rl.rlim_cur, rl.rlim_max, strerror(errno))); + } + } + // Permit bounding capabilities permitted_capabilities |= bounding_capabilities; @@ -1977,7 +1904,6 @@ static void SpecializeCommon(JNIEnv* env, uid_t uid, gid_t gid, jintArray gids, if (mount_sysprop_overrides) { BindMountSyspropOverride(fail_fn, env); - MountInitOverride(fail_fn, env); } // If this zygote isn't root, it won't be able to create a process group, @@ -2108,6 +2034,16 @@ static void SpecializeCommon(JNIEnv* env, uid_t uid, gid_t gid, jintArray gids, } mallopt(M_BIONIC_SET_HEAP_TAGGING_LEVEL, heap_tagging_level); + if (extra_args.hasFlag(ExtraArgsFlag::FORCIBLY_ENABLE_MEMORY_TAGGING)) { + ALOGD("FORCIBLY_ENABLE_MEMORY_TAGGING is set, enabling M_BIONIC_BLOCK_HEAP_TAGGING_LEVEL_DOWNGRADE and M_BIONIC_ENABLE_SIGCHAINLIB_MTE_SIGSEGV_INTERCEPTION"); + if (mallopt(M_BIONIC_BLOCK_HEAP_TAGGING_LEVEL_DOWNGRADE, 0) != (int) true) { + RuntimeAbort(env, __LINE__, "mallopt(M_BIONIC_BLOCK_HEAP_TAGGING_LEVEL_DOWNGRADE) failed"); + } + if (mallopt(M_BIONIC_ENABLE_SIGCHAINLIB_MTE_SIGSEGV_INTERCEPTION, 0) != (int) true) { + RuntimeAbort(env, __LINE__, "mallopt(M_BIONIC_ENABLE_SIGCHAINLIB_MTE_SIGSEGV_INTERCEPTION) failed"); + } + } + // Now that we've used the flag, clear it so that we don't pass unknown flags to the ART // runtime. runtime_flags &= ~RuntimeFlags::MEMORY_TAG_LEVEL_MASK; @@ -2171,9 +2107,9 @@ static void SpecializeCommon(JNIEnv* env, uid_t uid, gid_t gid, jintArray gids, const char* se_info_ptr = se_info.has_value() ? se_info.value().c_str() : nullptr; - if (selinux_android_setcontext(uid, is_system_server, se_info_ptr, nice_name_ptr) == -1) { - fail_fn(CREATE_ERROR("selinux_android_setcontext(%d, %d, \"%s\", \"%s\") failed", uid, - is_system_server, se_info_ptr, nice_name_ptr)); + if (selinux_android_setcontext2(uid, is_system_server, se_info_ptr, nice_name_ptr, extra_args.selinux_flags) == -1) { + fail_fn(CREATE_ERROR("selinux_android_setcontext(%d, %d, \"%s\", \"%s\", selinux_flags: \"%" PRIx64 "\") failed", uid, + is_system_server, se_info_ptr, nice_name_ptr, extra_args.selinux_flags)); } if ((runtime_flags & RuntimeFlags::AUDIT_OUTGOING_TRANSACTIONS) != 0) { @@ -2518,7 +2454,7 @@ pid_t zygote::ForkCommon(JNIEnv* env, bool is_system_server, android_fdsan_error_level fdsan_error_level = android_fdsan_get_error_level(); - if (purge) { + if (!gIsExecSpawning && purge) { // Purge unused native memory in an attempt to reduce the amount of false // sharing with the child process. By reducing the size of the libc_malloc // region shared with the child process we reduce the number of pages that @@ -2529,7 +2465,7 @@ pid_t zygote::ForkCommon(JNIEnv* env, bool is_system_server, } } - pid_t pid = fork(); + pid_t pid = gIsExecSpawning ? 0 : fork(); if (pid == 0) { if (is_top_app && use_fifo_ui) { @@ -2553,7 +2489,7 @@ pid_t zygote::ForkCommon(JNIEnv* env, bool is_system_server, #if defined(__BIONIC__) && !defined(NO_RESET_STACK_PROTECTOR) // Reset the stack guard for the new process. - android_reset_stack_guards(); + if (!gIsExecSpawning) android_reset_stack_guards(); #endif // The child process. @@ -2597,6 +2533,8 @@ pid_t zygote::ForkCommon(JNIEnv* env, bool is_system_server, setpriority(PRIO_PROCESS, 0, PROCESS_PRIORITY_DEFAULT); } + gIsExecSpawning = false; + return pid; } @@ -2606,12 +2544,13 @@ static void com_android_internal_os_Zygote_nativePreApplicationInit(JNIEnv*, jcl NO_STACK_PROTECTOR static jint com_android_internal_os_Zygote_nativeForkAndSpecialize( - JNIEnv* env, jclass, jint uid, jint gid, jintArray gids, jint runtime_flags, + JNIEnv* env, jclass, jlongArray extra_jlong_args, jint uid, jint gid, jintArray gids, jint runtime_flags, jobjectArray rlimits, jint mount_external, jstring se_info, jstring nice_name, jintArray managed_fds_to_close, jintArray managed_fds_to_ignore, jboolean is_child_zygote, jstring instruction_set, jstring app_data_dir, jboolean is_top_app, jboolean use_fifo_ui, jobjectArray pkg_data_info_list, jobjectArray allowlisted_data_info_list, jboolean mount_data_dirs, jboolean mount_storage_dirs, jboolean mount_sysprop_overrides) { + ExtraArgs extra_args(env, extra_jlong_args); // Containers run without some capabilities, so drop any caps that are not available. jlong capabilities = zygote::CalculateCapabilities(env, uid, gid, gids, is_child_zygote) & GetEffectiveCapabilityMask(env); @@ -2653,7 +2592,7 @@ static jint com_android_internal_os_Zygote_nativeForkAndSpecialize( true, is_top_app == JNI_TRUE, use_fifo_ui == JNI_TRUE); if (pid == 0) { - SpecializeCommon(env, uid, gid, gids, runtime_flags, rlimits, capabilities, capabilities, + SpecializeCommon(env, extra_args, uid, gid, gids, runtime_flags, rlimits, capabilities, capabilities, bounding_capabilities, mount_external, se_info, nice_name, false, is_child_zygote == JNI_TRUE, instruction_set, app_data_dir, is_top_app == JNI_TRUE, pkg_data_info_list, allowlisted_data_info_list, @@ -2691,7 +2630,8 @@ static jint com_android_internal_os_Zygote_nativeForkSystemServer( if (pid == 0) { // System server prcoess does not need data isolation so no need to // know pkg_data_info_list. - SpecializeCommon(env, uid, gid, gids, runtime_flags, rlimits, permitted_capabilities, + ExtraArgs extra_args; + SpecializeCommon(env, extra_args, uid, gid, gids, runtime_flags, rlimits, permitted_capabilities, effective_capabilities, 0, MOUNT_EXTERNAL_DEFAULT, nullptr, nullptr, true, false, nullptr, nullptr, /* is_top_app= */ false, /* pkg_data_info_list */ nullptr, @@ -2838,7 +2778,7 @@ static void com_android_internal_os_Zygote_nativeInstallSeccompUidGidFilter( * @param is_top_app If the process is for top (high priority) application */ static void com_android_internal_os_Zygote_nativeSpecializeAppProcess( - JNIEnv* env, jclass, jint uid, jint gid, jintArray gids, jint runtime_flags, + JNIEnv* env, jclass, jlongArray extra_jlong_args, jint uid, jint gid, jintArray gids, jint runtime_flags, jobjectArray rlimits, jint mount_external, jstring se_info, jstring nice_name, jboolean is_child_zygote, jstring instruction_set, jstring app_data_dir, jboolean is_top_app, jobjectArray pkg_data_info_list, @@ -2848,8 +2788,9 @@ static void com_android_internal_os_Zygote_nativeSpecializeAppProcess( jlong capabilities = zygote::CalculateCapabilities(env, uid, gid, gids, is_child_zygote) & GetEffectiveCapabilityMask(env); jlong bounding_capabilities = zygote::CalculateBoundingCapabilities(env, uid, gid, gids); + ExtraArgs extra_args(env, extra_jlong_args); - SpecializeCommon(env, uid, gid, gids, runtime_flags, rlimits, capabilities, capabilities, + SpecializeCommon(env, extra_args, uid, gid, gids, runtime_flags, rlimits, capabilities, capabilities, bounding_capabilities, mount_external, se_info, nice_name, false, is_child_zygote == JNI_TRUE, instruction_set, app_data_dir, is_top_app == JNI_TRUE, pkg_data_info_list, allowlisted_data_info_list, @@ -2866,28 +2807,38 @@ static void com_android_internal_os_Zygote_nativeSpecializeAppProcess( * of the environment variable storing the file descriptors. */ static void com_android_internal_os_Zygote_nativeInitNativeState(JNIEnv* env, jclass, - jboolean is_primary) { + jboolean is_exec_spawning, + jstring j_socket_name, + jstring j_usap_pool_socket_name) { + gIsExecSpawning = is_exec_spawning; /* * Obtain file descriptors created by init from the environment. */ - gZygoteSocketFD = - android_get_control_socket(is_primary ? "zygote" : "zygote_secondary"); - if (gZygoteSocketFD >= 0) { - ALOGV("Zygote:zygoteSocketFD = %d", gZygoteSocketFD); - } else { - ALOGE("Unable to fetch Zygote socket file descriptor"); - } + if (!gIsExecSpawning) { + ScopedUtfChars socket_name(env, j_socket_name); + ScopedUtfChars usap_pool_socket_name(env, j_usap_pool_socket_name); - gUsapPoolSocketFD = - android_get_control_socket(is_primary ? "usap_pool_primary" : "usap_pool_secondary"); - if (gUsapPoolSocketFD >= 0) { - ALOGV("Zygote:usapPoolSocketFD = %d", gUsapPoolSocketFD); - } else { - ALOGE("Unable to fetch USAP pool socket file descriptor"); + gZygoteSocketFD = + android_get_control_socket(socket_name.c_str()); + if (gZygoteSocketFD >= 0) { + ALOGV("Zygote:zygoteSocketFD = %d", gZygoteSocketFD); + } else { + ALOGE("Unable to fetch Zygote socket file descriptor"); + } + + gUsapPoolSocketFD = + android_get_control_socket(usap_pool_socket_name.c_str()); + if (gUsapPoolSocketFD >= 0) { + ALOGV("Zygote:usapPoolSocketFD = %d", gUsapPoolSocketFD); + } else { + ALOGE("Unable to fetch USAP pool socket file descriptor"); + } } - initUnsolSocketToSystemServer(); + if (!gIsExecSpawning) { + initUnsolSocketToSystemServer(); + } /* * Security Initialization @@ -2912,7 +2863,9 @@ static void com_android_internal_os_Zygote_nativeInitNativeState(JNIEnv* env, jc * Storage Initialization */ - UnmountStorageOnInit(env); + if (!gIsExecSpawning) { + UnmountStorageOnInit(env); + } /* * Performance Initialization @@ -3109,6 +3062,192 @@ static jint com_android_internal_os_Zygote_nativeCurrentTaggingLevel(JNIEnv* env #endif // defined(__aarch64__) } +static void free_environ(char** env) { + for (size_t i = 0; env[i] != nullptr; ++i) { + free(env[i]); + } + free(env); +} + +static char** clone_environ(const char*const* extra_variables) { + size_t count = 0; + + while (environ[count] != nullptr) { + ++count; + } + + size_t num_extra_vars = 0; + while (extra_variables[num_extra_vars] != nullptr) { + ++num_extra_vars; + } + + // 1 extra slot for NULL terminator + char** new_environ = (char**) calloc(count + num_extra_vars + 1, sizeof(char*)); + if (new_environ == nullptr) { + return nullptr; + } + + for (size_t i = 0; i < count; ++i) { + new_environ[i] = strdup(environ[i]); + if (new_environ[i] == nullptr) { + free_environ(new_environ); + return nullptr; + } + } + + for (size_t i = 0; i < num_extra_vars; ++i) { + new_environ[count + i] = strdup(extra_variables[i]); + if (new_environ[count + i] == nullptr) { + free_environ(new_environ); + return nullptr; + } + } + + return new_environ; +} +// sigprocmask functions filter out the reserved bionic signals, which is undesirable +static int __rt_sigprocmask(int how, const sigset64_t* new_set, sigset64_t* old_set) { + return (int) syscall(SYS_rt_sigprocmask, how, new_set, old_set, sizeof(sigset64_t)); +} + +static jint com_android_internal_os_Zygote_nativeForkExec(JNIEnv* env, jclass, + jboolean is_64_bit, + jboolean is_native_app_launch, + jbyteArray command_buf, + jboolean disable_hardened_malloc, + jboolean enable_compat_va_39_bit) { + int cmd_fd = memfd_create("zygote_fork_exec_cmds", 0); + if (cmd_fd < 0) { + ALOGE("memfd_create failed: %#m"); + return -1; + } + size_t cmd_buf_size; + { + ScopedByteArrayRO cmd(env, command_buf); + cmd_buf_size = cmd.size(); + if (!android::base::WriteFully(cmd_fd, cmd.get(), cmd_buf_size)) { + ALOGE("WriteFully failed: %#m"); + close(cmd_fd); + return -1; + } + + if (lseek(cmd_fd, 0, SEEK_SET) != 0) { + ALOGE("lseek(cmd_fd) failed: %#m"); + close(cmd_fd); + return -1; + } + } + + char cmd_fd_arg[50]; + snprintf(cmd_fd_arg, sizeof(cmd_fd_arg), "--command-fd=%d_%zu", cmd_fd, cmd_buf_size); + + const char*const java_app_argv[] = { + // keep in sync with system/core/rootdir/init.zygote64.rc + is_64_bit ? "/system/bin/app_process64" : "/system/bin/app_process32", + "-Xzygote", + "/system/bin", + "--zygote", + cmd_fd_arg, + nullptr, + }; + const char*const native_app_argv[] = { + // keep in sync with system/zygote/zygote/zygote_next.rc + "/system/bin/zygote_next", + "--name=zygote_next_exec_spawn", + "--species=android-native-app", + "--log-level=INFO", + cmd_fd_arg, + nullptr, + }; + + const char*const* argv = is_native_app_launch ? native_app_argv : java_app_argv; + + const char*const extra_env_vars_no_hmalloc[] = { + "IS_EXEC_SPAWNED_APP_PROCESS=1", + "DISABLE_HARDENED_MALLOC=1", + nullptr, + }; + const char*const extra_env_vars[] = { + "IS_EXEC_SPAWNED_APP_PROCESS=1", + nullptr, + }; + + char** environment = clone_environ(disable_hardened_malloc ? extra_env_vars_no_hmalloc : extra_env_vars); + if (environment == nullptr) { + ALOGE("clone_environ failed: %#m"); + close(cmd_fd); + return -1; + } + + // Signal handlers are set at this point in 64-bit zygote since system_server is forked from it + // first, but 32-bit zygote won't have them set if this is the first app launch zygote command. + SetSignalHandlers(); + + sigset64_t full_sig_set; + sigfillset64(&full_sig_set); + + sigset64_t prev_sig_set; + + // ensure that no new file descriptors are racily opened by signal handlers in the child process + if (__rt_sigprocmask(SIG_BLOCK, &full_sig_set, &prev_sig_set) != 0) { + ALOGE("__rt_sigprocmask failed before fork: %#m"); + close(cmd_fd); + return -1; + } + + // fork() runs bionic fork hooks which are unnecessary for this use-case + pid_t pid = _Fork(); + + if (pid != 0) { + // parent process + if (pid == -1) { + ALOGE("fork failed: %#m"); + } + if (__rt_sigprocmask(SIG_SETMASK, &prev_sig_set, nullptr) != 0) { + ALOGE("__rt_sigprocmask failed in parent after fork: %#m"); + _exit(1); + } + close(cmd_fd); + free_environ(environment); + return pid; + } else { + // Set CLOEXEC for all file descriptors except for the command file descriptor. Note that + // the parent process is multithreaded at fork time since it has Java daemon threads in + // addition to the main thread. + if (close_range(0, cmd_fd - 1, CLOSE_RANGE_CLOEXEC) != 0) { + async_safe_format_log(ANDROID_LOG_ERROR, "ZygoteForkExec", "close_range(CLOSE_RANGE_CLOEXEC) up to %d failed: %#m", cmd_fd - 1); + _exit(1); + } + if (close_range(cmd_fd + 1, ~0U, CLOSE_RANGE_CLOEXEC) != 0) { + async_safe_format_log(ANDROID_LOG_ERROR, "ZygoteForkExec", "close_range(CLOSE_RANGE_CLOEXEC) from %d failed: %#m", cmd_fd + 1); + _exit(1); + } + + if (__rt_sigprocmask(SIG_SETMASK, &prev_sig_set, nullptr) != 0) { + async_safe_format_log(ANDROID_LOG_ERROR, "ZygoteForkExec", "__rt_sigprocmask failed in child after fork: %#m"); + _exit(1); + } + +#if defined(__aarch64__) + const int FLAG_COMPAT_VA_39_BIT = 1 << 30; + execveat(-1, argv[0], (char **) argv, environment, enable_compat_va_39_bit ? FLAG_COMPAT_VA_39_BIT : 0); + async_safe_format_log(ANDROID_LOG_ERROR, "ZygoteForkExec", "execveat failed: %#m"); + if (errno == EINVAL) { + // kernel doesn't support FLAG_COMPAT_VA_39_BIT, or a different error that will + // be returned by execve() anyway + execve(argv[0], (char **) argv, environment); + async_safe_format_log(ANDROID_LOG_ERROR, "ZygoteForkExec", "execve failed: %#m"); + } +#else + execve(argv[0], (char **) argv, environment); + async_safe_format_log(ANDROID_LOG_ERROR, "ZygoteForkExec", "execve failed: %#m"); +#endif // defined(__aarch64__) + + // exec failed + _exit(1); + } +} + static void com_android_internal_os_Zygote_nativeMarkOpenedFilesBeforePreload(JNIEnv* env, jclass) { // Ignore invocations when too early or too late. if (gPreloadFds) { @@ -3139,8 +3278,10 @@ static void com_android_internal_os_Zygote_nativeAllowFilesOpenedByPreload(JNIEn } static const JNINativeMethod gMethods[] = { + {"nativeForkExec", "(ZZ[BZZ)I", + (void*)com_android_internal_os_Zygote_nativeForkExec}, {"nativeForkAndSpecialize", - "(II[II[[IILjava/lang/String;Ljava/lang/String;[I[IZLjava/lang/String;Ljava/lang/" + "([JII[II[[IILjava/lang/String;Ljava/lang/String;[I[IZLjava/lang/String;Ljava/lang/" "String;ZZ[Ljava/lang/String;[Ljava/lang/String;ZZZ)I", (void*)com_android_internal_os_Zygote_nativeForkAndSpecialize}, {"nativeForkSystemServer", "(II[II[[IJJ)I", @@ -3156,10 +3297,10 @@ static const JNINativeMethod gMethods[] = { {"nativeAddUsapTableEntry", "(II)V", (void*)com_android_internal_os_Zygote_nativeAddUsapTableEntry}, {"nativeSpecializeAppProcess", - "(II[II[[IILjava/lang/String;Ljava/lang/String;ZLjava/lang/String;Ljava/lang/" + "([JII[II[[IILjava/lang/String;Ljava/lang/String;ZLjava/lang/String;Ljava/lang/" "String;Z[Ljava/lang/String;[Ljava/lang/String;ZZZ)V", (void*)com_android_internal_os_Zygote_nativeSpecializeAppProcess}, - {"nativeInitNativeState", "(Z)V", + {"nativeInitNativeState", "(ZLjava/lang/String;Ljava/lang/String;)V", (void*)com_android_internal_os_Zygote_nativeInitNativeState}, {"nativeGetUsapPipeFDs", "()[I", (void*)com_android_internal_os_Zygote_nativeGetUsapPipeFDs}, diff --git a/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp b/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp index 2fbe4e8a6b878..5c886ba920215 100644 --- a/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp +++ b/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp @@ -160,6 +160,8 @@ class NativeCommandBuffer { if (mLinesLeft <= 0 || mLinesLeft >= static_cast(MAX_COMMAND_BYTES / 2)) { return std::make_pair(false, false); } + static const char* IS_COMPLEX_CMD = "--is-complex-zygote-command"; + static const size_t ICC_LENGTH = strlen(IS_COMPLEX_CMD); static const char* RUNTIME_ARGS = "--runtime-args"; static const char* INVOKE_WITH = "--invoke-with"; static const char* CHILD_ZYGOTE = "--start-child-zygote"; @@ -187,6 +189,11 @@ class NativeCommandBuffer { return std::make_pair(false, false); } const auto [arg_start, arg_end] = read_result.value(); + if (static_cast(arg_end - arg_start) == ICC_LENGTH && + strncmp(arg_start, IS_COMPLEX_CMD, ICC_LENGTH) == 0) { + // return to the Java code to handle exec spawning + return std::make_pair(false, false); + } if (static_cast(arg_end - arg_start) == RA_LENGTH && strncmp(arg_start, RUNTIME_ARGS, RA_LENGTH) == 0) { saw_runtime_args = true; diff --git a/core/jni/fd_utils.cpp b/core/jni/fd_utils.cpp index bf6ff81c9294f..ba4d49cef832f 100644 --- a/core/jni/fd_utils.cpp +++ b/core/jni/fd_utils.cpp @@ -36,8 +36,10 @@ static const char* kPathAllowlist[] = { "/dev/null", "/dev/socket/zygote", + "/dev/socket/zygote_compat", "/dev/socket/zygote_secondary", "/dev/socket/usap_pool_primary", + "/dev/socket/usap_pool_compat", "/dev/socket/usap_pool_secondary", "/dev/socket/webview_zygote", "/dev/socket/heapprofd", diff --git a/core/jni/platform/host/HostRuntime.cpp b/core/jni/platform/host/HostRuntime.cpp index 063c6c504575c..3dc69ff5c9972 100644 --- a/core/jni/platform/host/HostRuntime.cpp +++ b/core/jni/platform/host/HostRuntime.cpp @@ -63,7 +63,7 @@ static void NativeAllocationRegistry_applyFreeFunction(JNIEnv*, jclass, jlong fr nativeFreeFunction(nativePtr); } -static JNINativeMethod gMethods[] = { +static const JNINativeMethod gMethods[] = { NATIVE_METHOD(NativeAllocationRegistry, applyFreeFunction, "(JJ)V"), }; diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 20556db3bed42..fa2c3f0534c0d 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -595,6 +595,7 @@ + @@ -2049,6 +2050,18 @@ android:permissionFlags="allowedInPrivateComputeCore" /> + + + + @@ -2322,13 +2335,21 @@ + + + + android:protectionLevel="dangerous|instant" /> + + + + @@ -3254,6 +3283,10 @@ + + + + + + + + @@ -5792,6 +5833,11 @@ android:protectionLevel="signature|privileged|development" android:permissionFlags="allowedInPrivateComputeCore" /> + + + + @@ -7121,6 +7167,10 @@ + + + + + + + android:protectionLevel="normal" /> + + + + + + + + diff --git a/core/res/res/drawable/ic_pending.xml b/core/res/res/drawable/ic_pending.xml new file mode 100644 index 0000000000000..410b2d75018f8 --- /dev/null +++ b/core/res/res/drawable/ic_pending.xml @@ -0,0 +1,11 @@ + + + + diff --git a/core/res/res/drawable/ic_update.xml b/core/res/res/drawable/ic_update.xml new file mode 100644 index 0000000000000..8574d79d58a60 --- /dev/null +++ b/core/res/res/drawable/ic_update.xml @@ -0,0 +1,11 @@ + + + + diff --git a/core/res/res/drawable/toast_frame.xml b/core/res/res/drawable/toast_frame.xml index a8cdef6d05f13..34987394b2ece 100644 --- a/core/res/res/drawable/toast_frame.xml +++ b/core/res/res/drawable/toast_frame.xml @@ -17,7 +17,7 @@ --> - + diff --git a/core/res/res/layout/app_anr_dialog.xml b/core/res/res/layout/app_anr_dialog.xml index 5ad0f4c0f6cc7..ad3a2d2991de8 100644 --- a/core/res/res/layout/app_anr_dialog.xml +++ b/core/res/res/layout/app_anr_dialog.xml @@ -41,8 +41,8 @@ android:id="@+id/aerr_report" android:layout_width="match_parent" android:layout_height="wrap_content" - android:text="@string/aerr_report" - android:drawableStart="@drawable/ic_feedback" + android:text="@string/aerr_show_details" + android:drawableStart="@drawable/ic_info_outline_24" style="@style/aerr_list_item" /> diff --git a/core/res/res/layout/app_error_dialog.xml b/core/res/res/layout/app_error_dialog.xml index c3b149a1e2959..a47b820183777 100644 --- a/core/res/res/layout/app_error_dialog.xml +++ b/core/res/res/layout/app_error_dialog.xml @@ -52,8 +52,8 @@ android:id="@+id/aerr_report" android:layout_width="match_parent" android:layout_height="wrap_content" - android:text="@string/aerr_report" - android:drawableStart="@drawable/ic_feedback" + android:text="@string/aerr_show_details" + android:drawableStart="@drawable/ic_info_outline_24" style="@style/aerr_list_item" />