diff --git a/apex/jobscheduler/framework/java/android/app/AlarmManager.java b/apex/jobscheduler/framework/java/android/app/AlarmManager.java index ec6a8b8af899..bb0127db5124 100644 --- a/apex/jobscheduler/framework/java/android/app/AlarmManager.java +++ b/apex/jobscheduler/framework/java/android/app/AlarmManager.java @@ -26,6 +26,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; @@ -1049,6 +1050,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 17076bc4eea4..b0078c34db66 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.client.ClientPriorityManager; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Collections; @@ -609,6 +612,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()) { + ClientPriorityManager.raiseToForeground(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 caf7e7f4a4ed..1b1d2252dae1 100644 --- a/apex/jobscheduler/framework/java/com/android/server/DeviceIdleInternal.java +++ b/apex/jobscheduler/framework/java/com/android/server/DeviceIdleInternal.java @@ -73,7 +73,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 8316a2625ccd..90c5c8de5946 100644 --- a/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java +++ b/apex/jobscheduler/service/java/com/android/server/DeviceIdleController.java @@ -2294,14 +2294,14 @@ public boolean isAppOnWhitelist(int appid) { } /** - * 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 d5c9ae615486..9e3ebb9cf6bc 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 @@ -90,7 +90,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)); @@ -133,7 +133,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(); @@ -194,7 +194,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 f429966e042a..a278eac114b9 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 @@ -2275,6 +2275,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) != + 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/app_process/app_main.cpp b/cmds/app_process/app_main.cpp index 28db61f7d98a..ffcf9fae31cd 100644 --- a/cmds/app_process/app_main.cpp +++ b/cmds/app_process/app_main.cpp @@ -85,8 +85,10 @@ class AppRuntime : public AndroidRuntime AndroidRuntime* ar = AndroidRuntime::getRuntime(); ar->callMain(mClassName, mClass, mArgs); - IPCThreadState::self()->stopProcess(); - hardware::IPCThreadState::self()->stopProcess(); + if (mClassName != "com.android.internal.os.ExecInit") { + IPCThreadState::self()->stopProcess(); + hardware::IPCThreadState::self()->stopProcess(); + } } virtual void onZygoteInit() diff --git a/cmds/hid/jni/com_android_commands_hid_Device.cpp b/cmds/hid/jni/com_android_commands_hid_Device.cpp index 8b8d361edbd4..60aa5e4267aa 100644 --- a/cmds/hid/jni/com_android_commands_hid_Device.cpp +++ b/cmds/hid/jni/com_android_commands_hid_Device.cpp @@ -368,7 +368,7 @@ static void closeDevice(JNIEnv* /* env */, jclass /* clazz */, jlong ptr) { } } -static JNINativeMethod sMethods[] = { +static const JNINativeMethod sMethods[] = { {"nativeOpenDevice", "(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 3f4163dc54ec..fefb373095ef 100644 --- a/cmds/uinput/jni/com_android_commands_uinput_Device.cpp +++ b/cmds/uinput/jni/com_android_commands_uinput_Device.cpp @@ -307,7 +307,7 @@ static void setAbsInfo(JNIEnv* env, jclass /* clazz */, jint handle, jint axisCo ::ioctl(static_cast(handle), UI_ABS_SETUP, &absSetup); } -static JNINativeMethod sMethods[] = { +static const JNINativeMethod sMethods[] = { {"nativeOpenUinputDevice", "(Ljava/lang/String;IIIIILjava/lang/String;" "Lcom/android/commands/uinput/Device$DeviceCallback;)J", diff --git a/core/api/current.txt b/core/api/current.txt index 288ab479c0fb..45bb07b5bd20 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -219,6 +219,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 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"; field @Deprecated public static final String PERSISTENT_ACTIVITY = "android.permission.PERSISTENT_ACTIVITY"; @@ -329,7 +330,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"; @@ -42970,7 +42973,7 @@ package android.telecom { method public android.telecom.PhoneAccountHandle getSimCallManager(); method @Nullable public android.telecom.PhoneAccountHandle getSimCallManagerForSubscription(int); method @Nullable public String getSystemDialerPackage(); - method @Nullable @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public android.telecom.PhoneAccountHandle getUserSelectedOutgoingPhoneAccount(); + method @Nullable @RequiresPermission(anyOf={"android.permission.READ_PRIVILEGED_PHONE_STATE", "android.permission.READ_PRIVILEGED_PHONE_STATE_ANDROID_AUTO"}) public android.telecom.PhoneAccountHandle getUserSelectedOutgoingPhoneAccount(); method @RequiresPermission(android.Manifest.permission.READ_PHONE_STATE) public String getVoiceMailNumber(android.telecom.PhoneAccountHandle); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean handleMmi(String); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean handleMmi(String, android.telecom.PhoneAccountHandle); diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt index c1f6219025f9..b7c6bdf63194 100644 --- a/core/api/module-lib-current.txt +++ b/core/api/module-lib-current.txt @@ -80,6 +80,19 @@ 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 boolean interceptSynchronousResultReceiverException(@NonNull RuntimeException); + method public static void makeBluetoothAdapterDiscoverable(); + } + +} + package android.content { public abstract class ContentProvider implements android.content.ComponentCallbacks2 { @@ -128,6 +141,31 @@ package android.content.pm { } +package android.ext.settings { + + 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); + field public static final int WV_GRAPHENEOS_PROXY = 0; // 0x0 + field public static final int WV_STANDARD_SERVER = 1; // 0x1 + } + +} + package android.hardware.usb { public class UsbManager { @@ -450,6 +488,11 @@ package android.os { field public static final long TRACE_TAG_NETWORK = 2097152L; // 0x200000L } + public final class UserHandle implements android.os.Parcelable { + method public static int getUid(int, int); + method public static int getUserId(int); + } + } package android.os.storage { diff --git a/core/api/system-current.txt b/core/api/system-current.txt index ace7d59c9a45..195876ffb234 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -86,6 +86,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"; @@ -376,6 +377,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"; @@ -1098,6 +1100,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); @@ -2039,6 +2059,29 @@ package android.app.compat { } +package android.app.compat.gms { + + public class AndroidAuto { + field public static final long PKG_FLAG_GRANT_AUDIO_ROUTING_PERM = 4L; // 0x4L + field public static final long PKG_FLAG_GRANT_PERMS_FOR_ANDROID_AUTO_PHONE_CALLS = 8L; // 0x8L + field public static final long PKG_FLAG_GRANT_PERMS_FOR_WIRED_ANDROID_AUTO = 1L; // 0x1L + field public static final long PKG_FLAG_GRANT_PERMS_FOR_WIRELESS_ANDROID_AUTO = 2L; // 0x2L + } + + public final class GmsCompat { + method public static boolean hasPermission(@NonNull String); + method public static boolean isEnabled(); + method public static boolean isEnabledFor(@NonNull android.content.pm.ApplicationInfo); + method public static boolean isEnabledFor(int, boolean); + method public static boolean isEnabledFor(@NonNull String, int); + } + + public class GmsUtils { + method @NonNull public static android.content.Intent createAppPlayStoreIntent(@NonNull String); + } + +} + package android.app.contentsuggestions { public final class ClassificationsRequest implements android.os.Parcelable { @@ -3672,7 +3715,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(); @@ -3697,6 +3746,66 @@ package android.content.pm { method @NonNull public final int getType(); } + public final class GosPackageState implements android.os.Parcelable { + method public static boolean attachableToPackage(@NonNull String); + method public int describeContents(); + method @NonNull public android.content.pm.GosPackageState.Editor edit(); + method @NonNull public static android.content.pm.GosPackageState.Editor edit(@NonNull String); + method @NonNull public static android.content.pm.GosPackageState.Editor edit(@NonNull String, int); + method @Nullable public static android.content.pm.GosPackageState get(@NonNull String); + method @Nullable public static android.content.pm.GosPackageState get(@NonNull String, int); + method @Nullable public static android.content.pm.GosPackageState getForSelf(); + method @NonNull public static android.content.pm.GosPackageState getOrDefault(@NonNull String); + method @NonNull public static android.content.pm.GosPackageState getOrDefault(@NonNull String, int); + method @NonNull public String getPackageName(); + method public int getUserId(); + method public boolean hasDerivedFlag(int); + method public boolean hasDerivedFlags(int); + method public boolean hasFlag(int); + method public final boolean hasFlags(int); + method public final boolean hasPackageFlags(long); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator CREATOR; + field public static final int DFLAGS_SET = 1; // 0x1 + field public static final int DFLAG_EXPECTS_ACCESS_TO_MEDIA_FILES_ONLY = 4; // 0x4 + field public static final int DFLAG_EXPECTS_ALL_FILES_ACCESS = 2; // 0x2 + field public static final int DFLAG_EXPECTS_LEGACY_EXTERNAL_STORAGE = 8192; // 0x2000 + field public static final int DFLAG_EXPECTS_STORAGE_WRITE_ACCESS = 8; // 0x8 + field public static final int DFLAG_HAS_ACCESS_MEDIA_LOCATION_DECLARATION = 256; // 0x100 + field public static final int DFLAG_HAS_GET_ACCOUNTS_DECLARATION = 4194304; // 0x400000 + field public static final int DFLAG_HAS_MANAGE_EXTERNAL_STORAGE_DECLARATION = 64; // 0x40 + field public static final int DFLAG_HAS_MANAGE_MEDIA_DECLARATION = 128; // 0x80 + field public static final int DFLAG_HAS_READ_CONTACTS_DECLARATION = 1048576; // 0x100000 + field public static final int DFLAG_HAS_READ_EXTERNAL_STORAGE_DECLARATION = 16; // 0x10 + field public static final int DFLAG_HAS_READ_MEDIA_AUDIO_DECLARATION = 512; // 0x200 + field public static final int DFLAG_HAS_READ_MEDIA_IMAGES_DECLARATION = 1024; // 0x400 + field public static final int DFLAG_HAS_READ_MEDIA_VIDEO_DECLARATION = 2048; // 0x800 + field public static final int DFLAG_HAS_READ_MEDIA_VISUAL_USER_SELECTED_DECLARATION = 4096; // 0x1000 + field public static final int DFLAG_HAS_WRITE_CONTACTS_DECLARATION = 2097152; // 0x200000 + field public static final int DFLAG_HAS_WRITE_EXTERNAL_STORAGE_DECLARATION = 32; // 0x20 + field public static final int EDITOR_FLAG_KILL_UID_AFTER_APPLY = 1; // 0x1 + field public static final int EDITOR_FLAG_NOTIFY_UID_AFTER_APPLY = 2; // 0x2 + field public static final int FLAG_ALLOW_ACCESS_TO_OBB_DIRECTORY = 2; // 0x2 + field public static final int FLAG_CONTACT_SCOPES_ENABLED = 32; // 0x20 + field public static final int FLAG_STORAGE_SCOPES_ENABLED = 1; // 0x1 + field public final int derivedFlags; + } + + public static class GosPackageState.Editor { + method @NonNull public android.content.pm.GosPackageState.Editor addFlags(int); + method @NonNull public android.content.pm.GosPackageState.Editor addPackageFlags(long); + method public boolean apply(); + method @NonNull public android.content.pm.GosPackageState.Editor clearFlags(int); + method @NonNull public android.content.pm.GosPackageState.Editor clearPackageFlags(long); + 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 setFlagsState(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(long, boolean); + method @NonNull public android.content.pm.GosPackageState.Editor setStorageScopes(@Nullable byte[]); + } + public final class InstallationFile { method public long getLengthBytes(); method public int getLocation(); @@ -3907,6 +4016,7 @@ package android.content.pm { method public void replacePreferredActivity(@NonNull android.content.IntentFilter, int, @NonNull java.util.List, @NonNull android.content.ComponentName); 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); @@ -4091,6 +4201,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); @@ -4262,6 +4376,100 @@ 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 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; + field public static final int FLAG_HAS_GMSCORE_CLIENT_LIBRARY = 0; // 0x0 + } + + 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 int GSF = 1; // 0x1 + 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 PLAY_STORE = 3; // 0x3 + field public static final String PLAY_STORE_NAME = "com.android.vending"; + 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.graphics.fonts { public final class FontFamilyUpdateRequest { @@ -10241,6 +10449,11 @@ package android.nfc { 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 @@ -11290,6 +11503,7 @@ package android.permission { method @Deprecated @RequiresPermission(android.Manifest.permission.MANAGE_ONE_TIME_PERMISSION_SESSIONS) public void startOneTimePermissionSession(@NonNull String, long, int, int); method @RequiresPermission(android.Manifest.permission.MANAGE_ONE_TIME_PERMISSION_SESSIONS) public void startOneTimePermissionSession(@NonNull String, long, long, int, int); method @RequiresPermission(android.Manifest.permission.MANAGE_ONE_TIME_PERMISSION_SESSIONS) public void stopOneTimePermissionSession(@NonNull String); + method public void updatePermissionState(@NonNull String, int); field @RequiresPermission(android.Manifest.permission.START_REVIEW_PERMISSION_DECISIONS) public static final String ACTION_REVIEW_PERMISSION_DECISIONS = "android.permission.action.REVIEW_PERMISSION_DECISIONS"; field public static final String EXTRA_PERMISSION_USAGES = "android.permission.extra.PERMISSION_USAGES"; field public static final int PERMISSION_GRANTED = 0; // 0x0 diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index 6e0fc4f53a23..2bc7763e0c1e 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -47,6 +47,7 @@ import android.app.admin.DevicePolicyManager; import android.app.assist.AssistContent; 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; @@ -167,6 +168,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.PhoneWindow; import com.android.internal.util.dump.DumpableContainerImpl; @@ -1706,6 +1710,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) { @@ -5609,6 +5617,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 = @@ -5985,6 +6031,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 66d04a3132eb..f36d6ad0ec3f 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -34,6 +34,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; @@ -86,6 +87,8 @@ 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; @@ -3954,7 +3957,11 @@ public boolean setProcessMemoryTrimLevel(String process, int userId, int level) */ public List getRunningAppProcesses() { try { - return getService().getRunningAppProcesses(); + List res = getService().getRunningAppProcesses(); + if (GmsCompat.isEnabled()) { + res = GmsHooks.addRecentlyBoundPids(mContext, res); + } + return res; } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -4786,6 +4793,10 @@ public static int handleIncomingUser(int callingPid, int callingUid, int userId, "android.permission.INTERACT_ACROSS_USERS_FULL" }) public static int getCurrentUser() { + if (GmsCompat.isEnabled()) { + return GmcUserManager.amGetCurrentUser(); + } + try { return getService().getCurrentUserId(); } catch (RemoteException e) { @@ -5076,6 +5087,10 @@ public boolean stopUser(@UserIdInt int userId, boolean force) { */ @UnsupportedAppUsage public boolean isUserRunning(int userId) { + if (GmsCompat.isEnabled()) { + return GmcUserManager.amIsUserRunning(userId); + } + try { return getService().isUserRunning(userId, 0); } catch (RemoteException e) { diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java index 32c40df32585..17043f9da770 100644 --- a/core/java/android/app/ActivityManagerInternal.java +++ b/core/java/android/app/ActivityManagerInternal.java @@ -35,6 +35,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; @@ -1234,4 +1235,26 @@ public abstract void notifyMediaProjectionEvent(int uid, @NonNull IBinder projec */ public abstract boolean clearApplicationUserData(String packageName, boolean keepState, boolean isRestore, IPackageDataObserver observer, int userId); + + public abstract void onGosPackageStateChanged(int uid, @Nullable 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 f4caef0f2553..d57958f25a3f 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -82,6 +82,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; @@ -127,6 +128,7 @@ import android.os.Handler; import android.os.HandlerExecutor; import android.os.IBinder; +import android.os.IBinderCallback; import android.os.ICancellationSignal; import android.os.LocaleList; import android.os.Looper; @@ -359,6 +361,15 @@ public final class ActivityThread extends ClientTransactionHandler /** Maps from activity token to the pending override configuration. */ @GuardedBy("mPendingOverrideConfigs") private final ArrayMap mPendingOverrideConfigs = new ArrayMap<>(); + + /** + * A queue of pending ApplicationInfo updates. In case when we get a concurrent update + * this queue allows us to only apply the latest object, and it can be applied on demand + * instead of waiting for the handler thread to reach the scheduled callback. + */ + @GuardedBy("mResourcesManager") + private final ArrayMap mPendingAppInfoUpdates = new ArrayMap<>(); + /** The activities to be truly destroyed (not include relaunch). */ final Map mActivitiesToBeDestroyed = Collections.synchronizedMap(new ArrayMap()); @@ -1260,9 +1271,19 @@ public final void scheduleSuicide() { } public void scheduleApplicationInfoChanged(ApplicationInfo ai) { + synchronized (mResourcesManager) { + var oldAi = mPendingAppInfoUpdates.put(ai.packageName, ai); + if (oldAi != null && oldAi.createTimestamp > ai.createTimestamp) { + Slog.w(TAG, "Skipping application info changed for obsolete AI with TS " + + ai.createTimestamp + " < already pending TS " + + oldAi.createTimestamp); + mPendingAppInfoUpdates.put(ai.packageName, oldAi); + return; + } + } mResourcesManager.appendPendingAppInfoUpdate(new String[]{ai.sourceDir}, ai); - mH.removeMessages(H.APPLICATION_INFO_CHANGED, ai); - sendMessage(H.APPLICATION_INFO_CHANGED, ai); + mH.removeMessages(H.APPLICATION_INFO_CHANGED, ai.packageName); + sendMessage(H.APPLICATION_INFO_CHANGED, ai.packageName); } public void updateTimeZone() { @@ -2026,6 +2047,12 @@ public void updateUiTranslationState(IBinder activityToken, int state, args.arg6 = uiTranslationSpec; sendMessage(H.UPDATE_UI_TRANSLATION_STATE, args); } + + @Override + public void onGosPackageStateChanged(@Nullable GosPackageState state) { + // this is a oneway method, caller (ActivityManager) will not be blocked + ActivityThreadHooks.onGosPackageStateChanged(mInitialApplication, state, false); + } } private @NonNull SafeCancellationTransport createSafeCancellationTransport( @@ -2437,7 +2464,7 @@ public void handleMessage(Message msg) { break; } case APPLICATION_INFO_CHANGED: - handleApplicationInfoChanged((ApplicationInfo) msg.obj); + applyPendingApplicationInfoChanges((String) msg.obj); break; case RUN_ISOLATED_ENTRY_POINT: handleRunIsolatedEntryPoint((String) ((SomeArgs) msg.obj).arg1, @@ -3571,6 +3598,10 @@ public void updatePendingConfiguration(Configuration config) { } } + public int getProcessState() { + return mLastProcessState; + } + @Override public void updateProcessState(int processState, boolean fromIpc) { synchronized (mAppThread) { @@ -3922,7 +3953,8 @@ public Activity handleLaunchActivity(ActivityClientRecord r, mProfiler.startProfiling(); } - // Make sure we are running with the most recent config. + // Make sure we are running with the most recent config and resource paths. + applyPendingApplicationInfoChanges(r.activityInfo.packageName); mConfigurationController.handleConfigurationChanged(null, null); updateDeviceIdForNonUIContexts(deviceId); @@ -4642,8 +4674,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) { @@ -4820,6 +4858,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); @@ -6248,6 +6295,17 @@ private void handleWindowingModeChangeIfNeeded(ActivityClientRecord r, r.mLastReportedWindowingMode = newWindowingMode; } + private void applyPendingApplicationInfoChanges(String packageName) { + final ApplicationInfo ai; + synchronized (mResourcesManager) { + ai = mPendingAppInfoUpdates.remove(packageName); + } + if (ai == null) { + return; + } + handleApplicationInfoChanged(ai); + } + /** * Updates the application info. * @@ -6273,6 +6331,16 @@ public void handleApplicationInfoChanged(@NonNull final ApplicationInfo ai) { apk = ref != null ? ref.get() : null; ref = mResourcePackages.get(ai.packageName); resApk = ref != null ? ref.get() : null; + for (ActivityClientRecord ar : mActivities.values()) { + if (ar.activityInfo.applicationInfo.packageName.equals(ai.packageName)) { + ar.activityInfo.applicationInfo = ai; + if (apk != null || resApk != null) { + ar.packageInfo = apk != null ? apk : resApk; + } else { + apk = ar.packageInfo; + } + } + } } if (apk != null) { @@ -6895,6 +6963,7 @@ private void handleBindApplication(AppBindData data) { final IActivityManager mgr = ActivityManager.getService(); final ContextImpl appContext = ContextImpl.createAppContext(this, data.info); mConfigurationController.updateLocaleListFromAppContext(appContext); + final Bundle extraAppBindArgs = ActivityThreadHooks.onBind(appContext); // Initialize the default http proxy in this process. Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "Setup proxies"); @@ -6954,6 +7023,10 @@ private void handleBindApplication(AppBindData data) { dalvik.system.VMRuntime.getRuntime().clampGrowthLimit(); } + if (extraAppBindArgs != null) { + ActivityThreadHooks.onBind2(appContext, extraAppBindArgs); + } + // 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. @@ -7055,6 +7128,18 @@ private void handleBindApplication(AppBindData data) { } catch (RemoteException ex) { throw ex.rethrowFromSystemServer(); } + + // Set binder transaction callback after finishing bindApplication + Binder.setTransactionCallback(new IBinderCallback() { + @Override + public void onTransactionError(int pid, int code, int flags, int err) { + try { + mgr.frozenBinderTransactionDetected(pid, code, flags, err); + } catch (RemoteException ex) { + throw ex.rethrowFromSystemServer(); + } + } + }); } @UnsupportedAppUsage diff --git a/core/java/android/app/ActivityThreadHooks.java b/core/java/android/app/ActivityThreadHooks.java new file mode 100644 index 000000000000..5e4e5bd3d726 --- /dev/null +++ b/core/java/android/app/ActivityThreadHooks.java @@ -0,0 +1,83 @@ +package android.app; + +import android.annotation.Nullable; +import android.content.Context; +import android.content.pm.GosPackageState; +import android.content.pm.SrtPermissions; +import android.location.HookedLocationManager; +import android.os.Bundle; +import android.os.Process; +import android.os.RemoteException; +import android.util.Log; + +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(Context appContext) { + if (called) { + throw new IllegalStateException("onBind called for the second time"); + } + called = true; + + if (Process.isIsolated()) { + return null; + } + + final String pkgName = appContext.getPackageName(); + final String TAG = "AppBindArgs"; + + Bundle args = null; + try { + args = ActivityThread.getPackageManager().getExtraAppBindArgs(pkgName); + } catch (RemoteException e) { + Log.e(TAG, "", e); + } + + if (args == null) { + Log.e(TAG, "bundle is null"); + return null; + } + + 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]); + + 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, @Nullable GosPackageState state, boolean fromBind) { + if (state != null) { + 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 000000000000..68afdd7b54fa --- /dev/null +++ b/core/java/android/app/AppBindArgs.java @@ -0,0 +1,12 @@ +package android.app; + +/** @hide */ +public interface AppBindArgs { + String KEY_GOS_PACKAGE_STATE = "gosPs"; + String KEY_FLAGS_ARRAY = "flagsArr"; + + int FLAGS_IDX_SPECIAL_RUNTIME_PERMISSIONS = 0; + int FLAGS_IDX_HOOKED_LOCATION_MANAGER = 1; + + int FLAGS_ARRAY_LEN = 10; +} diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index ccd83f756730..ff4117a80dae 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -30,6 +30,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.compat.Compatibility; import android.compat.annotation.ChangeId; @@ -39,6 +40,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; @@ -8254,7 +8256,13 @@ public int unsafeCheckOpRawNoThrow(@NonNull String op, int uid, @NonNull String */ public int unsafeCheckOpRawNoThrow(int op, int uid, @NonNull String packageName) { try { - return mService.checkOperationRaw(op, uid, packageName, null); + final int mode = mService.checkOperationRaw(op, uid, packageName, null); + if (mode != MODE_ALLOWED && uid == Process.myUid()) { + if (AppPermissionUtils.shouldSpoofSelfAppOpCheck(op)) { + return MODE_ALLOWED; + } + } + return mode; } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } @@ -8405,6 +8413,10 @@ public int noteOpNoThrow(@NonNull String op, int uid, @NonNull String packageNam */ public int noteOpNoThrow(int op, int uid, @Nullable String packageName, @Nullable String attributionTag, @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); @@ -8428,7 +8440,15 @@ public 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(); } @@ -8603,7 +8623,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(); } @@ -8679,6 +8713,13 @@ public int checkOp(int op, int uid, String packageName) { public int checkOpNoThrow(int op, int uid, String packageName) { try { int mode = mService.checkOperation(op, uid, packageName); + + if (mode != MODE_ALLOWED && uid == Process.myUid()) { + if (AppPermissionUtils.shouldSpoofSelfAppOpCheck(op)) { + return MODE_ALLOWED; + } + } + return mode == AppOpsManager.MODE_FOREGROUND ? AppOpsManager.MODE_ALLOWED : mode; } catch (RemoteException e) { throw e.rethrowFromSystemServer(); @@ -8911,6 +8952,10 @@ public int startOpNoThrow(@NonNull IBinder token, int op, int uid, @NonNull Stri public int startOpNoThrow(@NonNull IBinder token, int op, int uid, @NonNull String packageName, boolean startIfModeDefault, @Nullable String attributionTag, @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); @@ -9136,6 +9181,11 @@ public void finishOp(int op, int uid, @NonNull String packageName, */ public void finishOp(IBinder token, int op, int uid, @NonNull String packageName, @Nullable String attributionTag) { + if (GmsCompat.isEnabled() && uid != Process.myUid()) { + finishProxyOp(opToPublicName(op), uid, packageName, attributionTag); + return; + } + try { mService.finishOperation(token, op, uid, packageName, attributionTag); } catch (RemoteException e) { diff --git a/core/java/android/app/Application.java b/core/java/android/app/Application.java index 2767b43a119c..fc3fd9108208 100644 --- a/core/java/android/app/Application.java +++ b/core/java/android/app/Application.java @@ -19,6 +19,7 @@ import android.annotation.CallSuper; 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; @@ -232,6 +233,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 5b57457a3315..34a05ab33ae1 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 9cea5e8ef4cf..22dccfd60a66 100644 --- a/core/java/android/app/ApplicationErrorReport.java +++ b/core/java/android/app/ApplicationErrorReport.java @@ -25,6 +25,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 +100,9 @@ public class ApplicationErrorReport implements Parcelable { */ public String packageName; + /** @hide */ + public long packageVersion; + /** * Package name of the application which installed the application this * report pertains to. @@ -162,13 +167,18 @@ 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 systemUiReceiver = getErrorReportReceiver(pm, packageName, "com.android.systemui"); + if (systemUiReceiver != null) { + return systemUiReceiver; + } + // look for receiver in the installer package String candidate = null; ComponentName result = null; @@ -233,6 +243,7 @@ static ComponentName getErrorReportReceiver(PackageManager pm, String errorPacka public void writeToParcel(Parcel dest, int flags) { dest.writeInt(type); dest.writeString(packageName); + dest.writeLong(packageVersion); dest.writeString(installerPackageName); dest.writeString(processName); dest.writeLong(time); @@ -260,6 +271,7 @@ public void writeToParcel(Parcel dest, int flags) { public void readFromParcel(Parcel in) { type = in.readInt(); packageName = in.readString(); + packageVersion = in.readLong(); installerPackageName = in.readString(); processName = in.readString(); time = in.readLong(); @@ -345,6 +357,11 @@ public static class CrashInfo { */ public String crashTag; + /** @hide */ + public long processUptimeMs; + /** @hide */ + public long processStartupLatencyMs; + /** * Create an uninitialized instance of CrashInfo. */ @@ -398,6 +415,9 @@ public CrashInfo(Throwable tr) { } exceptionMessage = sanitizeString(exceptionMessage); + + processUptimeMs = SystemClock.elapsedRealtime() - Process.getStartElapsedRealtime(); + processStartupLatencyMs = Process.getStartElapsedRealtime() - Process.getStartRequestedElapsedRealtime(); } /** {@hide} */ @@ -439,6 +459,8 @@ public CrashInfo(Parcel in) { throwLineNumber = in.readInt(); stackTrace = in.readString(); crashTag = in.readString(); + processUptimeMs = in.readLong(); + processStartupLatencyMs = in.readLong(); } /** @@ -455,6 +477,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); @@ -704,7 +728,7 @@ public int describeContents() { */ public void dump(Printer pw, String prefix) { pw.println(prefix + "type: " + type); - pw.println(prefix + "packageName: " + packageName); + pw.println(prefix + "packageName: " + packageName + ":" + packageVersion); pw.println(prefix + "installerPackageName: " + installerPackageName); pw.println(prefix + "processName: " + processName); pw.println(prefix + "time: " + time); diff --git a/core/java/android/app/ApplicationExitInfo.java b/core/java/android/app/ApplicationExitInfo.java index d859f3f9e175..24cb9ea87a12 100644 --- a/core/java/android/app/ApplicationExitInfo.java +++ b/core/java/android/app/ApplicationExitInfo.java @@ -460,6 +460,33 @@ public final class ApplicationExitInfo implements Parcelable { */ public static final int SUBREASON_SDK_SANDBOX_NOT_NEEDED = 28; + /** + * The process was killed because the binder proxy limit for system server was exceeded. + * + * For internal use only. + * @hide + */ + public static final int SUBREASON_EXCESSIVE_BINDER_OBJECTS = 29; + + /** + * The process was killed by the [kernel] Out-of-memory (OOM) killer; this + * would be set only when the reason is {@link #REASON_LOW_MEMORY}. + * + * For internal use only. + * @hide + */ + public static final int SUBREASON_OOM_KILL = 30; + + /** + * The process was killed because its async kernel binder buffer is running out + * while being frozen. + * this would be set only when the reason is {@link #REASON_FREEZER}. + * + * For internal use only. + * @hide + */ + public static final int SUBREASON_FREEZER_BINDER_ASYNC_FULL = 31; + // If there is any OEM code which involves additional app kill reasons, it should // be categorized in {@link #REASON_OTHER}, with subreason code starting from 1000. @@ -635,6 +662,9 @@ public final class ApplicationExitInfo implements Parcelable { SUBREASON_KILL_BACKGROUND, SUBREASON_PACKAGE_UPDATE, SUBREASON_UNDELIVERED_BROADCAST, + SUBREASON_EXCESSIVE_BINDER_OBJECTS, + SUBREASON_OOM_KILL, + SUBREASON_FREEZER_BINDER_ASYNC_FULL, }) @Retention(RetentionPolicy.SOURCE) public @interface SubReason {} @@ -1360,6 +1390,12 @@ public static String subreasonToString(@SubReason int subreason) { return "PACKAGE UPDATE"; case SUBREASON_UNDELIVERED_BROADCAST: return "UNDELIVERED BROADCAST"; + case SUBREASON_EXCESSIVE_BINDER_OBJECTS: + return "EXCESSIVE BINDER OBJECTS"; + case SUBREASON_OOM_KILL: + return "OOM KILL"; + case SUBREASON_FREEZER_BINDER_ASYNC_FULL: + return "FREEZER BINDER ASYNC FULL"; default: return "UNKNOWN"; } diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java index 0ba56b97d510..be7db1c4a990 100644 --- a/core/java/android/app/ApplicationPackageManager.java +++ b/core/java/android/app/ApplicationPackageManager.java @@ -37,6 +37,7 @@ import android.annotation.UserIdInt; import android.annotation.XmlRes; import android.app.admin.DevicePolicyManager; +import android.app.compat.gms.GmsCompat; import android.app.role.RoleManager; import android.compat.annotation.UnsupportedAppUsage; import android.content.ComponentName; @@ -120,6 +121,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.Immutable; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.gmscompat.sysservice.GmcPackageManager; import com.android.internal.os.SomeArgs; import com.android.internal.util.UserIcons; @@ -255,6 +257,7 @@ public PackageInfo getPackageInfoAsUser(String packageName, PackageInfoFlags fla if (pi == null) { throw new NameNotFoundException(packageName); } + GmcPackageManager.maybeAdjustPackageInfo(pi); return pi; } @@ -506,6 +509,9 @@ public ApplicationInfo getApplicationInfoAsUser(String packageName, ApplicationI if (ai == null) { throw new NameNotFoundException(packageName); } + + GmcPackageManager.maybeAdjustApplicationInfo(ai); + return maybeAdjustApplicationInfo(ai); } @@ -1679,12 +1685,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; } @Override @@ -2130,8 +2141,8 @@ static void configurationChanged() { } @UnsupportedAppUsage - protected ApplicationPackageManager(ContextImpl context, IPackageManager pm) { - mContext = context; + protected ApplicationPackageManager(Context context, IPackageManager pm) { + mContext = (ContextImpl) context; mPM = pm; } @@ -3911,4 +3922,25 @@ public void relinquishUpdateOwnership(String targetPackage) { throw e.rethrowFromSystemServer(); } } + + @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(); + } + } } diff --git a/core/java/android/app/BroadcastOptions.java b/core/java/android/app/BroadcastOptions.java index 9549ebf698a8..85d6a4a391e9 100644 --- a/core/java/android/app/BroadcastOptions.java +++ b/core/java/android/app/BroadcastOptions.java @@ -24,6 +24,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; @@ -563,6 +564,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 { @@ -775,6 +780,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 82f4315d033e..5aefa5aca5d5 100644 --- a/core/java/android/app/ContextImpl.java +++ b/core/java/android/app/ContextImpl.java @@ -20,13 +20,16 @@ import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.os.StrictMode.vmIncorrectContextUseEnabled; import static android.view.WindowManager.LayoutParams.WindowType; +import static com.android.internal.app.ContentProviderRedirector.translateContentProviderAuthority; +import android.Manifest; import android.annotation.CallbackExecutor; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; 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; @@ -98,6 +101,9 @@ import android.window.WindowTokenClient; import com.android.internal.annotations.GuardedBy; +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; @@ -428,7 +434,7 @@ public PackageManager getPackageManager() { final IPackageManager pm = ActivityThread.getPackageManager(); if (pm != null) { // Doesn't matter if we make more than one instance. - return (mPackageManager = new ApplicationPackageManager(this, pm)); + return (mPackageManager = GmsCompat.isEnabled() ? new GmcPackageManager(this, pm) : new ApplicationPackageManager(this, pm)); } return null; @@ -1316,6 +1322,10 @@ public void sendBroadcastMultiplePermissions(Intent intent, String[] receiverPer @Override public void sendBroadcast(Intent intent, String receiverPermission, Bundle options) { + if (GmsCompat.isEnabled()) { + options = GmsHooks.filterBroadcastOptions(intent, options); + } + warnIfCallingFromSystemProcess(); String resolvedType = intent.resolveTypeIfNeeded(getContentResolver()); String[] receiverPermissions = receiverPermission == null ? null @@ -1414,6 +1424,10 @@ void sendOrderedBroadcast(Intent intent, String receiverPermission, int appOp, BroadcastReceiver resultReceiver, Handler scheduler, int initialCode, String initialData, Bundle initialExtras, Bundle options) { + if (GmsCompat.isEnabled()) { + options = GmsHooks.filterBroadcastOptions(intent, options); + } + warnIfCallingFromSystemProcess(); IIntentReceiver rd = null; if (resultReceiver != null) { @@ -1449,6 +1463,10 @@ resultReceiver, getOuterContext(), scheduler, null, false) @Override public void sendBroadcastAsUser(Intent intent, UserHandle user) { + if (GmsCompat.isEnabled()) { + user = GmcUserManager.translateUserHandle(user); + } + String resolvedType = intent.resolveTypeIfNeeded(getContentResolver()); try { intent.prepareToLeaveProcess(this); @@ -1470,6 +1488,11 @@ public void sendBroadcastAsUser(Intent intent, UserHandle user, @Override public void sendBroadcastAsUser(Intent intent, UserHandle user, String receiverPermission, Bundle options) { + if (GmsCompat.isEnabled()) { + options = GmsHooks.filterBroadcastOptions(intent, options); + user = GmcUserManager.translateUserHandle(user); + } + String resolvedType = intent.resolveTypeIfNeeded(getContentResolver()); String[] receiverPermissions = receiverPermission == null ? null : new String[] {receiverPermission}; @@ -1488,6 +1511,10 @@ public void sendBroadcastAsUser(Intent intent, UserHandle user, String receiverP @Override public void sendBroadcastAsUser(Intent intent, UserHandle user, String receiverPermission, int appOp) { + if (GmsCompat.isEnabled()) { + user = GmcUserManager.translateUserHandle(user); + } + String resolvedType = intent.resolveTypeIfNeeded(getContentResolver()); String[] receiverPermissions = receiverPermission == null ? null : new String[] {receiverPermission}; @@ -1523,6 +1550,11 @@ public void sendOrderedBroadcastAsUser(Intent intent, UserHandle user, public void sendOrderedBroadcastAsUser(Intent intent, UserHandle user, String receiverPermission, int appOp, Bundle options, BroadcastReceiver resultReceiver, Handler scheduler, int initialCode, String initialData, Bundle initialExtras) { + if (GmsCompat.isEnabled()) { + options = GmsHooks.filterBroadcastOptions(intent, options); + user = GmcUserManager.translateUserHandle(user); + } + IIntentReceiver rd = null; if (resultReceiver != null) { if (mPackageInfo != null) { @@ -2116,6 +2148,25 @@ 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(Manifest.permission.START_ACTIVITIES_FROM_BACKGROUND)) { + flags &= ~BIND_ALLOW_BACKGROUND_ACTIVITY_STARTS; + } + } + + 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 @@ -2194,6 +2245,12 @@ public boolean startInstrumentation(ComponentName className, @Override 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()) { @@ -2318,6 +2375,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()); } @@ -3646,6 +3709,7 @@ public ApplicationContentResolver(Context context, ActivityThread mainThread) { @Override @UnsupportedAppUsage protected IContentProvider acquireProvider(Context context, String auth) { + auth = translateContentProviderAuthority(auth); return mMainThread.acquireProvider(context, ContentProvider.getAuthorityWithoutUserId(auth), resolveUserIdFromAuthority(auth), true); @@ -3653,6 +3717,7 @@ protected IContentProvider acquireProvider(Context context, String auth) { @Override protected IContentProvider acquireExistingProvider(Context context, String auth) { + auth = translateContentProviderAuthority(auth); return mMainThread.acquireExistingProvider(context, ContentProvider.getAuthorityWithoutUserId(auth), resolveUserIdFromAuthority(auth), true); @@ -3665,6 +3730,7 @@ public boolean releaseProvider(IContentProvider provider) { @Override 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 355092378279..3cc819433f58 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; @@ -705,6 +709,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; } @@ -1115,6 +1126,11 @@ public void onMediaStoreDownloadsDeleted(@NonNull LongSparseArray idToMi * calls related to this download. */ 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); long id = Long.parseLong(downloadUri.getLastPathSegment()); @@ -1147,6 +1163,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); } @@ -1162,6 +1183,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; @@ -1540,6 +1566,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 46260ea5e658..ad03bc2d8dce 100644 --- a/core/java/android/app/IActivityManager.aidl +++ b/core/java/android/app/IActivityManager.aidl @@ -924,4 +924,16 @@ interface IActivityManager { void unregisterUidFrozenStateChangedCallback(in IUidFrozenStateChangedCallback callback); @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS)") int[] getUidFrozenState(in int[] uids); + + /** + * Notify AMS about binder transactions to frozen apps. + * + * @param debugPid The binder transaction sender + * @param code The binder transaction code + * @param flags The binder transaction flags + * @param err The binder transaction error + */ + oneway void frozenBinderTransactionDetected(int debugPid, int code, int flags, int err); + + String[] getSystemIdmapPaths(); } diff --git a/core/java/android/app/IApplicationThread.aidl b/core/java/android/app/IApplicationThread.aidl index 6b5f6b03028e..ed8eb1832339 100644 --- a/core/java/android/app/IApplicationThread.aidl +++ b/core/java/android/app/IApplicationThread.aidl @@ -173,4 +173,6 @@ oneway interface IApplicationThread { in UiTranslationSpec uiTranslationSpec); void scheduleTimeoutService(IBinder token, int startId); void schedulePing(in RemoteCallback pong); + + void onGosPackageStateChanged(in @nullable android.content.pm.GosPackageState state); } diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java index e31486f18dbf..d4aa03d6eb60 100644 --- a/core/java/android/app/Instrumentation.java +++ b/core/java/android/app/Instrumentation.java @@ -19,6 +19,7 @@ import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.compat.gms.GmsCompat; import android.compat.annotation.UnsupportedAppUsage; import android.content.ActivityNotFoundException; import android.content.ComponentName; @@ -58,7 +59,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; @@ -1278,6 +1281,7 @@ public void sendTrackballEventSync(MotionEvent event) { public Application newApplication(ClassLoader cl, String className, Context context) throws InstantiationException, IllegalAccessException, ClassNotFoundException { + GmsCompat.maybeEnable(context); Application app = getFactory(context.getPackageName()) .instantiateApplication(cl, className); app.attach(context); @@ -1296,6 +1300,7 @@ public Application newApplication(ClassLoader cl, String className, Context cont static public Application newApplication(Class clazz, Context context) throws InstantiationException, IllegalAccessException, ClassNotFoundException { + GmsCompat.maybeEnable(context); Application app = (Application)clazz.newInstance(); app.attach(context); return app; @@ -1870,12 +1875,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 b5efb73225d6..f092ce2363aa 100644 --- a/core/java/android/app/LoadedApk.java +++ b/core/java/android/app/LoadedApk.java @@ -343,7 +343,9 @@ private static String[] getLibrariesFor(String packageName) { */ public void updateApplicationInfo(@NonNull ApplicationInfo aInfo, @Nullable List oldPaths) { - setApplicationInfo(aInfo); + if (!setApplicationInfo(aInfo)) { + return; + } final List newPaths = new ArrayList<>(); makePaths(mActivityThread, aInfo, newPaths); @@ -388,7 +390,13 @@ mApplicationInfo.sharedLibraryFiles, null, null, getCompatibilityInfo(), mAppComponentFactory = createAppFactory(aInfo, mDefaultClassLoader); } - private void setApplicationInfo(ApplicationInfo aInfo) { + private boolean setApplicationInfo(ApplicationInfo aInfo) { + if (mApplicationInfo != null && mApplicationInfo.createTimestamp > aInfo.createTimestamp) { + Slog.w(TAG, "New application info for package " + aInfo.packageName + + " is out of date with TS " + aInfo.createTimestamp + " < the current TS " + + mApplicationInfo.createTimestamp); + return false; + } final int myUid = Process.myUid(); aInfo = adjustNativeLibraryPaths(aInfo); mApplicationInfo = aInfo; @@ -411,6 +419,7 @@ private void setApplicationInfo(ApplicationInfo aInfo) { if (aInfo.requestsIsolatedSplitLoading() && !ArrayUtils.isEmpty(mSplitNames)) { mSplitLoader = new SplitDependencyLoaderImpl(aInfo.splitDependencies); } + return true; } void setSdkSandboxStorage(@Nullable String sdkSandboxClientAppVolumeUuid, diff --git a/core/java/android/app/NotificationManager.java b/core/java/android/app/NotificationManager.java index 79b68c1456c7..b934b4b7d0af 100644 --- a/core/java/android/app/NotificationManager.java +++ b/core/java/android/app/NotificationManager.java @@ -16,6 +16,7 @@ package android.app; +import android.Manifest; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -27,6 +28,7 @@ import android.annotation.TestApi; import android.annotation.WorkerThread; import android.app.Notification.Builder; +import android.app.compat.gms.GmsCompat; import android.compat.annotation.UnsupportedAppUsage; import android.content.ComponentName; import android.content.Context; @@ -55,6 +57,8 @@ import android.util.Log; import android.util.proto.ProtoOutputStream; +import com.android.internal.gmscompat.GmsCompatApp; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; @@ -685,6 +689,17 @@ public void notifyAsPackage(@NonNull String targetPackage, @Nullable String tag, @UnsupportedAppUsage public void notifyAsUser(String tag, int id, Notification notification, 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 = getService(); String pkg = mContext.getPackageName(); @@ -1519,6 +1534,10 @@ public boolean isNotificationPolicyAccessGranted() { * {@link android.provider.Settings#ACTION_NOTIFICATION_LISTENER_SETTINGS}. */ public boolean isNotificationListenerAccessGranted(ComponentName listener) { + if (GmsCompat.isAndroidAuto()) { + return true; + } + INotificationManager service = getService(); try { return service.isNotificationListenerAccessGranted(listener); diff --git a/core/java/android/app/PendingIntent.java b/core/java/android/app/PendingIntent.java index c7522b429dbc..18c795ceaa8b 100644 --- a/core/java/android/app/PendingIntent.java +++ b/core/java/android/app/PendingIntent.java @@ -30,6 +30,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; @@ -60,6 +61,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; @@ -428,8 +432,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(); @@ -447,7 +451,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 @@ -464,6 +470,7 @@ private static void checkPendingIntent(int flags, @NonNull Intent intent, + " for security reasons."; Log.w(TAG, new StackTrace(msg)); } + return flags; } /** @hide */ @@ -563,7 +570,7 @@ public static PendingIntent getActivityAsUser(Context context, int requestCode, @NonNull Intent intent, int flags, Bundle options, 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); @@ -697,7 +704,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 = @@ -750,7 +757,7 @@ public static PendingIntent getBroadcastAsUser(Context context, int requestCode, Intent intent, int flags, 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 = @@ -829,7 +836,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 = @@ -1071,6 +1078,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/Service.java b/core/java/android/app/Service.java index a1554572c7df..89cf14e551cb 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/SharedPreferencesImpl.java b/core/java/android/app/SharedPreferencesImpl.java index a87187b8affb..9f5e947fe9fd 100644 --- a/core/java/android/app/SharedPreferencesImpl.java +++ b/core/java/android/app/SharedPreferencesImpl.java @@ -17,6 +17,7 @@ package android.app; import android.annotation.Nullable; +import android.app.compat.gms.GmsCompat; import android.compat.Compatibility; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledSince; @@ -32,6 +33,7 @@ import android.util.Log; import com.android.internal.annotations.GuardedBy; +import com.android.internal.gmscompat.GmsHooks; import com.android.internal.util.ExponentiallyBucketedHistogram; import com.android.internal.util.XmlUtils; @@ -295,11 +297,24 @@ private void awaitLoadedLocked() { @Override public Map getAll() { + HashMap res; synchronized (mLock) { awaitLoadedLocked(); //noinspection unchecked - return new HashMap(mMap); + res = new HashMap(mMap); } + + if (GmsCompat.isEnabled()) { + String fileName = mFile.getName(); + String suffix = ".xml"; + if (fileName.endsWith(suffix)) { + int endIndex = fileName.length() - suffix.length(); + String name = fileName.substring(0, endIndex); + GmsHooks.maybeModifySharedPreferencesValues(name, res); + } + } + + return res; } @Override diff --git a/core/java/android/app/StorageScope.java b/core/java/android/app/StorageScope.java new file mode 100644 index 000000000000..c9f1af124954 --- /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 565226d0333e..163096619e9a 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -29,6 +29,7 @@ import android.app.ambientcontext.IAmbientContextManager; import android.app.appsearch.AppSearchManagerFrameworkInitializer; import android.app.blob.BlobStoreManagerFrameworkInitializer; +import android.app.compat.gms.GmsCompat; import android.app.contentsuggestions.ContentSuggestionsManager; import android.app.contentsuggestions.IContentSuggestionsManager; import android.app.job.JobSchedulerFrameworkInitializer; @@ -121,6 +122,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; @@ -246,6 +248,7 @@ import com.android.internal.app.IBatteryStats; import com.android.internal.app.ISoundTriggerService; 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; @@ -554,7 +557,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, @@ -796,6 +803,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); }}); diff --git a/core/java/android/app/WallpaperManager.java b/core/java/android/app/WallpaperManager.java index 1c5332a3c8bd..e01478b79304 100644 --- a/core/java/android/app/WallpaperManager.java +++ b/core/java/android/app/WallpaperManager.java @@ -34,6 +34,7 @@ import android.annotation.TestApi; import android.annotation.UiContext; 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; @@ -85,6 +86,7 @@ import android.view.WindowManagerGlobal; import com.android.internal.R; +import com.android.internal.app.StorageScopesAppHooks; import libcore.io.IoUtils; @@ -967,6 +969,16 @@ public Drawable getDrawable() { @Nullable @RequiresPermission(anyOf = {MANAGE_EXTERNAL_STORAGE, READ_WALLPAPER_INTERNAL}) public Drawable getDrawable(@SetWallpaperFlags int which) { + if (StorageScopesAppHooks.shouldSpoofSelfPermissionCheck(android.Manifest.permission.MANAGE_EXTERNAL_STORAGE)) { + return null; + } + + if (GmsCompat.isEnabled()) { + if (!GmsCompat.hasPermission(READ_WALLPAPER_INTERNAL)) { + return null; + } + } + final ColorManagementProxy cmProxy = getColorManagementProxy(); boolean returnDefault = which != FLAG_LOCK; Bitmap bm = sGlobals.peekWallpaperBitmap(mContext, returnDefault, which, cmProxy); @@ -1296,6 +1308,10 @@ public Drawable getFastDrawable() { @Nullable @RequiresPermission(anyOf = {MANAGE_EXTERNAL_STORAGE, READ_WALLPAPER_INTERNAL}) public Drawable getFastDrawable(@SetWallpaperFlags int which) { + if (StorageScopesAppHooks.shouldSpoofSelfPermissionCheck(android.Manifest.permission.MANAGE_EXTERNAL_STORAGE)) { + return null; + } + final ColorManagementProxy cmProxy = getColorManagementProxy(); boolean returnDefault = which != FLAG_LOCK; Bitmap bm = sGlobals.peekWallpaperBitmap(mContext, returnDefault, which, cmProxy); @@ -1544,6 +1560,10 @@ public Rect peekBitmapDimensions(@SetWallpaperFlags int which, boolean returnDef @Nullable @RequiresPermission(anyOf = {MANAGE_EXTERNAL_STORAGE, READ_WALLPAPER_INTERNAL}) public ParcelFileDescriptor getWallpaperFile(@SetWallpaperFlags int which) { + if (StorageScopesAppHooks.shouldSpoofSelfPermissionCheck(android.Manifest.permission.MANAGE_EXTERNAL_STORAGE)) { + return null; + } + return getWallpaperFile(which, mContext.getUserId()); } diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index 90d96160053c..f959036fde3f 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -3630,7 +3630,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 = 64; /** * Service Action: Service implemented by a device owner or profile owner supervision app to diff --git a/core/java/android/app/compat/gms/AndroidAuto.java b/core/java/android/app/compat/gms/AndroidAuto.java new file mode 100644 index 000000000000..e1f3963dd035 --- /dev/null +++ b/core/java/android/app/compat/gms/AndroidAuto.java @@ -0,0 +1,14 @@ +package android.app.compat.gms; + +import android.annotation.SystemApi; + +/** @hide */ +@SystemApi +public class AndroidAuto { + public static final long PKG_FLAG_GRANT_PERMS_FOR_WIRED_ANDROID_AUTO = 1L; + public static final long PKG_FLAG_GRANT_PERMS_FOR_WIRELESS_ANDROID_AUTO = 1L << 1; + public static final long PKG_FLAG_GRANT_AUDIO_ROUTING_PERM = 1L << 2; + public static final long PKG_FLAG_GRANT_PERMS_FOR_ANDROID_AUTO_PHONE_CALLS = 1L << 3; + + private AndroidAuto() {} +} 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 000000000000..1e61e46aacc6 --- /dev/null +++ b/core/java/android/app/compat/gms/GmsCompat.java @@ -0,0 +1,316 @@ +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.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 com.android.internal.gmscompat.GmsHooks; +import com.android.internal.gmscompat.GmsInfo; +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; + + // Static only + private GmsCompat() { } + + public static boolean isEnabled() { + return isGmsCompatEnabled; + } + + /** @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; + } + + /** @hide */ + public static boolean isAndroidAuto() { + return curPackageId == PackageId.ANDROID_AUTO; + } + + /** @hide */ + public static int getCurrentPackageId() { + return curPackageId; + } + + 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)) { + isGmsCompatEnabled = true; + GmsHooks.init(appCtx, appInfo.packageName); + } + + isEligibleForClientCompat = !isGmsCore() && + appInfoExt.hasFlag(AppInfoExt.FLAG_HAS_GMSCORE_CLIENT_LIBRARY); + } + + public static boolean isEnabledFor(@NonNull ApplicationInfo app) { + if (Build.IS_DEBUGGABLE) { + if (isTestPackage(app.packageName)) { + return true; + } + } + + return isEnabledFor(app.ext().getPackageId(), app.isPrivilegedApp()); + } + + public static boolean isEnabledFor(int packageId, boolean isPrivileged) { + if (isPrivileged) { + // don't enable GmsCompat for privileged GMS + return false; + } + + return switch (packageId) { + case + PackageId.GSF, + 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 (Build.IS_DEBUGGABLE) { + if (isTestPackage(pkgName)) { + return true; + } + } + + return switch (pkgName) { + case + PackageId.GSF_NAME, + 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 (Build.isDebuggable()) { + if (isTestPackage(packageName, userId, matchDisabledApp)) { + return true; + } + } + + if (!canBeEnabledFor(packageName)) { + return false; + } + + if (userId == UserHandle.USER_CURRENT) { + userId = UserHandle.myUserId(); + } + + Context ctx = AppGlobals.getInitialApplication(); + if (ctx == null) { + return false; + } + + 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); + } + + // call only when Build.isDebuggable() is true + /** @hide */ + public static boolean isTestPackage(String packageName, int userId, boolean matchDisabledApp) { + if (!Build.isDebuggable()) { + 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; + } +} 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 000000000000..7056d0962abc --- /dev/null +++ b/core/java/android/app/compat/gms/GmsModuleHooks.java @@ -0,0 +1,120 @@ +/* + * 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.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 (Build.isDebuggable()) { + 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() {} +} 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 000000000000..e7fa4b9378ad --- /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/usage/StorageStatsManager.java b/core/java/android/app/usage/StorageStatsManager.java index 1ef15637d578..2d2fccc670b3 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,7 +36,8 @@ import android.os.storage.CrateInfo; import android.os.storage.StorageManager; -import com.android.internal.util.Preconditions; +import com.android.internal.gmscompat.GmsInfo; +import com.android.internal.gmscompat.PlayStoreHooks; import java.io.File; import java.io.IOException; @@ -209,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 2200af679531..38be567ac011 100644 --- a/core/java/android/content/ContentProvider.java +++ b/core/java/android/content/ContentProvider.java @@ -2724,7 +2724,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/ContentResolver.java b/core/java/android/content/ContentResolver.java index b2cd7e90f291..52755ac0fca1 100644 --- a/core/java/android/content/ContentResolver.java +++ b/core/java/android/content/ContentResolver.java @@ -31,6 +31,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; @@ -71,6 +72,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; @@ -1255,12 +1261,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(); } @@ -2193,6 +2214,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); @@ -2490,6 +2514,8 @@ public final IContentProvider acquireProvider(Uri uri) { } final String auth = uri.getAuthority(); if (auth != null) { + GmsDynamiteClientHooks.maybeInit(auth); + return acquireProvider(mContext, auth); } return null; @@ -2568,7 +2594,18 @@ public final IContentProvider acquireUnstableProvider(String name) { */ 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); } @@ -2724,11 +2761,27 @@ public final void registerContentObserverAsUser(@NonNull Uri uri, @UnsupportedAppUsage public final void registerContentObserver(Uri uri, boolean notifyForDescendents, ContentObserver observer, @UserIdInt int userHandle) { + if (ContentProviderRedirector.shouldSkipRegisterContentObserver(uri, notifyForDescendents, + observer, userHandle)) { + return; + } + + if (GmsCompat.isEnabled()) { + if (GmsCompatApp.registerObserver(uri, observer)) { + return; + } + } + try { getContentService().registerContentObserver(uri, notifyForDescendents, 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; } } @@ -2740,6 +2793,17 @@ public final void registerContentObserver(Uri uri, boolean notifyForDescendents, */ 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) { @@ -2834,6 +2898,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, @@ -2881,6 +2950,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) { @@ -3956,6 +4029,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/Intent.java b/core/java/android/content/Intent.java index ec94ebef99c6..090414592625 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -36,6 +36,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; @@ -69,6 +70,7 @@ import android.provider.DocumentsProvider; import android.provider.MediaStore; import android.provider.OpenableColumns; +import android.provider.Settings; import android.service.chooser.ChooserAction; import android.telecom.PhoneAccount; import android.telecom.TelecomManager; @@ -1856,6 +1858,13 @@ public static Intent createChooser(Intent target, CharSequence title, IntentSend public static final String EXTRA_UNINSTALL_ALL_USERS = "android.intent.extra.UNINSTALL_ALL_USERS"; + /** + * Specify whether the "More options" button should be shown in the package uninstallation UI. + * @hide + */ + public static final String EXTRA_UNINSTALL_SHOW_MORE_OPTIONS_BUTTON + = "android.intent.extra.UNINSTALL_SHOW_MORE_OPTIONS_BUTTON"; + /** * A string that associates with a metadata entry, indicating the last run version of the * platform that was setup. @@ -9669,6 +9678,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/pm/AppPermissionUtils.java b/core/java/android/content/pm/AppPermissionUtils.java new file mode 100644 index 000000000000..1d9ed009a450 --- /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 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(GosPackageState.FLAG_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(GosPackageState.FLAG_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 3487e0b1f3e8..59dfa2d500a3 100644 --- a/core/java/android/content/pm/ApplicationInfo.java +++ b/core/java/android/content/pm/ApplicationInfo.java @@ -29,6 +29,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; @@ -1901,6 +1902,7 @@ public ApplicationInfo() { public ApplicationInfo(ApplicationInfo orig) { super(orig); + ext = orig.ext; taskAffinity = orig.taskAffinity; permission = orig.permission; mKnownActivityEmbeddingCerts = orig.mKnownActivityEmbeddingCerts; @@ -1989,6 +1991,7 @@ public void writeToParcel(Parcel dest, int parcelableFlags) { return; } super.writeToParcel(dest, parcelableFlags); + ext.writeToParcel(dest, parcelableFlags); dest.writeString8(taskAffinity); dest.writeString8(permission); dest.writeString8(processName); @@ -2092,6 +2095,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(); @@ -2807,4 +2811,17 @@ public void setEnableOnBackInvokedCallback(boolean isEnable) { privateFlagsExt &= ~PRIVATE_FLAG_EXT_ENABLE_ON_BACK_INVOKED_CALLBACK; } } + + private AppInfoExt ext = AppInfoExt.DEFAULT; + + /** @hide */ + public void setExt(AppInfoExt ext) { + this.ext = ext; + } + + /** @hide */ + @SystemApi + public @NonNull AppInfoExt ext() { + return ext; + } } diff --git a/core/java/android/content/pm/CrossProfileApps.java b/core/java/android/content/pm/CrossProfileApps.java index 529363f828bb..8f2e72ccc777 100644 --- a/core/java/android/content/pm/CrossProfileApps.java +++ b/core/java/android/content/pm/CrossProfileApps.java @@ -30,6 +30,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; @@ -307,6 +308,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) { @@ -439,6 +444,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) { @@ -470,6 +479,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 000000000000..c7a48e85d078 --- /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 000000000000..959b1bda9c84 --- /dev/null +++ b/core/java/android/content/pm/GosPackageState.java @@ -0,0 +1,477 @@ +/* + * 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.Nullable; +import android.annotation.SystemApi; +import android.annotation.UserIdInt; +import android.app.ActivityThread; +import android.app.PropertyInvalidatedCache; +import android.content.Context; +import android.os.Parcel; +import android.os.Parcelable; +import android.os.Process; +import android.os.RemoteException; +import android.os.UserHandle; +import android.permission.PermissionManager; + +import java.util.Objects; + +/** + * @hide + */ +@SystemApi +public final class GosPackageState extends GosPackageStateBase implements Parcelable { + public final int derivedFlags; // derived from persistent state, but not persisted themselves + + // packageName and userId are stored here for convenience, they don't get serialized + private String packageName; + private int userId; + + public static final int FLAG_STORAGE_SCOPES_ENABLED = 1; + // checked only if REQUEST_INSTALL_PACKAGES permission is granted + public static final int FLAG_ALLOW_ACCESS_TO_OBB_DIRECTORY = 1 << 1; + // 1 << 2, 1 << 3, 1 << 4 were used previously, do not reuse them + public static final int FLAG_CONTACT_SCOPES_ENABLED = 1 << 5; + + /** @hide */ public static final int FLAG_BLOCK_NATIVE_DEBUGGING_NON_DEFAULT = 1 << 6; + /** @hide */ public static final int FLAG_BLOCK_NATIVE_DEBUGGING = 1 << 7; + /** @hide */ public static final int FLAG_BLOCK_NATIVE_DEBUGGING_SUPPRESS_NOTIF = 1 << 8; + + /** @hide */ public static final int FLAG_RESTRICT_MEMORY_DYN_CODE_EXEC_NON_DEFAULT = 1 << 9; + /** @hide */ public static final int FLAG_RESTRICT_MEMORY_DYN_CODE_EXEC = 1 << 10; + /** @hide */ public static final int FLAG_RESTRICT_MEMORY_DYN_CODE_EXEC_SUPPRESS_NOTIF = 1 << 11; + + /** @hide */ public static final int FLAG_RESTRICT_STORAGE_DYN_CODE_EXEC_NON_DEFAULT = 1 << 12; + /** @hide */ public static final int FLAG_RESTRICT_STORAGE_DYN_CODE_EXEC = 1 << 13; + /** @hide */ public static final int FLAG_RESTRICT_STORAGE_DYN_CODE_EXEC_SUPPRESS_NOTIF = 1 << 14; + + /** @hide */ public static final int FLAG_RESTRICT_WEBVIEW_DYN_CODE_EXEC_NON_DEFAULT = 1 << 15; + /** @hide */ public static final int FLAG_RESTRICT_WEBVIEW_DYN_CODE_EXEC = 1 << 16; + + /** @hide */ public static final int FLAG_USE_HARDENED_MALLOC_NON_DEFAULT = 1 << 17; + /** @hide */ public static final int FLAG_USE_HARDENED_MALLOC = 1 << 18; + + /** @hide */ public static final int FLAG_USE_EXTENDED_VA_SPACE_NON_DEFAULT = 1 << 19; + /** @hide */ public static final int FLAG_USE_EXTENDED_VA_SPACE = 1 << 20; + + /** @hide */ public static final int FLAG_FORCE_MEMTAG_NON_DEFAULT = 1 << 21; + /** @hide */ public static final int FLAG_FORCE_MEMTAG = 1 << 22; + /** @hide */ public static final int FLAG_FORCE_MEMTAG_SUPPRESS_NOTIF = 1 << 23; + + /** @hide */ public static final int FLAG_ENABLE_EXPLOIT_PROTECTION_COMPAT_MODE = 1 << 24; + + /** @hide */ public static final int FLAG_HAS_PACKAGE_FLAGS = 1 << 25; + + // to distinguish between the case when no dflags are set and the case when dflags weren't calculated yet + public static final int DFLAGS_SET = 1; + + public static final int DFLAG_EXPECTS_ALL_FILES_ACCESS = 1 << 1; + public static final int DFLAG_EXPECTS_ACCESS_TO_MEDIA_FILES_ONLY = 1 << 2; + public static final int DFLAG_EXPECTS_STORAGE_WRITE_ACCESS = 1 << 3; + public static final int DFLAG_HAS_READ_EXTERNAL_STORAGE_DECLARATION = 1 << 4; + public static final int DFLAG_HAS_WRITE_EXTERNAL_STORAGE_DECLARATION = 1 << 5; + public static final int DFLAG_HAS_MANAGE_EXTERNAL_STORAGE_DECLARATION = 1 << 6; + public static final int DFLAG_HAS_MANAGE_MEDIA_DECLARATION = 1 << 7; + public static final int DFLAG_HAS_ACCESS_MEDIA_LOCATION_DECLARATION = 1 << 8; + public static final int DFLAG_HAS_READ_MEDIA_AUDIO_DECLARATION = 1 << 9; + public static final int DFLAG_HAS_READ_MEDIA_IMAGES_DECLARATION = 1 << 10; + public static final int DFLAG_HAS_READ_MEDIA_VIDEO_DECLARATION = 1 << 11; + public static final int DFLAG_HAS_READ_MEDIA_VISUAL_USER_SELECTED_DECLARATION = 1 << 12; + public static final int DFLAG_EXPECTS_LEGACY_EXTERNAL_STORAGE = 1 << 13; + + public static final int DFLAG_HAS_READ_CONTACTS_DECLARATION = 1 << 20; + public static final int DFLAG_HAS_WRITE_CONTACTS_DECLARATION = 1 << 21; + public static final int DFLAG_HAS_GET_ACCOUNTS_DECLARATION = 1 << 22; + + /** @hide */ + public GosPackageState(int flags, long packageFlags, + @Nullable byte[] storageScopes, @Nullable byte[] contactScopes, + int derivedFlags) { + super(flags, packageFlags, storageScopes, contactScopes); + this.derivedFlags = derivedFlags; + } + + @Nullable + public static GosPackageState getForSelf() { + String packageName = ActivityThread.currentPackageName(); + if (packageName == null) { + // currentPackageName is null inside system_server + if (ActivityThread.isSystem()) { + return null; + } else { + throw new IllegalStateException("ActivityThread.currentPackageName() is null"); + } + } + return get(packageName); + } + + // uses current userId, don't use in places that deal with multiple users (eg system_server) + @Nullable + public static GosPackageState get(@NonNull String packageName) { + Object res = sCurrentUserCache.query(packageName); + if (res instanceof GosPackageState) { + return (GosPackageState) res; + } + return null; + } + + @Nullable + public static GosPackageState get(@NonNull String packageName, @UserIdInt int userId) { + if (userId == myUserId()) { + return get(packageName); + } + Object res = getOtherUsersCache().query(new CacheQuery(packageName, userId)); + if (res instanceof GosPackageState) { + return (GosPackageState) res; + } + return null; + } + + @NonNull + public static GosPackageState getOrDefault(@NonNull String packageName) { + var s = get(packageName); + if (s == null) { + s = createDefault(packageName, myUserId()); + } + return s; + } + + @NonNull + public static GosPackageState getOrDefault(@NonNull String packageName, int userId) { + var s = get(packageName, userId); + if (s == null) { + s = createDefault(packageName, userId); + } + return s; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeInt(this.flags); + dest.writeLong(this.packageFlags); + dest.writeByteArray(storageScopes); + dest.writeByteArray(contactScopes); + dest.writeInt(derivedFlags); + } + + @NonNull + public static final Creator CREATOR = new Creator<>() { + @Override + public GosPackageState createFromParcel(Parcel in) { + return new GosPackageState(in.readInt(), in.readLong(), + in.createByteArray(), in.createByteArray(), + in.readInt()); + } + + @Override + public GosPackageState[] newArray(int size) { + return new GosPackageState[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @NonNull + public String getPackageName() { + return packageName; + } + + public int getUserId() { + return userId; + } + + public boolean hasFlag(int flag) { + return (flags & flag) != 0; + } + + public boolean hasDerivedFlag(int flag) { + return (derivedFlags & flag) != 0; + } + + public boolean hasDerivedFlags(int flags) { + return (derivedFlags & flags) == flags; + } + + /** @hide */ + public static boolean attachableToPackage(int appId) { + // Packages with this appId use the "android.uid.system" sharedUserId, which is expensive + // to deal with due to the large number of packages that it includes (see GosPackageStatePm + // doc). These packages have no need for GosPackageState. + return appId != Process.SYSTEM_UID; + } + + public static boolean attachableToPackage(@NonNull String pkg) { + Context ctx = ActivityThread.currentApplication(); + if (ctx == null) { + return false; + } + + ApplicationInfo ai; + try { + ai = ctx.getPackageManager().getApplicationInfo(pkg, 0); + } catch (PackageManager.NameNotFoundException e) { + return false; + } + + return attachableToPackage(UserHandle.getAppId(ai.uid)); + } + + // invalidated by PackageManager#invalidatePackageInfoCache() (eg when + // PackageManagerService#setGosPackageState succeeds) + private static final PropertyInvalidatedCache sCurrentUserCache = + new PropertyInvalidatedCache( + 256, PermissionManager.CACHE_KEY_PACKAGE_INFO, + "getGosPackageStateCurrentUser") { + + @Override + public Object recompute(String packageName) { + return getUncached(packageName, myUserId()); + } + }; + + + static final class CacheQuery { + final String packageName; + final int userId; + + CacheQuery(String packageName, int userId) { + this.packageName = packageName; + this.userId = userId; + } + + @Override + public int hashCode() { + return Objects.hashCode(packageName.hashCode()) + 31 * userId; + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof CacheQuery) { + CacheQuery o = (CacheQuery) obj; + return packageName.equals(o.packageName) && userId == o.userId; + } + return false; + } + } + + private static volatile PropertyInvalidatedCache sOtherUsersCache; + + private static PropertyInvalidatedCache getOtherUsersCache() { + var c = sOtherUsersCache; + if (c != null) { + return c; + } + return sOtherUsersCache = new PropertyInvalidatedCache( + 256, PermissionManager.CACHE_KEY_PACKAGE_INFO, + "getGosPackageStateOtherUsers") { + + @Override + public Object recompute(CacheQuery query) { + return getUncached(query.packageName, query.userId); + } + }; + } + + static Object getUncached(String packageName, int userId) { + try { + GosPackageState s = ActivityThread.getPackageManager().getGosPackageState(packageName, userId); + if (s != null) { + s.packageName = packageName; + s.userId = userId; + return s; + } + // return non-null to cache null results, see javadoc for PropertyInvalidatedCache#recompute() + return GosPackageState.class; + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + @NonNull + public Editor edit() { + return new Editor(this, getPackageName(), getUserId()); + } + + @NonNull + public static Editor edit(@NonNull String packageName) { + return edit(packageName, myUserId()); + } + + @NonNull + public static Editor edit(@NonNull String packageName, int userId) { + GosPackageState s = GosPackageState.get(packageName, userId); + if (s != null) { + return s.edit(); + } + + return new Editor(packageName, userId); + } + + public static final int EDITOR_FLAG_KILL_UID_AFTER_APPLY = 1; + 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 int flags; + private long packageFlags; + private byte[] storageScopes; + private byte[] contactScopes; + private int editorFlags; + + /** + * Don't call directly, use GosPackageState#edit or GosPackageStatePm#getEditor + * + * @hide + * */ + public Editor(String packageName, int userId) { + this(createDefault(packageName, userId), packageName, userId); + } + + /** @hide */ + public Editor(GosPackageStateBase s, String packageName, int userId) { + this.packageName = packageName; + this.userId = userId; + this.flags = s.flags; + this.packageFlags = s.packageFlags; + this.storageScopes = s.storageScopes; + this.contactScopes = s.contactScopes; + } + + @NonNull + public Editor setFlagsState(int flags, boolean state) { + if (state) { + addFlags(flags); + } else { + clearFlags(flags); + } + return this; + } + + @NonNull + public Editor addFlags(int flags) { + this.flags |= flags; + return this; + } + + @NonNull + public Editor clearFlags(int flags) { + this.flags &= ~flags; + return this; + } + + @NonNull + public Editor addPackageFlags(long flags) { + this.packageFlags |= flags; + return this; + } + + @NonNull + public Editor clearPackageFlags(long flags) { + this.packageFlags &= ~flags; + return this; + } + + @NonNull + public Editor setPackageFlagState(long flags, boolean state) { + if (state) { + addPackageFlags(flags); + } else { + clearPackageFlags(flags); + } + + 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() { + setFlagsState(GosPackageState.FLAG_HAS_PACKAGE_FLAGS, packageFlags != 0); + + try { + return ActivityThread.getPackageManager().setGosPackageState(packageName, userId, + new GosPackageState(flags, packageFlags, storageScopes, contactScopes, 0), + editorFlags); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + } + + private static volatile int myUid; + private static int myUserId; + + private static int myUserId() { + if (myUid == 0) { + int uid = Process.myUid(); + // order is important, volatile write to myUid publishes write to myUserId + myUserId = UserHandle.getUserId(uid); + myUid = uid; + } + return myUserId; + } + + /** @hide */ + public static GosPackageState createDefault(String pkgName, int userId) { + var ps = new GosPackageState(0, 0L, null, null, 0); + ps.packageName = pkgName; + ps.userId = userId; + return ps; + } +} diff --git a/core/java/android/content/pm/GosPackageStateBase.java b/core/java/android/content/pm/GosPackageStateBase.java new file mode 100644 index 000000000000..bb15db057c98 --- /dev/null +++ b/core/java/android/content/pm/GosPackageStateBase.java @@ -0,0 +1,67 @@ +package android.content.pm; + +import android.annotation.Nullable; + +import java.util.Arrays; + +/** + * Common code between GosPackageState and GosPackageStatePm. + * + * @hide + */ +public abstract class GosPackageStateBase { + public final int flags; + // flags that have package-specific meaning + public final long packageFlags; + @Nullable + public final byte[] storageScopes; + @Nullable + public final byte[] contactScopes; + + protected GosPackageStateBase(int flags, long packageFlags, + @Nullable byte[] storageScopes, @Nullable byte[] contactScopes) { + this.flags = flags; + this.packageFlags = packageFlags; + this.storageScopes = storageScopes; + this.contactScopes = contactScopes; + } + + public final boolean hasFlags(int flags) { + return (this.flags & flags) == flags; + } + + public final boolean hasPackageFlags(long packageFlags) { + return (this.packageFlags & packageFlags) == packageFlags; + } + + @Override + public final int hashCode() { + return 31 * flags + Arrays.hashCode(storageScopes) + Arrays.hashCode(contactScopes) + Long.hashCode(packageFlags); + } + + @Override + public final boolean equals(Object obj) { + if (!(obj instanceof GosPackageStateBase)) { + return false; + } + + GosPackageStateBase o = (GosPackageStateBase) obj; + if (flags != o.flags) { + return false; + } + + if (!Arrays.equals(storageScopes, o.storageScopes)) { + return false; + } + + if (!Arrays.equals(contactScopes, o.contactScopes)) { + return false; + } + + if (packageFlags != o.packageFlags) { + return false; + } + + return true; + } +} diff --git a/core/java/android/content/pm/IPackageInstallerSession.aidl b/core/java/android/content/pm/IPackageInstallerSession.aidl index 081f263b698e..8659fe5c0d98 100644 --- a/core/java/android/content/pm/IPackageInstallerSession.aidl +++ b/core/java/android/content/pm/IPackageInstallerSession.aidl @@ -70,4 +70,6 @@ interface IPackageInstallerSession { ParcelFileDescriptor getAppMetadataFd(); ParcelFileDescriptor openWriteAppMetadata(); void removeAppMetadata(); + + long getSilentUpdateWaitMillis(); } diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl index 3fdd023a6b68..8b5cc3baa9dd 100644 --- a/core/java/android/content/pm/IPackageManager.aidl +++ b/core/java/android/content/pm/IPackageManager.aidl @@ -798,4 +798,21 @@ interface IPackageManager { boolean[] canPackageQuery(String sourcePackageName, in String[] targetPackageNames, int userId); boolean waitForHandler(long timeoutMillis, boolean forBackgroundHandler); + + @nullable Bundle getExtraAppBindArgs(String packageName); + + void skipSpecialRuntimePermissionAutoGrantsForPackage(String packageName, int userId, in List permissions); + + 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 537e7d48dafa..61a530ff693e 100644 --- a/core/java/android/content/pm/PackageInstaller.java +++ b/core/java/android/content/pm/PackageInstaller.java @@ -46,6 +46,7 @@ import android.app.ActivityManager; import android.app.ActivityThread; import android.app.AppGlobals; +import android.app.compat.gms.GmsCompat; import android.compat.annotation.UnsupportedAppUsage; import android.content.IIntentReceiver; import android.content.IIntentSender; @@ -74,6 +75,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; @@ -82,8 +84,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; @@ -646,6 +650,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); @@ -1745,6 +1753,26 @@ public void onChecksumsReady(List checksums) * @see #requestUserPreapproval */ public void commit(@NonNull IntentSender statusReceiver) { + if (GmsCompat.isPlayStore()) { + long waitMs = 0; + try { + waitMs = mSession.getSilentUpdateWaitMillis(); + } catch (Exception e) { + // getSilentUpdateWaitMillis() will fail if Play Store didn't set packageName + // of this session. It always does currently AFAIK (September 2022) + Log.e("GmsCompat", "", e); + } + + if (waitMs > 0) { + // Should happen only if the same package is updated twice within 30 seconds + // (likely a Play Store bug, possibly related to APK splits) + Log.d("GmsCompat", "PackageInstaller.Session.getSilentUpdateWaitMillis returned " + waitMs + ", sleeping..."); + SystemClock.sleep(waitMs + 100); + } + + statusReceiver = PlayStoreHooks.wrapCommitStatusReceiver(this, statusReceiver); + } + try { mSession.commit(statusReceiver, false); } catch (RemoteException e) { @@ -2371,6 +2399,12 @@ public static class SessionParams implements Parcelable { public boolean applicationEnabledSettingPersistent = false; private final ArrayMap mPermissionStates; + /** + * {@hide} + * + * Used only by gmscompat, to disallow updates to unknown versions of GmsCore and Play Store. + */ + public long maxAllowedVersion = Long.MAX_VALUE; /** * Construct parameters for a new package install session. @@ -2382,6 +2416,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} */ @@ -2418,6 +2456,7 @@ public SessionParams(Parcel source) { requireUserAction = source.readInt(); packageSource = source.readInt(); applicationEnabledSettingPersistent = source.readBoolean(); + maxAllowedVersion = source.readLong(); } /** {@hide} */ @@ -2449,6 +2488,7 @@ public SessionParams copy() { ret.requireUserAction = requireUserAction; ret.packageSource = packageSource; ret.applicationEnabledSettingPersistent = applicationEnabledSettingPersistent; + ret.maxAllowedVersion = maxAllowedVersion; return ret; } @@ -3156,6 +3196,7 @@ public void writeToParcel(Parcel dest, int flags) { dest.writeInt(requireUserAction); dest.writeInt(packageSource); dest.writeBoolean(applicationEnabledSettingPersistent); + dest.writeLong(maxAllowedVersion); } public static final Parcelable.Creator diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java index 54fb4e72a79c..cf6bcb6d506f 100644 --- a/core/java/android/content/pm/PackageManager.java +++ b/core/java/android/content/pm/PackageManager.java @@ -1666,7 +1666,8 @@ public interface OnPermissionsChangedListener { /** @hide */ @IntDef(flag = true, value = { DONT_KILL_APP, - SYNCHRONOUS + SYNCHRONOUS, + SKIP_IF_MISSING, }) @Retention(RetentionPolicy.SOURCE) public @interface EnabledFlags {} @@ -1688,6 +1689,9 @@ public interface OnPermissionsChangedListener { */ public static final int SYNCHRONOUS = 0x00000002; + /** @hide */ + public static final int SKIP_IF_MISSING = 0x4000_0000; + /** @hide */ @IntDef(prefix = { "INSTALL_REASON_" }, value = { INSTALL_REASON_UNKNOWN, @@ -11109,4 +11113,17 @@ public void relinquishUpdateOwnership(@NonNull String targetPackage) { throw new UnsupportedOperationException( "relinquishUpdateOwnership not implemented in subclass"); } + + /** @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(); + } + } } diff --git a/core/java/android/content/pm/SigningInfo.java b/core/java/android/content/pm/SigningInfo.java index ee9aaca3ed43..69f422f46a31 100644 --- a/core/java/android/content/pm/SigningInfo.java +++ b/core/java/android/content/pm/SigningInfo.java @@ -138,4 +138,10 @@ public SigningInfo[] newArray(int size) { return new SigningInfo[size]; } }; + + /** @hide */ + public SigningDetails getSigningDetails() { + // SigningInfo doesn't expose the SigningDetails#hasSha256Certificate() method, among others + return mSigningDetails; + } } diff --git a/core/java/android/content/pm/SpecialRuntimePermAppUtils.java b/core/java/android/content/pm/SpecialRuntimePermAppUtils.java new file mode 100644 index 000000000000..85ca727f0def --- /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 000000000000..73338d14b7dc --- /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/parsing/ApkLiteParseUtils.java b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java index 4f6bcb6f0be5..0fe05538f9ea 100644 --- a/core/java/android/content/pm/parsing/ApkLiteParseUtils.java +++ b/core/java/android/content/pm/parsing/ApkLiteParseUtils.java @@ -567,8 +567,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 143c00dd4d81..43aa70991264 100644 --- a/core/java/android/content/res/ApkAssets.java +++ b/core/java/android/content/res/ApkAssets.java @@ -25,6 +25,7 @@ import android.text.TextUtils; import com.android.internal.annotations.GuardedBy; +import com.android.internal.gmscompat.dynamite.GmsDynamiteClientHooks; import dalvik.annotation.optimization.CriticalNative; @@ -144,7 +145,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); } /** @@ -158,6 +159,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 0f284f491c29..38dc3d14ee83 100644 --- a/core/java/android/content/res/AssetManager.java +++ b/core/java/android/content/res/AssetManager.java @@ -24,6 +24,7 @@ import android.annotation.StringRes; import android.annotation.StyleRes; import android.annotation.TestApi; +import android.app.ActivityManager; import android.compat.annotation.UnsupportedAppUsage; import android.content.pm.ActivityInfo; import android.content.res.Configuration.NativeConfig; @@ -38,6 +39,7 @@ import com.android.internal.annotations.GuardedBy; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.content.om.OverlayConfig; +import com.android.internal.os.ExecInit; import java.io.FileDescriptor; import java.io.FileNotFoundException; @@ -232,6 +234,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 @@ -248,8 +253,32 @@ public static void createSystemAssetsInZygoteLocked(boolean reinitialize, final ArrayList apkAssets = new ArrayList<>(); apkAssets.add(ApkAssets.loadFromPath(frameworkPath, ApkAssets.PROPERTY_SYSTEM)); - final String[] systemIdmapPaths = - OverlayConfig.getZygoteInstance().createImmutableFrameworkIdmapsInZygote(); + // 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. + // + // As a workaround, ask the ActivityManager to return paths of cached idmaps and use + // them directly. ActivityManager runs in system_server, which always uses zygote-based + // spawning. + + String[] systemIdmapPaths; + if (ExecInit.isExecSpawned) { + try { + systemIdmapPaths = ActivityManager.getService().getSystemIdmapPaths(); + Objects.requireNonNull(systemIdmapPaths); + } catch (Throwable t) { + Log.e(TAG, "unable to retrieve systemIdmapPaths", t); + systemIdmapPaths = new String[0]; + } + } 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/database/sqlite/SQLiteOpenHelper.java b/core/java/android/database/sqlite/SQLiteOpenHelper.java index 5e523c0112b1..a2a325866cdd 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; @@ -168,6 +171,10 @@ private SQLiteOpenHelper(@Nullable Context context, @Nullable String name, int v mNewVersion = version; mMinimumSupportedVersion = Math.max(0, minimumSupportedVersion); setOpenParamsBuilder(openParamsBuilder); + + 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 000000000000..5fd4182ca3fb --- /dev/null +++ b/core/java/android/ext/AppInfoExt.java @@ -0,0 +1,82 @@ +package android.ext; + +import android.annotation.NonNull; +import android.annotation.SystemApi; +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 static final int FLAG_HAS_GMSCORE_CLIENT_LIBRARY = 0; + + 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 boolean hasFlag(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/BrowserUtils.java b/core/java/android/ext/BrowserUtils.java new file mode 100644 index 000000000000..41bf530388f9 --- /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/ErrorReportUi.java b/core/java/android/ext/ErrorReportUi.java new file mode 100644 index 000000000000..5800269a0e06 --- /dev/null +++ b/core/java/android/ext/ErrorReportUi.java @@ -0,0 +1,43 @@ +package android.ext; + +import android.content.ComponentName; +import android.content.Intent; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.UUID; +import java.util.zip.GZIPOutputStream; + +/** @hide */ +public class ErrorReportUi { + + public static final String ACTION_CUSTOM_REPORT = "action_custom_report"; + public static final String EXTRA_TYPE = "type"; + public static final String EXTRA_GZIPPED_MESSAGE = "gzipped_msg"; + public static final String EXTRA_SOURCE_PACKAGE = "source_pkg"; + public static final String EXTRA_SHOW_REPORT_BUTTON = "show_report_button"; + + private static final String HOST_PACKAGE = KnownSystemPackages.SYSTEM_UI; + + public static Intent createBaseIntent(String action, String msg) { + var i = new Intent(action); + i.putExtra(ErrorReportUi.EXTRA_GZIPPED_MESSAGE, gzipString(msg)); + // this is needed for correct usage via PendingIntent + i.setIdentifier(UUID.randomUUID().toString()); + i.setComponent(ComponentName.createRelative(HOST_PACKAGE, ".ErrorReportActivity")); + 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(StandardCharsets.UTF_8)); + } catch (IOException e) { + throw new IllegalStateException(e); + } + + return bos.toByteArray(); + } +} diff --git a/core/java/android/ext/KnownSystemPackages.java b/core/java/android/ext/KnownSystemPackages.java new file mode 100644 index 000000000000..956c94e165b3 --- /dev/null +++ b/core/java/android/ext/KnownSystemPackages.java @@ -0,0 +1,13 @@ +package android.ext; + +/** @hide */ +public interface KnownSystemPackages { + String PERMISSION_CONTROLLER = "com.android.permissioncontroller"; + String SETTINGS = "com.android.settings"; + String SYSTEM_UI = "com.android.systemui"; + + String MEDIA_PROVIDER = "com.android.providers.media.module"; + String CONTACTS_PROVIDER = "com.android.providers.contacts"; + + String LAUNCHER = "com.android.launcher3"; +} diff --git a/core/java/android/ext/PackageId.java b/core/java/android/ext/PackageId.java new file mode 100644 index 000000000000..0e6b9606de03 --- /dev/null +++ b/core/java/android/ext/PackageId.java @@ -0,0 +1,44 @@ +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"; + 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; +} diff --git a/core/java/android/ext/SettingsIntents.java b/core/java/android/ext/SettingsIntents.java new file mode 100644 index 000000000000..0a0314824dbb --- /dev/null +++ b/core/java/android/ext/SettingsIntents.java @@ -0,0 +1,19 @@ +package android.ext; + +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 Intent getAppIntent(String action, String pkgName) { + var i = new Intent(action); + i.setData(Uri.fromParts("package", pkgName, null)); + i.setPackage(KnownSystemPackages.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 000000000000..8565a233c3e0 --- /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 000000000000..044ccfae94d9 --- /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 000000000000..267aed4fdde8 --- /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 000000000000..7445fc22e614 --- /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/settings/BoolSetting.java b/core/java/android/ext/settings/BoolSetting.java new file mode 100644 index 000000000000..c4573f14b50b --- /dev/null +++ b/core/java/android/ext/settings/BoolSetting.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2022 GrapheneOS + * SPDX-License-Identifier: Apache-2.0 + */ + +package android.ext.settings; + +import android.content.Context; + +import java.util.function.BooleanSupplier; + +/** @hide */ +public class BoolSetting extends Setting { + private boolean defaultValue; + private volatile BooleanSupplier defaultValueSupplier; + + public BoolSetting(Scope scope, String key, boolean defaultValue) { + super(scope, key); + this.defaultValue = defaultValue; + } + + public BoolSetting(Scope scope, String key, BooleanSupplier defaultValue) { + super(scope, key); + defaultValueSupplier = defaultValue; + } + + public final boolean get(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(Context ctx, int userId) { + String valueStr = getRaw(ctx, userId); + + if (valueStr == null) { + return getDefaultValue(); + } + + 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(); + } + + public final boolean put(Context ctx, boolean val) { + return putRaw(ctx, val ? "1" : "0"); + } + + private boolean getDefaultValue() { + BooleanSupplier supplier = defaultValueSupplier; + if (supplier != null) { + defaultValue = supplier.getAsBoolean(); + 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 000000000000..2667eee153d2 --- /dev/null +++ b/core/java/android/ext/settings/BoolSysProperty.java @@ -0,0 +1,25 @@ +package android.ext.settings; + +import android.os.UserHandle; + +import java.util.function.BooleanSupplier; + +/** @hide */ +public class BoolSysProperty extends BoolSetting { + + public BoolSysProperty(String key, boolean defaultValue) { + super(Scope.SYSTEM_PROPERTY, key, defaultValue); + } + + public BoolSysProperty(String key, BooleanSupplier defaultValue) { + super(Scope.SYSTEM_PROPERTY, key, defaultValue); + } + + public boolean get() { + return super.get(null, UserHandle.USER_SYSTEM); + } + + public boolean put(boolean val) { + return super.put(null, val); + } +} diff --git a/core/java/android/ext/settings/ConnChecksSetting.java b/core/java/android/ext/settings/ConnChecksSetting.java new file mode 100644 index 000000000000..c2af4fd8f013 --- /dev/null +++ b/core/java/android/ext/settings/ConnChecksSetting.java @@ -0,0 +1,32 @@ +package android.ext.settings; + +import android.annotation.SystemApi; +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; + + public static int get() { + return ExtSettings.CONNECTIVITY_CHECKS.get(); + } + + public static boolean put(int val) { + return ExtSettings.CONNECTIVITY_CHECKS.put(val); + } + + /** + * Use only during migration, to decide whether to perform it. + * @hide + */ + public static boolean isSet() { + return !SystemProperties.get(ExtSettings.CONNECTIVITY_CHECKS.getKey()).isEmpty(); + } + + private ConnChecksSetting() {} +} diff --git a/core/java/android/ext/settings/ExtSettings.java b/core/java/android/ext/settings/ExtSettings.java new file mode 100644 index 000000000000..9e843cfdd234 --- /dev/null +++ b/core/java/android/ext/settings/ExtSettings.java @@ -0,0 +1,214 @@ +package android.ext.settings; + +import android.annotation.BoolRes; +import android.annotation.IntegerRes; +import android.annotation.StringRes; +import android.app.AppGlobals; +import android.content.Context; +import android.content.res.Resources; + +import com.android.internal.R; + +import java.lang.reflect.Field; +import java.util.Set; +import java.util.concurrent.TimeUnit; +import java.util.function.BooleanSupplier; +import java.util.function.IntSupplier; +import java.util.function.Supplier; + +import static android.ext.settings.GnssConstants.PSDS_DISABLED; +import static android.ext.settings.GnssConstants.PSDS_SERVER_GRAPHENEOS; +import static android.ext.settings.GnssConstants.PSDS_SERVER_STANDARD; +import static android.ext.settings.GnssConstants.SUPL_DISABLED; +import static android.ext.settings.GnssConstants.SUPL_SERVER_GRAPHENEOS_PROXY; +import static android.ext.settings.GnssConstants.SUPL_SERVER_STANDARD; +import static android.ext.settings.RemoteKeyProvisioningSettings.GRAPHENEOS_PROXY; +import static android.ext.settings.RemoteKeyProvisioningSettings.STANDARD_SERVER; +import static android.ext.settings.WidevineProvisioningSettings.WV_GRAPHENEOS_PROXY; +import static android.ext.settings.WidevineProvisioningSettings.WV_STANDARD_SERVER; + +/** @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, "auto_grant_OTHER_SENSORS_perm", true); + + public static final IntSetting AUTO_REBOOT_TIMEOUT = new IntSetting( + Setting.Scope.GLOBAL, "settings_reboot_after_timeout", + // default value: 3 days + (int) TimeUnit.DAYS.toMillis(3)); + + public static final BoolSetting SCREENSHOT_TIMESTAMP_EXIF = new BoolSetting( + Setting.Scope.PER_USER, "screenshot_timestamp_exif", false); + + public static final IntSetting GNSS_SUPL = new IntSetting( + Setting.Scope.GLOBAL, "force_disable_supl", // historical name + SUPL_SERVER_GRAPHENEOS_PROXY, // default + SUPL_SERVER_STANDARD, SUPL_DISABLED, SUPL_SERVER_GRAPHENEOS_PROXY // valid values + ); + + public static final IntSetting GNSS_PSDS_STANDARD = new IntSetting( + Setting.Scope.GLOBAL, "psds_server", // historical name + PSDS_SERVER_GRAPHENEOS, // default + PSDS_SERVER_GRAPHENEOS, PSDS_SERVER_STANDARD, PSDS_DISABLED // valid values + ); + + public static final IntSysProperty GNSS_PSDS_VENDOR = new IntSysProperty( + // keep in sync with bionic/libc/bionic/gnss_psds_setting.c + "persist.sys.gnss_psds", + PSDS_SERVER_GRAPHENEOS, // default + PSDS_SERVER_GRAPHENEOS, PSDS_SERVER_STANDARD, PSDS_DISABLED + ); + + public static IntSetting getGnssPsdsSetting(Context ctx) { + String type = ctx.getString(com.android.internal.R.string.config_gnssPsdsType); + switch (type) { + case GnssConstants.PSDS_TYPE_QUALCOMM_XTRA: + return GNSS_PSDS_VENDOR; + default: + return GNSS_PSDS_STANDARD; + } + } + + public static boolean isStandardGnssPsds(Context ctx) { + return getGnssPsdsSetting(ctx) == GNSS_PSDS_STANDARD; + } + + public static final BoolSetting SCRAMBLE_PIN_LAYOUT = new BoolSetting( + Setting.Scope.PER_USER, "lockscreen_scramble_pin_layout", false); + + public static final IntSetting REMOTE_KEY_PROVISIONING_SERVER = new IntSetting( + Setting.Scope.GLOBAL, "attest_remote_provisioner_server", + GRAPHENEOS_PROXY, // default + STANDARD_SERVER, GRAPHENEOS_PROXY // valid values + ); + + 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); + + // also read in packages/modules/DnsResolver (DnsTlsTransport.cpp and doh/network/driver.rs) + public static final IntSysProperty CONNECTIVITY_CHECKS = new IntSysProperty( + "persist.sys.connectivity_checks", + ConnChecksSetting.VAL_DEFAULT, + ConnChecksSetting.VAL_GRAPHENEOS, ConnChecksSetting.VAL_STANDARD, ConnChecksSetting.VAL_DISABLED + ); + + // 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, "wifi_off_timeout", 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, "bluetooth_off_timeout", 0 /* off by default */); + + public static final String DENY_NEW_USB_DISABLED = "disabled"; + public static final String DENY_NEW_USB_DYNAMIC = "dynamic"; + public static final String DENY_NEW_USB_ENABLED = "enabled"; + // also specified in build/make/core/main.mk + public static final String DENY_NEW_USB_DEFAULT = DENY_NEW_USB_DYNAMIC; + + // see system/core/rootdir/init.rc + public static final String DENY_NEW_USB_TRANSIENT_PROP = "security.deny_new_usb"; + public static final String DENY_NEW_USB_TRANSIENT_ENABLE = "1"; + public static final String DENY_NEW_USB_TRANSIENT_DISABLE = "0"; + + public static final StringSysProperty DENY_NEW_USB = new StringSysProperty( + "persist.security.deny_new_usb", DENY_NEW_USB_DEFAULT) { + @Override + public boolean validateValue(String val) { + switch (val) { + case DENY_NEW_USB_DISABLED: + case DENY_NEW_USB_DYNAMIC: + case DENY_NEW_USB_ENABLED: + return true; + default: + return false; + } + } + }; + + 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, "allow_automatic_pkg_hardening_config", // historical name + defaultBool(R.bool.setting_default_allow_disabling_hardening_via_app_compat_config)); + + public static final BoolSetting RESTRICT_MEMORY_DYN_CODE_EXEC_BY_DEFAULT = new BoolSetting( + Setting.Scope.GLOBAL, "restrict_memory_dyn_code_exec", + defaultBool(R.bool.setting_default_restrict_memory_dyn_code_exec)); + + public static final BoolSetting RESTRICT_STORAGE_DYN_CODE_EXEC_BY_DEFAULT = new BoolSetting( + Setting.Scope.GLOBAL, "restrict_storage_dyn_code_exec", + defaultBool(R.bool.setting_default_restrict_storage_dyn_code_exec)); + + public static final BoolSetting RESTRICT_WEBVIEW_DYN_CODE_EXEC_BY_DEFAULT = new BoolSetting( + Setting.Scope.GLOBAL, "restrict_webview_dyn_code_exec", + defaultBool(R.bool.setting_default_restrict_webview_dyn_code_exec)); + + public static final BoolSetting FORCE_APP_MEMTAG_BY_DEFAULT = new BoolSetting( + Setting.Scope.GLOBAL, "force_app_memtag", + defaultBool(R.bool.setting_default_force_app_memtag)); + + public static final BoolSetting SHOW_SYSTEM_PROCESS_CRASH_NOTIFICATIONS = new BoolSetting( + Setting.Scope.GLOBAL, "show_system_process_crash_notifs", false); + + public static final IntSetting WIDEVINE_PROVISIONING_SERVER = new IntSetting( + Setting.Scope.GLOBAL, "widevine_provisioner_server", + WV_GRAPHENEOS_PROXY, // default + WV_STANDARD_SERVER, WV_GRAPHENEOS_PROXY // valid values + ); + + private ExtSettings() {} + + // used for making settings defined in this class unreadable by third-party apps + public static void getKeys(Setting.Scope scope, Set dest) { + for (Field field : ExtSettings.class.getDeclaredFields()) { + if (!Setting.class.isAssignableFrom(field.getType())) { + continue; + } + Setting s; + try { + s = (Setting) field.get(null); + } catch (IllegalAccessException e) { + throw new IllegalStateException(e); + } + + if (s.getScope() == scope) { + if (!dest.add(s.getKey())) { + throw new IllegalStateException("duplicate definition of setting " + s.getKey()); + } + } + } + } + + public static BooleanSupplier defaultBool(@BoolRes int res) { + return () -> getResources().getBoolean(res); + } + + public static IntSupplier defaultInt(@IntegerRes int res) { + return () -> getResources().getInteger(res); + } + + public static Supplier defaultString(@StringRes int res) { + return () -> getResources().getString(res); + } + + public static Resources getResources() { + return AppGlobals.getInitialApplication().getResources(); + } +} diff --git a/core/java/android/ext/settings/GnssConstants.java b/core/java/android/ext/settings/GnssConstants.java new file mode 100644 index 000000000000..d521d71ffb42 --- /dev/null +++ b/core/java/android/ext/settings/GnssConstants.java @@ -0,0 +1,18 @@ +package android.ext.settings; + +/** @hide */ +public interface GnssConstants { + int SUPL_SERVER_STANDARD = 0; + int SUPL_DISABLED = 1; + int SUPL_SERVER_GRAPHENEOS_PROXY = 2; + + String PSDS_TYPE_QUALCOMM_XTRA = "qualcomm_xtra"; + String PSDS_TYPE_BROADCOM = "broadcom"; + + String PSDS_SERVER_GRAPHENEOS_BROADCOM = "https://broadcom.psds.grapheneos.org"; + + // keep in sync with bionic/libc/bionic/gnss_psds_setting.c + int PSDS_SERVER_GRAPHENEOS = 0; + int PSDS_SERVER_STANDARD = 1; + int PSDS_DISABLED = 2; +} diff --git a/core/java/android/ext/settings/IntSetting.java b/core/java/android/ext/settings/IntSetting.java new file mode 100644 index 000000000000..8db6fbf16884 --- /dev/null +++ b/core/java/android/ext/settings/IntSetting.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2022 GrapheneOS + * SPDX-License-Identifier: Apache-2.0 + */ + +package android.ext.settings; + +import android.annotation.Nullable; +import android.content.Context; + +import java.util.function.IntSupplier; + +/** @hide */ +public class IntSetting extends Setting { + private int defaultValue; + private volatile IntSupplier defaultValueSupplier; + + @Nullable private final int[] validValues; + + private IntSetting(Scope scope, String key, @Nullable int[] validValues) { + super(scope, key); + this.validValues = validValues; + } + + public IntSetting(Scope scope, String key, int defaultValue) { + this(scope, key, (int[]) null); + setDefaultValue(defaultValue); + } + + public IntSetting(Scope scope, String key, int defaultValue, int... validValues) { + this(scope, key, validValues); + setDefaultValue(defaultValue); + } + + public IntSetting(Scope scope, String key, IntSupplier defaultValue) { + this(scope, key, (int[]) null); + defaultValueSupplier = defaultValue; + } + + public IntSetting(Scope scope, String key, IntSupplier defaultValue, 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(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(Context ctx, int userId) { + String valueStr = getRaw(ctx, userId); + + if (valueStr == null) { + return getDefaultValue(); + } + + int value; + try { + value = Integer.parseInt(valueStr); + } catch (NumberFormatException e) { + e.printStackTrace(); + return getDefaultValue(); + } + + if (!validateValue(value)) { + return getDefaultValue(); + } + + return value; + } + + public final boolean put(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() { + IntSupplier supplier = defaultValueSupplier; + if (supplier != null) { + setDefaultValue(supplier.getAsInt()); + 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 000000000000..dcb7f4a6f960 --- /dev/null +++ b/core/java/android/ext/settings/IntSysProperty.java @@ -0,0 +1,33 @@ +package android.ext.settings; + +import android.os.UserHandle; + +import java.util.function.IntSupplier; + +/** @hide */ +public class IntSysProperty extends IntSetting { + + public IntSysProperty(String key, int defaultValue) { + super(Scope.SYSTEM_PROPERTY, key, defaultValue); + } + + public IntSysProperty(String key, int defaultValue, int... validValues) { + super(Scope.SYSTEM_PROPERTY, key, defaultValue, validValues); + } + + public IntSysProperty(String key, IntSupplier defaultValue) { + super(Scope.SYSTEM_PROPERTY, key, defaultValue); + } + + public IntSysProperty(String key, IntSupplier defaultValue, int... validValues) { + super(Scope.SYSTEM_PROPERTY, key, defaultValue, validValues); + } + + public int get() { + return super.get(null, UserHandle.USER_SYSTEM); + } + + public boolean put(int val) { + return super.put(null, val); + } +} diff --git a/core/java/android/ext/settings/RemoteKeyProvisioningSettings.java b/core/java/android/ext/settings/RemoteKeyProvisioningSettings.java new file mode 100644 index 000000000000..9da018709282 --- /dev/null +++ b/core/java/android/ext/settings/RemoteKeyProvisioningSettings.java @@ -0,0 +1,29 @@ +package android.ext.settings; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.content.Context; + +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"; + + @Nullable + public static String getServerUrlOverride(@NonNull Context ctx) { + if (ExtSettings.REMOTE_KEY_PROVISIONING_SERVER.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 000000000000..7c9e7b2a6d21 --- /dev/null +++ b/core/java/android/ext/settings/Setting.java @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2022 GrapheneOS + * SPDX-License-Identifier: Apache-2.0 + */ + +package android.ext.settings; + +import android.annotation.Nullable; +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 java.util.function.Consumer; + +/** @hide */ +public abstract class Setting { + + 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; + + protected Setting(Scope scope, String key) { + this.scope = scope; + this.key = key; + } + + public final String getKey() { + return key; + } + + public final Scope getScope() { + return scope; + } + + @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) { + e.printStackTrace(); + 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(); + } + + 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 + public final Object registerObserver(Context ctx, Consumer callback, Handler handler) { + return registerObserver(ctx, ctx.getUserId(), callback, handler); + } + + public final Object registerObserver(Context ctx, int userId, Consumer callback, Handler handler) { + 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(Context ctx, 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 000000000000..5f875dfcd0fd --- /dev/null +++ b/core/java/android/ext/settings/StringSetting.java @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2022 GrapheneOS + * SPDX-License-Identifier: Apache-2.0 + */ + +package android.ext.settings; + +import android.content.Context; + +import java.util.function.Supplier; + +/** @hide */ +public class StringSetting extends Setting { + private String defaultValue; + private volatile Supplier defaultValueSupplier; + + public StringSetting(Scope scope, String key, String defaultValue) { + super(scope, key); + setDefaultValue(defaultValue); + } + + public StringSetting(Scope scope, String key, Supplier defaultValue) { + super(scope, key); + this.defaultValueSupplier = defaultValue; + } + + public boolean validateValue(String val) { + return true; + } + + public final String get(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 String get(Context ctx, int userId) { + String s = getRaw(ctx, userId); + if (s == null || !validateValue(s)) { + return getDefaultValue(); + } + return s; + } + + public final boolean put(Context ctx, String val) { + if (!validateValue(val)) { + throw new IllegalStateException("invalid value " + val); + } + return putRaw(ctx, val); + } + + private void setDefaultValue(String val) { + if (!validateValue(val)) { + throw new IllegalStateException("invalid default value " + val); + } + defaultValue = val; + } + + private String getDefaultValue() { + Supplier supplier = defaultValueSupplier; + if (supplier != null) { + setDefaultValue(supplier.get()); + 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 000000000000..ab4570100128 --- /dev/null +++ b/core/java/android/ext/settings/StringSysProperty.java @@ -0,0 +1,25 @@ +package android.ext.settings; + +import android.os.UserHandle; + +import java.util.function.Supplier; + +/** @hide */ +public class StringSysProperty extends StringSetting { + + public StringSysProperty(String key, String defaultValue) { + super(Scope.SYSTEM_PROPERTY, key, defaultValue); + } + + public StringSysProperty(String key, Supplier defaultValue) { + super(Scope.SYSTEM_PROPERTY, key, defaultValue); + } + + public String get() { + return super.get(null, UserHandle.USER_SYSTEM); + } + + public boolean put(String val) { + return super.put(null, val); + } +} diff --git a/core/java/android/ext/settings/WidevineProvisioningSettings.java b/core/java/android/ext/settings/WidevineProvisioningSettings.java new file mode 100644 index 000000000000..d80e570b049e --- /dev/null +++ b/core/java/android/ext/settings/WidevineProvisioningSettings.java @@ -0,0 +1,27 @@ +package android.ext.settings; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.SystemApi; +import android.content.Context; + +import static android.annotation.SystemApi.Client.MODULE_LIBRARIES; + +/** @hide */ +@SystemApi(client = MODULE_LIBRARIES) +public class WidevineProvisioningSettings { + public static final int WV_GRAPHENEOS_PROXY = 0; + public static final int WV_STANDARD_SERVER = 1; + + private static final String WV_GRAPHENEOS_PROXY_HOSTNAME = "widevineprovisioning.grapheneos.org"; + + @Nullable + public static String getServerHostnameOverride(@NonNull Context ctx) { + if (ExtSettings.WIDEVINE_PROVISIONING_SERVER.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 000000000000..4a6a81e4c6ca --- /dev/null +++ b/core/java/android/ext/settings/app/AppSwitch.java @@ -0,0 +1,169 @@ +package android.ext.settings.app; + +import android.annotation.Nullable; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.GosPackageState; +import android.content.pm.GosPackageStateBase; +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 + int gosPsFlagNonDefault; + // GosPackageState flag that indicates whether the switch is on or off, ignored if + // non-default flag is not set + 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) + 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; + + // 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 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, + @Nullable GosPackageStateBase ps) { + return getImmutableValue(ctx, userId, appInfo, ps) != null; + } + + public final Boolean getImmutableValue(Context ctx, int userId, ApplicationInfo appInfo, + @Nullable GosPackageStateBase 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, + @Nullable GosPackageStateBase ps, StateInfo si) { + return null; + } + + public final boolean getDefaultValue(Context ctx, int userId, ApplicationInfo appInfo, + @Nullable GosPackageStateBase ps) { + return getDefaultValue(ctx, userId, appInfo, ps, StateInfo.PLACEHOLDER); + } + + public final boolean getDefaultValue(Context ctx, int userId, ApplicationInfo appInfo, + @Nullable GosPackageStateBase 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, + @Nullable GosPackageStateBase ps, StateInfo si); + + + public final boolean get(Context ctx, int userId, ApplicationInfo appInfo, + @Nullable GosPackageStateBase ps) { + return get(ctx, userId, appInfo, ps, StateInfo.PLACEHOLDER); + } + + public final boolean get(Context ctx, int userId, ApplicationInfo appInfo, + @Nullable GosPackageStateBase 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.hasFlags(gosPsFlag); + if (gosPsFlagInverted) { + res = !res; + } + } + + return res; + } + + public final void set(GosPackageState.Editor ed, boolean on) { + if (gosPsFlagNonDefault != 0) { + ed.addFlags(gosPsFlagNonDefault); + } + + if (gosPsFlagInverted) { + ed.setFlagsState(gosPsFlag, !on); + } else { + ed.setFlagsState(gosPsFlag, on); + } + } + + private boolean isUsingDefaultValue(@Nullable GosPackageStateBase ps) { + return ps == null || (gosPsFlagNonDefault != 0 && !ps.hasFlags(gosPsFlagNonDefault)); + } + + public final void setUseDefaultValue(GosPackageState.Editor ed) { + ed.clearFlags(gosPsFlagNonDefault | gosPsFlag); + } + + public final boolean isNotificationSuppressed(@Nullable GosPackageStateBase ps) { + int flag = gosPsFlagSuppressNotif; + if (flag == 0) { + return false; + } + return ps == null || ps.hasFlags(flag); + } + + public final void addSuppressNotificationFlag(GosPackageState.Editor ed) { + ed.addFlags(gosPsFlagSuppressNotif); + } +} 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 000000000000..7fb2e3eb0858 --- /dev/null +++ b/core/java/android/ext/settings/app/AswDenyNativeDebug.java @@ -0,0 +1,50 @@ +package android.ext.settings.app; + +import android.annotation.Nullable; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.GosPackageState; +import android.content.pm.GosPackageStateBase; +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 = GosPackageState.FLAG_BLOCK_NATIVE_DEBUGGING_NON_DEFAULT; + gosPsFlag = GosPackageState.FLAG_BLOCK_NATIVE_DEBUGGING; + gosPsFlagSuppressNotif = GosPackageState.FLAG_BLOCK_NATIVE_DEBUGGING_SUPPRESS_NOTIF; + compatChangeToDisableHardening = AppCompatProtos.ALLOW_NATIVE_DEBUGGING; + } + + @Override + public Boolean getImmutableValue(Context ctx, int userId, ApplicationInfo appInfo, + @Nullable GosPackageStateBase ps, StateInfo si) { + if (appInfo.isSystemApp() && !SELinuxFlags.isSystemAppSepolicyWeakeningAllowed()) { + si.immutabilityReason = IR_IS_SYSTEM_APP; + return true; + } + + if (ps != null && ps.hasFlags(GosPackageState.FLAG_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, + @Nullable GosPackageStateBase ps, StateInfo si) { + if (appInfo.isSystemApp()) { + return true; + } + + si.defaultValueReason = DVR_DEFAULT_SETTING; + return !ExtSettings.ALLOW_NATIVE_DEBUG_BY_DEFAULT.get(); + } +} diff --git a/core/java/android/ext/settings/app/AswRestrictMemoryDynCodeExec.java b/core/java/android/ext/settings/app/AswRestrictMemoryDynCodeExec.java new file mode 100644 index 000000000000..3accb7e90ae8 --- /dev/null +++ b/core/java/android/ext/settings/app/AswRestrictMemoryDynCodeExec.java @@ -0,0 +1,70 @@ +package android.ext.settings.app; + +import android.annotation.Nullable; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.GosPackageState; +import android.content.pm.GosPackageStateBase; +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 AswRestrictMemoryDynCodeExec extends AppSwitch { + public static final AswRestrictMemoryDynCodeExec I = new AswRestrictMemoryDynCodeExec(); + + private AswRestrictMemoryDynCodeExec() { + gosPsFlagNonDefault = GosPackageState.FLAG_RESTRICT_MEMORY_DYN_CODE_EXEC_NON_DEFAULT; + gosPsFlag = GosPackageState.FLAG_RESTRICT_MEMORY_DYN_CODE_EXEC; + gosPsFlagSuppressNotif = GosPackageState.FLAG_RESTRICT_MEMORY_DYN_CODE_EXEC_SUPPRESS_NOTIF; + compatChangeToDisableHardening = AppCompatProtos.ALLOW_MEMORY_DYN_CODE_EXEC; + } + + 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_exec_by_default)); + allowedSystemPkgs = set; + } + return set.contains(pkg); + } + + @Override + public Boolean getImmutableValue(Context ctx, int userId, ApplicationInfo appInfo, + @Nullable GosPackageStateBase ps, StateInfo si) { + if (appInfo.isSystemApp()) { + if (shouldAllowByDefaultToSystemPkg(ctx, appInfo.packageName)) { + // allow manual restriction + return null; + } + if (SELinuxFlags.isSystemAppSepolicyWeakeningAllowed()) { + return null; + } + return true; + } + + if (ps != null && ps.hasFlags(GosPackageState.FLAG_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, + @Nullable GosPackageStateBase ps, StateInfo si) { + if (appInfo.isSystemApp()) { + return !shouldAllowByDefaultToSystemPkg(ctx, appInfo.packageName); + } else { + si.defaultValueReason = DVR_DEFAULT_SETTING; + return ExtSettings.RESTRICT_MEMORY_DYN_CODE_EXEC_BY_DEFAULT.get(ctx, userId); + } + } +} diff --git a/core/java/android/ext/settings/app/AswRestrictStorageDynCodeExec.java b/core/java/android/ext/settings/app/AswRestrictStorageDynCodeExec.java new file mode 100644 index 000000000000..5a9835bfbe40 --- /dev/null +++ b/core/java/android/ext/settings/app/AswRestrictStorageDynCodeExec.java @@ -0,0 +1,70 @@ +package android.ext.settings.app; + +import android.annotation.Nullable; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.GosPackageState; +import android.content.pm.GosPackageStateBase; +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 AswRestrictStorageDynCodeExec extends AppSwitch { + public static final AswRestrictStorageDynCodeExec I = new AswRestrictStorageDynCodeExec(); + + private AswRestrictStorageDynCodeExec() { + gosPsFlagNonDefault = GosPackageState.FLAG_RESTRICT_STORAGE_DYN_CODE_EXEC_NON_DEFAULT; + gosPsFlag = GosPackageState.FLAG_RESTRICT_STORAGE_DYN_CODE_EXEC; + gosPsFlagSuppressNotif = GosPackageState.FLAG_RESTRICT_STORAGE_DYN_CODE_EXEC_SUPPRESS_NOTIF; + compatChangeToDisableHardening = AppCompatProtos.ALLOW_STORAGE_DYN_CODE_EXEC; + } + + 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_exec_by_default)); + allowedSystemPkgs = set; + } + return set.contains(pkg); + } + + @Override + public Boolean getImmutableValue(Context ctx, int userId, ApplicationInfo appInfo, + @Nullable GosPackageStateBase ps, StateInfo si) { + if (appInfo.isSystemApp()) { + if (shouldAllowByDefaultToSystemPkg(ctx, appInfo.packageName)) { + // allow manual restriction + return null; + } + if (SELinuxFlags.isSystemAppSepolicyWeakeningAllowed()) { + return null; + } + return true; + } + + if (ps != null && ps.hasFlags(GosPackageState.FLAG_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, + @Nullable GosPackageStateBase ps, StateInfo si) { + if (appInfo.isSystemApp()) { + return !shouldAllowByDefaultToSystemPkg(ctx, appInfo.packageName); + } else { + si.defaultValueReason = DVR_DEFAULT_SETTING; + return ExtSettings.RESTRICT_STORAGE_DYN_CODE_EXEC_BY_DEFAULT.get(ctx, userId); + } + } +} diff --git a/core/java/android/ext/settings/app/AswRestrictWebViewDynamicCodeExecution.java b/core/java/android/ext/settings/app/AswRestrictWebViewDynamicCodeExecution.java new file mode 100644 index 000000000000..7d5700e360cb --- /dev/null +++ b/core/java/android/ext/settings/app/AswRestrictWebViewDynamicCodeExecution.java @@ -0,0 +1,65 @@ +package android.ext.settings.app; + +import android.annotation.Nullable; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.GosPackageState; +import android.content.pm.GosPackageStateBase; +import android.ext.settings.ExtSettings; +import android.util.ArraySet; + +import com.android.internal.R; + +/** @hide */ +public class AswRestrictWebViewDynamicCodeExecution extends AppSwitch { + public static final AswRestrictWebViewDynamicCodeExecution I = new AswRestrictWebViewDynamicCodeExecution(); + + private AswRestrictWebViewDynamicCodeExecution() { + gosPsFlagNonDefault = GosPackageState.FLAG_RESTRICT_WEBVIEW_DYN_CODE_EXEC_NON_DEFAULT; + gosPsFlag = GosPackageState.FLAG_RESTRICT_WEBVIEW_DYN_CODE_EXEC; + } + + private static volatile ArraySet allowedSystemPkgs; + + private static boolean shouldAllowByDefaultToSystemPackage(Context ctx, String pkg) { + var set = allowedSystemPkgs; + if (set == null) { + set = new ArraySet<>(ctx.getResources() + .getStringArray(R.array.system_pkgs_allowed_webview_dyn_code_exec_by_default)); + allowedSystemPkgs = set; + } + return set.contains(pkg); + } + + @Override + public Boolean getImmutableValue(Context ctx, int userId, ApplicationInfo appInfo, + @Nullable GosPackageStateBase ps, StateInfo si) { + if (appInfo.isSystemApp()) { + // TODO uncomment after WebView respects Environment.isDynamicCodeExecutionRestricted() + /* + if (shouldAllowByDefaultToSystemPackage(ctx, packageName)) { + // allow manual restriction + return null; + } else { + return true; + } + */ + return null; + } + + return null; + } + + @Override + protected boolean getDefaultValueInner(Context ctx, int userId, ApplicationInfo appInfo, + @Nullable GosPackageStateBase ps, StateInfo si) { + if (appInfo.isSystemApp()) { + // TODO uncomment after WebView respects Environment.isDynamicCodeExecutionRestricted() + // return !shouldAllowByDefaultToSystemPackage(ctx, packageName); + return false; + } + else { + return ExtSettings.RESTRICT_WEBVIEW_DYN_CODE_EXEC_BY_DEFAULT.get(ctx, userId); + } + } +} 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 000000000000..18dc830b4d2f --- /dev/null +++ b/core/java/android/ext/settings/app/AswUseExtendedVaSpace.java @@ -0,0 +1,50 @@ +package android.ext.settings.app; + +import android.annotation.Nullable; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.GosPackageState; +import android.content.pm.GosPackageStateBase; + +import com.android.server.os.nano.AppCompatProtos; + +import dalvik.system.VMRuntime; + +/** @hide */ +public class AswUseExtendedVaSpace extends AppSwitch { + public static final AswUseExtendedVaSpace I = new AswUseExtendedVaSpace(); + + private AswUseExtendedVaSpace() { + gosPsFlag = GosPackageState.FLAG_USE_EXTENDED_VA_SPACE; + gosPsFlagNonDefault = GosPackageState.FLAG_USE_EXTENDED_VA_SPACE_NON_DEFAULT; + compatChangeToDisableHardening = AppCompatProtos.DISABLE_EXTENDED_VA_SPACE; + } + + @Override + public Boolean getImmutableValue(Context ctx, int userId, ApplicationInfo appInfo, + @Nullable GosPackageStateBase ps, StateInfo si) { + if (AswUseHardenedMalloc.I.get(ctx, userId, appInfo, ps)) { + si.immutabilityReason = IR_REQUIRED_BY_HARDENED_MALLOC; + return true; + } + + String primaryAbi = appInfo.primaryCpuAbi; + if (primaryAbi != null && !VMRuntime.is64BitAbi(primaryAbi)) { + si.immutabilityReason = IR_NON_64_BIT_NATIVE_CODE; + return false; + } + + if (ps != null && ps.hasFlags(GosPackageState.FLAG_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, + @Nullable GosPackageStateBase 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 000000000000..64cdea3f7b16 --- /dev/null +++ b/core/java/android/ext/settings/app/AswUseHardenedMalloc.java @@ -0,0 +1,63 @@ +package android.ext.settings.app; + +import android.annotation.Nullable; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.GosPackageState; +import android.content.pm.GosPackageStateBase; + +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 = GosPackageState.FLAG_USE_HARDENED_MALLOC; + gosPsFlagNonDefault = GosPackageState.FLAG_USE_HARDENED_MALLOC_NON_DEFAULT; + compatChangeToDisableHardening = AppCompatProtos.DISABLE_HARDENED_MALLOC; + } + + @Override + public Boolean getImmutableValue(Context ctx, int userId, ApplicationInfo appInfo, + @Nullable GosPackageStateBase 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.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) { + // turning off hardened_malloc requires exec spawning, which is always disabled for + // debuggable apps + si.immutabilityReason = IR_IS_DEBUGGABLE_APP; + return true; + } + + if (appInfo.isSystemApp()) { + si.immutabilityReason = IR_IS_SYSTEM_APP; + return true; + } + + if (ps != null && ps.hasFlags(GosPackageState.FLAG_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, + @Nullable GosPackageStateBase 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 000000000000..9f8234fb1716 --- /dev/null +++ b/core/java/android/ext/settings/app/AswUseMemoryTagging.java @@ -0,0 +1,64 @@ +package android.ext.settings.app; + +import android.annotation.Nullable; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.GosPackageState; +import android.content.pm.GosPackageStateBase; +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 = GosPackageState.FLAG_FORCE_MEMTAG; + gosPsFlagNonDefault = GosPackageState.FLAG_FORCE_MEMTAG_NON_DEFAULT; + gosPsFlagSuppressNotif = GosPackageState.FLAG_FORCE_MEMTAG_SUPPRESS_NOTIF; + compatChangeToDisableHardening = AppCompatProtos.DISABLE_MEMORY_TAGGING; + } + + @Override + public Boolean getImmutableValue(Context ctx, int userId, ApplicationInfo appInfo, + @Nullable GosPackageStateBase 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()) { + 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 != null && ps.hasFlags(GosPackageState.FLAG_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, + @Nullable GosPackageStateBase 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/camera2/CameraExtensionCharacteristics.java b/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java index ea951a55bfca..c07310cd5ad2 100644 --- a/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java +++ b/core/java/android/hardware/camera2/CameraExtensionCharacteristics.java @@ -22,6 +22,9 @@ import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.ApplicationInfoFlags; import android.graphics.ImageFormat; import android.hardware.camera2.extension.IAdvancedExtenderImpl; import android.hardware.camera2.extension.ICameraExtensionsProxyService; @@ -44,6 +47,8 @@ import android.util.Range; import android.util.Size; +import com.android.internal.util.PixelCameraServicesUtils; + import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.ArrayList; @@ -250,6 +255,20 @@ public static CameraExtensionManagerGlobal get() { return GLOBAL_CAMERA_MANAGER; } + private static boolean validateVendorCameraExtensionsPackage(Context ctx, String pkgName) { + if (PixelCameraServicesUtils.validatePackage(ctx, pkgName)) { + return true; + } + + try { + ApplicationInfo ai = ctx.getPackageManager().getApplicationInfo(pkgName, 0); + return ai.isSystemApp(); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + return false; + } + } + private void releaseProxyConnectionLocked(Context ctx) { if (mConnection != null ) { ctx.unbindService(mConnection); @@ -267,7 +286,8 @@ private void connectToProxyLocked(Context ctx) { "ro.vendor.camera.extensions.package"); String vendorProxyService = SystemProperties.get( "ro.vendor.camera.extensions.service"); - 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/net/SntpClient.java b/core/java/android/net/SntpClient.java index 0fb04b9d7299..a897f6c84680 100644 --- a/core/java/android/net/SntpClient.java +++ b/core/java/android/net/SntpClient.java @@ -22,6 +22,7 @@ import android.net.sntp.Timestamp64; import android.os.SystemClock; import android.util.Log; +import android.util.NtpTrustedTime; import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; @@ -31,6 +32,8 @@ import java.net.DatagramSocket; import java.net.InetAddress; import java.net.InetSocketAddress; +import java.net.MalformedURLException; +import java.net.URL; import java.net.UnknownHostException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; @@ -40,6 +43,32 @@ import java.util.Random; import java.util.function.Supplier; +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 java.security.cert.X509Certificate; + +import java.math.BigInteger; +import java.util.Date; +import java.util.Set; +import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.KeyStore; +import java.security.Principal; +import java.security.PublicKey; +import java.security.NoSuchProviderException; +import java.security.SignatureException; + +import static android.os.Build.TIME; + /** * Simple, single-use SNTP client class for retrieving network time. * @@ -94,6 +123,8 @@ public class SntpClient { // Details of the NTP server used to obtain the time last. @Nullable private InetSocketAddress mServerSocketAddress; + private String mNtpMode = NtpTrustedTime.NtpConfig.MODE_HTTPS; + private static class InvalidServerReplyException extends Exception { public InvalidServerReplyException(String message) { super(message); @@ -125,13 +156,24 @@ public SntpClient(Supplier systemTimeSupplier, Random random) { public boolean requestTime(String host, int port, int timeout, Network network) { final Network networkForResolv = network.getPrivateDnsBypassingCopy(); try { - final InetAddress[] addresses = networkForResolv.getAllByName(host); - for (int i = 0; i < addresses.length; i++) { - if (requestTime(addresses[i], port, timeout, networkForResolv)) { - return true; - } + switch (mNtpMode) { + case NtpTrustedTime.NtpConfig.MODE_HTTPS: + URL url = new URL("https://" + host); + return requestHttpTime(url, timeout, network); + case NtpTrustedTime.NtpConfig.MODE_NTP: + final InetAddress[] addresses = networkForResolv.getAllByName(host); + for (int i = 0; i < addresses.length; i++) { + if (requestTime(addresses[i], port, timeout, networkForResolv)) { + return true; + } + } + break; + default: + EventLogTags.writeNtpFailure(host, "unknown protocol " + mNtpMode + " provided"); + if (DBG) Log.d(TAG, "request time failed, wrong protocol"); + return false; } - } catch (UnknownHostException e) { + } catch (UnknownHostException | MalformedURLException e) { Log.w(TAG, "Unknown host: " + host); EventLogTags.writeNtpFailure(host, e.toString()); } @@ -199,7 +241,7 @@ public boolean requestTime(InetAddress address, int port, int timeout, Network n EventLogTags.writeNtpSuccess( address.toString(), roundTripTimeMillis, clockOffsetMillis); if (DBG) { - Log.d(TAG, "round trip: " + roundTripTimeMillis + "ms, " + Log.d(TAG, "default method -- round trip: " + roundTripTimeMillis + "ms, " + "clock offset: " + clockOffsetMillis + "ms"); } @@ -246,6 +288,258 @@ public static Duration calculateClockOffset(Timestamp64 clientRequestTimestamp, .dividedBy(2); } + private boolean requestHttpTime(URL url, int timeout, Network network) { + final int oldTag = TrafficStats.getAndSetThreadStatsTag( + TrafficStatsConstants.TAG_SYSTEM_NTP); + final Network networkForResolv = network.getPrivateDnsBypassingCopy(); + try { + TrustManagerFactory tmf = TrustManagerFactory + .getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init((KeyStore) null); + + X509TrustManager x509Tm = null; + for (TrustManager tm : tmf.getTrustManagers()) { + if (tm instanceof X509TrustManager) { + x509Tm = (X509TrustManager) tm; + break; + } + } + + final X509TrustManager finalTm = x509Tm; + X509TrustManager customTm = new X509TrustManager() { + + // custom eternal certificate class that ignores expired SSL certificates + // adapted from https://gist.github.com/divergentdave/9a68d820e3610513bd4fcdc4ae5f91a1 + class TimeLeewayCertificate extends X509Certificate { + private final X509Certificate originalCertificate; + + public TimeLeewayCertificate(X509Certificate originalCertificate) { + this.originalCertificate = originalCertificate; + } + + @Override + public void checkValidity() throws CertificateExpiredException, CertificateNotYetValidException { + // Ignore notBefore/notAfter + checkValidity(new Date()); + } + + @Override + public void checkValidity(Date date) throws CertificateExpiredException, CertificateNotYetValidException { + // expiration must be set after OS build date + if (getNotAfter().compareTo(new Date(TIME)) < 0) { + throw new CertificateExpiredException("Certificate expired at " + + getNotAfter().toString() + " (compared to " + date.toString() + ")"); + } + } + + @Override + public int getVersion() { + return originalCertificate.getVersion(); + } + + @Override + public BigInteger getSerialNumber() { + return originalCertificate.getSerialNumber(); + } + + @Override + public Principal getIssuerDN() { + return originalCertificate.getIssuerDN(); + } + + @Override + public Principal getSubjectDN() { + return originalCertificate.getSubjectDN(); + } + + @Override + public Date getNotBefore() { + return originalCertificate.getNotBefore(); + } + + @Override + public Date getNotAfter() { + return originalCertificate.getNotAfter(); + } + + @Override + public byte[] getTBSCertificate() throws CertificateEncodingException { + return originalCertificate.getTBSCertificate(); + } + + @Override + public byte[] getSignature() { + return originalCertificate.getSignature(); + } + + @Override + public String getSigAlgName() { + return originalCertificate.getSigAlgName(); + } + + @Override + public String getSigAlgOID() { + return originalCertificate.getSigAlgOID(); + } + + @Override + public byte[] getSigAlgParams() { + return originalCertificate.getSigAlgParams(); + } + + @Override + public boolean[] getIssuerUniqueID() { + return originalCertificate.getIssuerUniqueID(); + } + + @Override + public boolean[] getSubjectUniqueID() { + return originalCertificate.getSubjectUniqueID(); + } + + @Override + public boolean[] getKeyUsage() { + return originalCertificate.getKeyUsage(); + } + + @Override + public int getBasicConstraints() { + return originalCertificate.getBasicConstraints(); + } + + @Override + public byte[] getEncoded() throws CertificateEncodingException { + return originalCertificate.getEncoded(); + } + + @Override + public void verify(PublicKey key) throws CertificateException, + NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, SignatureException + { + originalCertificate.verify(key); + } + + @Override + public void verify(PublicKey key, String sigProvider) throws CertificateException, + NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, SignatureException + { + originalCertificate.verify(key, sigProvider); + } + + @Override + public String toString() { + return originalCertificate.toString(); + } + + @Override + public PublicKey getPublicKey() { + return originalCertificate.getPublicKey(); + } + + @Override + public Set getCriticalExtensionOIDs() { + return originalCertificate.getCriticalExtensionOIDs(); + } + + @Override + public byte[] getExtensionValue(String oid) { + return originalCertificate.getExtensionValue(oid); + } + + @Override + public Set getNonCriticalExtensionOIDs() { + return originalCertificate.getNonCriticalExtensionOIDs(); + } + + @Override + public boolean hasUnsupportedCriticalExtension() { + return originalCertificate.hasUnsupportedCriticalExtension(); + } + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return finalTm.getAcceptedIssuers(); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { + // replace the top-level certificate with a certificate validation routine that + // ignores expired certificates due to clock drift. + X509Certificate[] timeLeewayChain = new X509Certificate[chain.length]; + for (int i = 0; i < chain.length; i++) { + timeLeewayChain[i] = new TimeLeewayCertificate(chain[i]); + } + finalTm.checkServerTrusted(timeLeewayChain, authType); + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + // never gets used, so default to stock. + finalTm.checkClientTrusted(chain, authType); + } + }; + + SSLContext sslContext = SSLContext.getInstance("SSL"); + sslContext.init(null, new TrustManager[] { customTm }, null); + SSLSocketFactory socketFactory = sslContext.getSocketFactory(); + HttpsURLConnection connection = (HttpsURLConnection) networkForResolv.openConnection(url); + try { + connection.setSSLSocketFactory(socketFactory); + connection.setConnectTimeout(timeout); + connection.setReadTimeout(timeout); + connection.getInputStream().close(); + + connection = (HttpsURLConnection) networkForResolv.openConnection(url); + connection.setSSLSocketFactory(socketFactory); + connection.setConnectTimeout(timeout); + connection.setReadTimeout(timeout); + connection.setRequestProperty("Connection", "close"); + + final long requestTime = System.currentTimeMillis(); + final long requestTicks = SystemClock.elapsedRealtime(); + connection.getInputStream(); + long serverTime; + try { + serverTime = Long.parseLong(connection.getHeaderField("X-Time")); + } catch (final NumberFormatException e) { + Log.w(TAG, "https method received response without X-Time header resulting in low precision"); + serverTime = connection.getDate(); + } + final long responseTicks = SystemClock.elapsedRealtime(); + final long roundTripTime = responseTicks - requestTicks; + final long responseTime = requestTime + roundTripTime; + final long clockOffset = ((serverTime - requestTime) + (serverTime - responseTime)) / 2; + if (DBG) { + Log.d(TAG, "https method -- round trip: " + roundTripTime + "ms, " + + "clock offset: " + clockOffset + "ms"); + } + if (serverTime < TIME) { + throw new GeneralSecurityException("https method received timestamp before BUILD unix time, rejecting"); + } + EventLogTags.writeNtpSuccess(url.toString(), roundTripTime, clockOffset); + // save our results - use the times on this side of the network latency + // (response rather than request time) + mClockOffset = clockOffset; + mNtpTime = responseTime + clockOffset; + mNtpTimeReference = responseTicks; + mRoundTripTime = roundTripTime; + } finally { + connection.disconnect(); + } + } catch (Exception e) { + EventLogTags.writeNtpFailure(url.toString(), e.toString()); + Log.e(TAG, "request time failed: " + e.toString()); + if (DBG) { + e.printStackTrace(); + } + return false; + } finally { + TrafficStats.setThreadStatsTag(oldTag); + } + return true; + } + @Deprecated @UnsupportedAppUsage public boolean requestTime(String host, int timeout) { @@ -300,6 +594,13 @@ public InetSocketAddress getServerSocketAddress() { return mServerSocketAddress; } + /** + * Sets the ntp mode + */ + public void setNtpMode(String mode) { + mNtpMode = mode; + } + private static void checkValidServerReply( byte leap, byte mode, int stratum, Timestamp64 transmitTimestamp, Timestamp64 referenceTimestamp, Timestamp64 randomizedRequestTimestamp, diff --git a/core/java/android/nfc/NfcAdapter.java b/core/java/android/nfc/NfcAdapter.java index 9e97216ac632..7b50a955dacf 100644 --- a/core/java/android/nfc/NfcAdapter.java +++ b/core/java/android/nfc/NfcAdapter.java @@ -29,6 +29,7 @@ import android.app.ActivityThread; import android.app.OnActivityPausedListener; import android.app.PendingIntent; +import android.app.compat.gms.GmsCompat; import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.content.IntentFilter; @@ -45,6 +46,8 @@ import android.os.RemoteException; import android.util.Log; +import com.android.internal.gmscompat.GmsHooks; + import java.io.IOException; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -948,6 +951,13 @@ public int getAdapterState() { @SystemApi @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean enable() { + if (GmsCompat.isEnabled()) { + if (!GmsCompat.hasPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS)) { + GmsHooks.enableNfc(); + return false; + } + } + try { return sService.enable(); } catch (RemoteException e) { diff --git a/core/java/android/os/AppZygote.java b/core/java/android/os/AppZygote.java index 07fbe4a04ff1..3959902cdd21 100644 --- a/core/java/android/os/AppZygote.java +++ b/core/java/android/os/AppZygote.java @@ -16,6 +16,7 @@ package android.os; +import android.annotation.Nullable; import android.content.pm.ApplicationInfo; import android.content.pm.ProcessInfo; import android.util.Log; @@ -72,11 +73,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(@Nullable String flatExtraArgs) { synchronized (mLock) { if (mZygote != null) return mZygote; - connectToZygoteIfNeededLocked(); + connectToZygoteIfNeededLocked(flatExtraArgs); return mZygote; } } @@ -105,7 +106,7 @@ private void stopZygoteLocked() { } @GuardedBy("mLock") - private void connectToZygoteIfNeededLocked() { + private void connectToZygoteIfNeededLocked(@Nullable String flatExtraArgs) { String abi = mAppInfo.primaryCpuAbi != null ? mAppInfo.primaryCpuAbi : Build.SUPPORTED_ABIS[0]; try { @@ -123,7 +124,8 @@ private void connectToZygoteIfNeededLocked() { abi, // acceptedAbiList VMRuntime.getInstructionSet(abi), // instructionSet mZygoteUidGidMin, - mZygoteUidGidMax); + mZygoteUidGidMax, + flatExtraArgs); ZygoteProcess.waitForConnectionToZygote(mZygote.getPrimarySocketAddress()); // preload application code in the zygote diff --git a/core/java/android/os/BaseBundle.java b/core/java/android/os/BaseBundle.java index 4e3adfbdda3f..a5951713f5ab 100644 --- a/core/java/android/os/BaseBundle.java +++ b/core/java/android/os/BaseBundle.java @@ -20,6 +20,8 @@ import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SuppressLint; +import android.annotation.SystemApi; import android.compat.annotation.UnsupportedAppUsage; import android.util.ArrayMap; import android.util.Log; @@ -1955,4 +1957,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 01e8fea1019d..510ed3ea2482 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.util.ExceptionUtils; import android.util.Log; import android.util.Slog; import com.android.internal.annotations.VisibleForTesting; +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; @@ -642,6 +644,32 @@ public static final boolean isProxy(IInterface iface) { */ public static final native void blockUntilThreadAvailable(); + + /** + * TODO (b/308179628): Move this to libbinder for non-Java usages. + */ + private static IBinderCallback sBinderCallback = null; + + /** + * Set callback function for unexpected binder transaction errors. + * + * @hide + */ + public static final void setTransactionCallback(IBinderCallback callback) { + sBinderCallback = callback; + } + + /** + * Execute the callback function if it's already set. + * + * @hide + */ + public static final void transactionCallback(int pid, int code, int flags, int err) { + if (sBinderCallback != null) { + sBinderCallback.onTransactionError(pid, code, flags, err); + } + } + /** * Default constructor just initializes the object. * @@ -688,8 +716,18 @@ 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); + } } + private boolean mIsIGmsCallbacks; + private boolean mIsGmsServiceBroker; + /** * Default implementation returns an empty interface name. */ @@ -1254,10 +1292,22 @@ 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) { + final int binderCallingUid = Binder.getCallingUid(); + if (GmsCompat.isEnabled()) { + if (binderCallingUid != mPreviousUid) { + // harmless race + mPreviousUid = binderCallingUid; + if (Process.isApplicationUid(binderCallingUid)) { + GmsHooks.onBinderTransaction(Binder.getCallingPid(), binderCallingUid); + } + } + } Parcel data = Parcel.obtain(dataObj); Parcel reply = Parcel.obtain(replyObj); @@ -1270,7 +1320,7 @@ private boolean execTransact(int code, long dataObj, long replyObj, // for Java now // // This attribution support is not generic and therefore not support in RPC mode - final int callingUid = data.isForRpc() ? -1 : Binder.getCallingUid(); + final int callingUid = data.isForRpc() ? -1 : binderCallingUid; final long origWorkSource = callingUid == -1 ? -1 : ThreadLocalWorkSource.setUid(callingUid); @@ -1311,7 +1361,12 @@ private boolean execTransactInternal(int code, Parcel data, Parcel reply, int fl } final boolean tracingEnabled = tagEnabled && transactionTraceName != null; + data.mCallMaybeOverrideBinder = mIsIGmsCallbacks; + boolean onBeginGmsServiceBrokerCallRet = false; try { + if (mIsGmsServiceBroker) { + onBeginGmsServiceBrokerCallRet = GmsHooks.onBeginGmsServiceBrokerCall(code, data); + } // TODO - 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; @@ -1357,6 +1412,10 @@ private boolean execTransactInternal(int code, Parcel data, Parcel reply, int fl } res = true; } finally { + data.mCallMaybeOverrideBinder = false; + if (onBeginGmsServiceBrokerCallRet) { + GmsHooks.onEndGmsServiceBrokerCall(); + } if (tracingEnabled) { Trace.traceEnd(Trace.TRACE_TAG_AIDL); } diff --git a/core/java/android/os/BinderDef.aidl b/core/java/android/os/BinderDef.aidl new file mode 100644 index 000000000000..6b55861e809a --- /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 000000000000..f846f3ed0f73 --- /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/Build.java b/core/java/android/os/Build.java index cd6acfe149f8..1db03dc2d650 100755 --- a/core/java/android/os/Build.java +++ b/core/java/android/os/Build.java @@ -25,6 +25,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.sysprop.DeviceProperties; @@ -35,6 +36,8 @@ import android.util.Slog; import android.view.View; +import com.android.internal.gmscompat.GmsHooks; + import dalvik.system.VMRuntime; import java.util.ArrayList; @@ -255,6 +258,16 @@ public class Build { @SuppressAutoDoc // No support for device / profile owner. @RequiresPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE) public static String getSerial() { + if (GmsCompat.isEnabled()) { + boolean shouldHook = + !GmsCompat.hasPermission(Manifest.permission.READ_PRIVILEGED_PHONE_STATE) + && !GmsCompat.hasPermission(Manifest.permission.READ_DEVICE_SERIAL_NUMBER); + + if (shouldHook) { + return GmsHooks.getSerial(); + } + } + IDeviceIdentifiersPolicyService service = IDeviceIdentifiersPolicyService.Stub .asInterface(ServiceManager.getService(Context.DEVICE_IDENTIFIERS_SERVICE)); try { @@ -1342,7 +1355,6 @@ public static boolean isBuildConsistent() { } } - /* TODO: Figure out issue with checks failing if (!TextUtils.isEmpty(bootimage)) { if (!Objects.equals(system, bootimage)) { Slog.e(TAG, "Mismatched fingerprints; system reported " + system @@ -1359,14 +1371,13 @@ public static boolean isBuildConsistent() { } } - if (!TextUtils.isEmpty(requiredRadio)) { + if (!TextUtils.isEmpty(requiredRadio) && !TextUtils.isEmpty(currentRadio)) { if (!Objects.equals(currentRadio, requiredRadio)) { Slog.e(TAG, "Mismatched radio version: build requires " + requiredRadio + " but runtime reports " + currentRadio); return false; } } - */ return true; } diff --git a/core/java/android/os/Environment.java b/core/java/android/os/Environment.java index 536ef31f334a..445d9fd10b6d 100644 --- a/core/java/android/os/Environment.java +++ b/core/java/android/os/Environment.java @@ -1594,4 +1594,10 @@ public static File buildPath(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(); + } } diff --git a/core/java/android/os/HybridBinder.java b/core/java/android/os/HybridBinder.java new file mode 100644 index 000000000000..0776baebc18a --- /dev/null +++ b/core/java/android/os/HybridBinder.java @@ -0,0 +1,85 @@ +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 ("original") binder. + * + * @hide + */ +public final class HybridBinder implements IBinder { + private static final String TAG = "HybridBinder"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.VERBOSE); + + private final IBinder original; + private final IBinder secondBinder; + // sorted array of handled transactions codes + private final int[] secondBinderTxnCodes; + + public HybridBinder(Context ctx, IBinder original, BinderDef secondBinderDef) { + this.original = original; + 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 original.transact(code, data, reply, flags); + } + + @Nullable + public IInterface queryLocalInterface(@NonNull String descriptor) { + return null; + } + + @Nullable + public String getInterfaceDescriptor() throws RemoteException { + return original.getInterfaceDescriptor(); + } + + public boolean pingBinder() { + return original.pingBinder(); + } + + public boolean isBinderAlive() { + return original.isBinderAlive(); + } + + public void dump(@NonNull FileDescriptor fd, @Nullable String[] args) throws RemoteException { + original.dump(fd, args); + } + + public void dumpAsync(@NonNull FileDescriptor fd, @Nullable String[] args) throws RemoteException { + original.dumpAsync(fd, args); + } + + public void shellCommand(@Nullable FileDescriptor in, @Nullable FileDescriptor out, @Nullable FileDescriptor err, @NonNull String[] args, @Nullable ShellCallback shellCallback, @NonNull ResultReceiver resultReceiver) throws RemoteException { + original.shellCommand(in, out, err, args, shellCallback, resultReceiver); + } + + public void linkToDeath(@NonNull DeathRecipient recipient, int flags) throws RemoteException { + original.linkToDeath(recipient, flags); + } + + public boolean unlinkToDeath(@NonNull DeathRecipient recipient, int flags) { + return original.unlinkToDeath(recipient, flags); + } + + @Nullable + public IBinder getExtension() throws RemoteException { + return original.getExtension(); + } +} diff --git a/core/java/android/os/IBinderCallback.java b/core/java/android/os/IBinderCallback.java new file mode 100644 index 000000000000..e4be5b02e7e0 --- /dev/null +++ b/core/java/android/os/IBinderCallback.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2023 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 android.os; + +/** + * Callback interface for binder transaction errors + * + * @hide + */ +public interface IBinderCallback { + /** + * Callback function for unexpected binder transaction errors. + * + * @param debugPid The binder transaction sender + * @param code The binder transaction code + * @param flags The binder transaction flags + * @param err The binder transaction error + */ + void onTransactionError(int debugPid, int code, int flags, int err); +} diff --git a/core/java/android/os/Parcel.java b/core/java/android/os/Parcel.java index e784c2669575..72619976a4b0 100644 --- a/core/java/android/os/Parcel.java +++ b/core/java/android/os/Parcel.java @@ -26,6 +26,7 @@ import android.annotation.SuppressLint; import android.annotation.TestApi; import android.app.AppOpsManager; +import android.app.compat.gms.GmsCompat; import android.compat.annotation.UnsupportedAppUsage; import android.text.TextUtils; import android.util.ArrayMap; @@ -42,6 +43,7 @@ import android.util.SparseIntArray; import com.android.internal.annotations.GuardedBy; +import com.android.internal.gmscompat.GmsHooks; import com.android.internal.util.ArrayUtils; import dalvik.annotation.optimization.CriticalNative; @@ -3028,6 +3030,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); } @@ -3160,6 +3169,9 @@ public final CharSequence readCharSequence() { return TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(this); } + /** {@hide} */ + public boolean mCallMaybeOverrideBinder; + /** * Read an object from the parcel at the current dataPosition(). */ @@ -3172,6 +3184,14 @@ public final IBinder readStrongBinder() { FLAG_IS_REPLY_FROM_BLOCKING_ALLOWED_OBJECT | FLAG_PROPAGATE_ALLOW_BLOCKING)) { Binder.allowBlocking(result); } + + if (mCallMaybeOverrideBinder && result != null) { + IBinder override = GmsHooks.maybeOverrideBinder(result); + if (override != null) { + return override; + } + } + return result; } diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java index 04525e8b8ff7..c80ea4d13752 100644 --- a/core/java/android/os/Process.java +++ b/core/java/android/os/Process.java @@ -730,13 +730,14 @@ public static ProcessStartResult start(@NonNull final String processClass, whitelistedDataInfoMap, boolean bindMountAppsData, boolean bindMountAppStorageDirs, - @Nullable String[] zygoteArgs) { + @Nullable String[] zygoteArgs, + @Nullable String flatExtraArgs) { return ZYGOTE_PROCESS.start(processClass, niceName, uid, gid, gids, runtimeFlags, mountExternal, targetSdkVersion, seInfo, abi, instructionSet, appDataDir, invokeWith, packageName, zygotePolicyFlags, isTopApp, disabledCompatChanges, pkgDataInfoMap, whitelistedDataInfoMap, bindMountAppsData, - bindMountAppStorageDirs, zygoteArgs); + bindMountAppStorageDirs, zygoteArgs, flatExtraArgs); } /** @hide */ @@ -753,7 +754,8 @@ public static ProcessStartResult startWebView(@NonNull final String processClass @Nullable String invokeWith, @Nullable String packageName, @Nullable long[] disabledCompatChanges, - @Nullable String[] zygoteArgs) { + @Nullable String[] zygoteArgs, + @Nullable String flatExtraArgs) { // Webview zygote can't access app private data files, so doesn't need to know its data // info. return WebViewZygote.getProcess().start(processClass, niceName, uid, gid, gids, @@ -761,7 +763,8 @@ public static ProcessStartResult startWebView(@NonNull final String processClass abi, instructionSet, appDataDir, invokeWith, packageName, /*zygotePolicyFlags=*/ ZYGOTE_POLICY_FLAG_EMPTY, /*isTopApp=*/ false, disabledCompatChanges, /* pkgDataInfoMap */ null, - /* whitelistedDataInfoMap */ null, false, false, zygoteArgs); + /* whitelistedDataInfoMap */ null, false, false, zygoteArgs, + flatExtraArgs); } /** diff --git a/core/java/android/os/UserHandle.java b/core/java/android/os/UserHandle.java index ef3901082833..23914a439c91 100644 --- a/core/java/android/os/UserHandle.java +++ b/core/java/android/os/UserHandle.java @@ -22,6 +22,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; @@ -279,6 +280,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) { @@ -369,6 +371,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) { @@ -572,6 +575,10 @@ public static void formatUid(PrintWriter pw, int uid) { @Deprecated @SystemApi public boolean isOwner() { + if (GmsCompat.isEnabled()) { + return isSystem(); + } + return this.equals(OWNER); } @@ -581,6 +588,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 2d217186c9c7..a6ec7866902b 100644 --- a/core/java/android/os/UserManager.java +++ b/core/java/android/os/UserManager.java @@ -2994,8 +2994,8 @@ private 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()) { diff --git a/core/java/android/os/ZygoteProcess.java b/core/java/android/os/ZygoteProcess.java index 3cb5c60259eb..edbb215767db 100644 --- a/core/java/android/os/ZygoteProcess.java +++ b/core/java/android/os/ZygoteProcess.java @@ -355,7 +355,8 @@ public final Process.ProcessStartResult start(@NonNull final String processClass allowlistedDataInfoList, boolean bindMountAppsData, boolean bindMountAppStorageDirs, - @Nullable String[] zygoteArgs) { + @Nullable String[] zygoteArgs, + @Nullable String flatExtraArgs) { // TODO (chriswailes): Is there a better place to check this value? if (fetchUsapPoolEnabledPropWithMinInterval()) { informZygotesOfUsapPoolStatus(); @@ -367,7 +368,7 @@ public final Process.ProcessStartResult start(@NonNull final String processClass abi, instructionSet, appDataDir, invokeWith, /*startChildZygote=*/ false, packageName, zygotePolicyFlags, isTopApp, disabledCompatChanges, pkgDataInfoMap, allowlistedDataInfoList, bindMountAppsData, - bindMountAppStorageDirs, zygoteArgs); + bindMountAppStorageDirs, zygoteArgs, flatExtraArgs); } catch (ZygoteStartFailedEx ex) { Log.e(LOG_TAG, "Starting VM process through Zygote failed"); @@ -638,7 +639,8 @@ private Process.ProcessStartResult startViaZygote(@NonNull final String processC allowlistedDataInfoList, boolean bindMountAppsData, boolean bindMountAppStorageDirs, - @Nullable String[] extraArgs) + @Nullable String[] extraArgs, + @Nullable String flatExtraArgs) throws ZygoteStartFailedEx { ArrayList argsForZygote = new ArrayList<>(); @@ -768,6 +770,10 @@ private Process.ProcessStartResult startViaZygote(@NonNull final String processC argsForZygote.add(sb.toString()); } + if (flatExtraArgs != null) { + argsForZygote.add(flatExtraArgs); + } + argsForZygote.add(processClass); if (extraArgs != null) { @@ -1285,7 +1291,8 @@ public ChildZygoteProcess startChildZygote(final String processClass, String acceptedAbiList, String instructionSet, int uidRangeStart, - int uidRangeEnd) { + int uidRangeEnd, + @Nullable String flatExtraArgs) { // Create an unguessable address in the global abstract namespace. final LocalSocketAddress serverAddress = new LocalSocketAddress( processClass + "/" + UUID.randomUUID().toString()); @@ -1306,7 +1313,7 @@ public ChildZygoteProcess startChildZygote(final String processClass, ZYGOTE_POLICY_FLAG_SYSTEM_PROCESS /* zygotePolicyFlags */, false /* isTopApp */, null /* disabledCompatChanges */, null /* pkgDataInfoMap */, null /* allowlistedDataInfoList */, true /* bindMountAppsData*/, - /* bindMountAppStorageDirs */ false, extraArgs); + /* bindMountAppStorageDirs */ false, extraArgs, flatExtraArgs); } catch (ZygoteStartFailedEx ex) { throw new RuntimeException("Starting child-zygote through Zygote failed", ex); diff --git a/core/java/android/os/logcat/ILogcatManagerService.aidl b/core/java/android/os/logcat/ILogcatManagerService.aidl index 67a930a17665..6a74b4dfa37c 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/permission/IPermissionManager.aidl b/core/java/android/permission/IPermissionManager.aidl index 16ae3bc64315..74aca77e67ad 100644 --- a/core/java/android/permission/IPermissionManager.aidl +++ b/core/java/android/permission/IPermissionManager.aidl @@ -93,4 +93,6 @@ interface IPermissionManager { void registerAttributionSource(in AttributionSourceState source); boolean isRegisteredAttributionSource(in AttributionSourceState source); + + void updatePermissionState(String packageName, int userId); } diff --git a/core/java/android/permission/PermissionManager.java b/core/java/android/permission/PermissionManager.java index b0dda6f6ced2..a40f30642e34 100644 --- a/core/java/android/permission/PermissionManager.java +++ b/core/java/android/permission/PermissionManager.java @@ -22,6 +22,7 @@ import static android.content.pm.PackageManager.FLAG_PERMISSION_SYSTEM_FIXED; import static android.content.pm.PackageManager.FLAG_PERMISSION_USER_FIXED; import static android.content.pm.PackageManager.FLAG_PERMISSION_USER_SET; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; import static android.os.Build.VERSION_CODES.S; import android.Manifest; @@ -49,6 +50,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; @@ -70,6 +72,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; @@ -84,6 +87,8 @@ import java.util.Objects; import java.util.Set; +import libcore.util.EmptyArray; + /** * System level service for accessing the permission capabilities of the platform. * @@ -164,6 +169,11 @@ 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 CELL_BROADCAST_SERVICE_PKG = "com.android.cellbroadcastservice"; /** * Refuse to install package if groups of permissions are bad @@ -186,7 +196,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]; @@ -1171,12 +1181,32 @@ 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); + pkgNames.add(FUSED_LOCATION_PKG); + // indicator pops up when determining location during a geofenced alert + pkgNames.add(CELL_BROADCAST_SERVICE_PKG); + 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; } @@ -1521,12 +1551,22 @@ private static int checkPermissionUncached(@Nullable String permission, int pid, + permission); return PackageManager.PERMISSION_DENIED; } + int res; try { sShouldWarnMissingActivityManager = true; - return am.checkPermission(permission, pid, uid); + res = am.checkPermission(permission, pid, uid); } 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; } /** @@ -1659,12 +1699,24 @@ public boolean equals(@Nullable Object rval) { /* @hide */ private static int checkPackageNamePermissionUncached( String permName, String pkgName, @UserIdInt int userId) { + int res; try { - return ActivityThread.getPackageManager().checkPermission( + res = ActivityThread.getPackageManager().checkPermission( permName, pkgName, userId); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } + + if (res != PERMISSION_GRANTED) { + if (pkgName.equals(ActivityThread.currentPackageName()) + && userId == UserHandle.myUserId() + && AppPermissionUtils.shouldSpoofSelfCheck(permName)) + { + res = PERMISSION_GRANTED; + } + } + + return res; } /* @hide */ @@ -1734,4 +1786,12 @@ public boolean handleMessage(Message msg) { } } } + + public void updatePermissionState(@NonNull String packageName, int userId) { + try { + mPermissionManager.updatePermissionState(packageName, userId); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } } diff --git a/core/java/android/permission/PermissionUsageHelper.java b/core/java/android/permission/PermissionUsageHelper.java index 2a4c01e1c46e..f23c42c776cf 100644 --- a/core/java/android/permission/PermissionUsageHelper.java +++ b/core/java/android/permission/PermissionUsageHelper.java @@ -118,7 +118,7 @@ private static boolean shouldShowIndicators() { private static boolean shouldShowLocationIndicator() { return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_PRIVACY, - PROPERTY_LOCATION_INDICATORS_ENABLED, false); + PROPERTY_LOCATION_INDICATORS_ENABLED, true); } private static long getRecentThreshold(Long now) { diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index a55183c0f7c5..72eb7a12254c 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -40,6 +40,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.ComponentName; import android.content.ContentResolver; @@ -56,6 +57,8 @@ import android.database.ContentObserver; import android.database.Cursor; import android.database.SQLException; +import android.ext.settings.ExtSettings; +import android.ext.settings.Setting; import android.location.ILocationManager; import android.location.LocationManager; import android.media.AudioManager; @@ -98,6 +101,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; @@ -3179,9 +3183,31 @@ private NameValueCache(Uri uri, String getCommand, mReadableFieldsWithMaxTargetSdk); } + // 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 int userHandle, 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); @@ -3242,6 +3268,15 @@ public boolean deleteStringForUser(ContentResolver cr, String name, final int us @UnsupportedAppUsage public String getStringForUser(ContentResolver cr, String name, final int userHandle) { + if (GmsCompat.isEnabled()) { + String ns = maybeGetGmsCompatNamespace(); + if (ns != null) { + if (!mAllFields.contains(name) && !name.startsWith("gmscompat")) { + return GmsCompatApp.getString(ns, name); + } + } + } + final boolean isSelf = (userHandle == UserHandle.myUserId()); final boolean useCache = isSelf && !isInSystemServer(); boolean needsGenerationTracker = false; @@ -3289,6 +3324,10 @@ public String getStringForUser(ContentResolver cr, String name, final int userHa // still be regarded as readable. if (!isCallerExemptFromReadableRestriction() && mAllFields.contains(name)) { if (!mReadableFields.contains(name)) { + if (GmsCompat.isEnabled()) { + return null; + } + throw new SecurityException( "Settings key: <" + name + "> is not readable. From S+, settings keys " + "annotated with @hide are restricted to system_server and " @@ -3305,6 +3344,10 @@ public String getStringForUser(ContentResolver cr, String name, final int userHa && 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: " @@ -3689,6 +3732,17 @@ private static void getPublicSettingsForClass( } } } + + Setting.Scope extSettingsScope = null; + if (callerClass == Global.class) { + extSettingsScope = Setting.Scope.GLOBAL; + } else if (callerClass == Secure.class) { + extSettingsScope = Setting.Scope.PER_USER; + } + + if (extSettingsScope != null) { + ExtSettings.getKeys(extSettingsScope, allKeys); + } } catch (IllegalAccessException ignored) { } } @@ -6299,6 +6353,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 @@ -10013,6 +10072,17 @@ public static boolean putFloatForUser(ContentResolver cr, String name, float val public static final String LOCK_SCREEN_SHOW_ONLY_UNSEEN_NOTIFICATIONS = "lock_screen_show_only_unseen_notifications"; + /** + * 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 + */ + 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 *

@@ -16864,6 +16934,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; diff --git a/core/java/android/speech/tts/TtsEngines.java b/core/java/android/speech/tts/TtsEngines.java index a8aea7c1eb59..3697b9494949 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 434b1c76113f..aa5ef36c44da 100644 --- a/core/java/android/telephony/TelephonyRegistryManager.java +++ b/core/java/android/telephony/TelephonyRegistryManager.java @@ -19,6 +19,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.RequiresPermission; +import android.app.compat.gms.GmsCompat; import android.compat.Compatibility; import android.compat.annotation.ChangeId; import android.compat.annotation.EnabledAfter; @@ -45,6 +46,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; @@ -273,6 +275,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) { @@ -294,6 +303,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/util/FeatureFlagUtils.java b/core/java/android/util/FeatureFlagUtils.java index 827600c83fae..922cdf748147 100644 --- a/core/java/android/util/FeatureFlagUtils.java +++ b/core/java/android/util/FeatureFlagUtils.java @@ -233,7 +233,7 @@ public class FeatureFlagUtils { DEFAULT_FLAGS.put(SETTINGS_NEW_KEYBOARD_MODIFIER_KEY, "true"); DEFAULT_FLAGS.put(SETTINGS_NEW_KEYBOARD_TRACKPAD, "true"); DEFAULT_FLAGS.put(SETTINGS_NEW_KEYBOARD_TRACKPAD_GESTURE, "false"); - DEFAULT_FLAGS.put(SETTINGS_ENABLE_SPA, "true"); + DEFAULT_FLAGS.put(SETTINGS_ENABLE_SPA, "false"); DEFAULT_FLAGS.put(SETTINGS_ENABLE_SPA_PHASE2, "false"); DEFAULT_FLAGS.put(SETTINGS_ENABLE_SPA_METRICS, "false"); DEFAULT_FLAGS.put(SETTINGS_ADB_METRICS_WRITER, "false"); diff --git a/core/java/android/util/LongArray.java b/core/java/android/util/LongArray.java index 9f269ed74048..34d3add1e92f 100644 --- a/core/java/android/util/LongArray.java +++ b/core/java/android/util/LongArray.java @@ -120,6 +120,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. */ @@ -134,7 +142,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 5aa0f59024f8..183e58662415 100644 --- a/core/java/android/util/NtpTrustedTime.java +++ b/core/java/android/util/NtpTrustedTime.java @@ -66,9 +66,12 @@ public abstract class NtpTrustedTime implements TrustedTime { * @hide */ public static final class NtpConfig { + public static final String MODE_HTTPS = "https"; + public static final String MODE_NTP = "ntp"; @NonNull private final List mServerUris; @NonNull private final Duration mTimeout; + @NonNull private final String mNtpMode; /** * Creates an instance with the supplied properties. There must be at least one NTP server @@ -78,7 +81,11 @@ public static final class NtpConfig { * See {@link #parseNtpUriStrict(String)} and {@link #parseNtpServerSetting(String)} to * create valid URIs. */ - public NtpConfig(@NonNull List serverUris, @NonNull Duration timeout) + public NtpConfig(@NonNull List serverUris, @NonNull Duration timeout) { + this(serverUris, timeout, MODE_HTTPS); + } + + public NtpConfig(@NonNull List serverUris, @NonNull Duration timeout, @NonNull String ntpMode) throws IllegalArgumentException { Objects.requireNonNull(serverUris); @@ -102,6 +109,7 @@ public NtpConfig(@NonNull List serverUris, @NonNull Duration timeout) throw new IllegalArgumentException("timeout < 0"); } mTimeout = timeout; + mNtpMode = ntpMode; } /** Returns a non-empty, immutable list of NTP server URIs. */ @@ -115,6 +123,11 @@ public Duration getTimeout() { return mTimeout; } + @NonNull + public String getNtpMode() { + return mNtpMode; + } + @Override public String toString() { return "NtpConnectionInfo{" @@ -134,15 +147,15 @@ public static class TimeResult { private final long mUnixEpochTimeMillis; private final long mElapsedRealtimeMillis; private final int mUncertaintyMillis; - @NonNull private final InetSocketAddress mNtpServerSocketAddress; + @Nullable private final InetSocketAddress mNtpServerSocketAddress; public TimeResult( long unixEpochTimeMillis, long elapsedRealtimeMillis, int uncertaintyMillis, - @NonNull InetSocketAddress ntpServerSocketAddress) { + @Nullable InetSocketAddress ntpServerSocketAddress) { mUnixEpochTimeMillis = unixEpochTimeMillis; mElapsedRealtimeMillis = elapsedRealtimeMillis; mUncertaintyMillis = uncertaintyMillis; - mNtpServerSocketAddress = Objects.requireNonNull(ntpServerSocketAddress); + mNtpServerSocketAddress = ntpServerSocketAddress; } public long getTimeMillis() { @@ -190,14 +203,13 @@ public boolean equals(Object o) { return mUnixEpochTimeMillis == that.mUnixEpochTimeMillis && mElapsedRealtimeMillis == that.mElapsedRealtimeMillis && mUncertaintyMillis == that.mUncertaintyMillis - && mNtpServerSocketAddress.equals( - that.mNtpServerSocketAddress); + && Objects.equals(mNtpServerSocketAddress, that.mNtpServerSocketAddress); } @Override public int hashCode() { return Objects.hash(mUnixEpochTimeMillis, mElapsedRealtimeMillis, mUncertaintyMillis, - mNtpServerSocketAddress); + Objects.hashCode(mNtpServerSocketAddress)); } @Override @@ -253,7 +265,14 @@ 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() { + final ContentResolver resolver = getContext().getContentResolver(); + + if (Settings.Global.getInt(resolver, Settings.Global.AUTO_TIME, 1) == 0) { + Log.d(TAG, "forceRefresh: nitzTimeUpdate disabled bailing early"); + return false; + } + synchronized (this) { Network network = getDefaultNetwork(); if (network == null) { @@ -269,6 +288,13 @@ public boolean forceRefresh() { public boolean forceRefresh(@NonNull Network network) { Objects.requireNonNull(network); + final ContentResolver resolver = getContext().getContentResolver(); + + if (Settings.Global.getInt(resolver, Settings.Global.AUTO_TIME, 1) == 0) { + Log.d(TAG, "forceRefresh: nitzTimeUpdate disabled bailing early"); + return false; + } + synchronized (this) { return forceRefreshLocked(network); } @@ -573,7 +599,7 @@ private static URI validateNtpServerUri(@NonNull URI uri) throws URISyntaxExcept if (!uri.isAbsolute()) { throw new URISyntaxException(uri.toString(), "Relative URI not supported"); } - if (!URI_SCHEME_NTP.equals(uri.getScheme())) { + if (!URI_SCHEME_NTP.equals(uri.getScheme()) && !"https".equals(uri.getScheme())) { throw new URISyntaxException(uri.toString(), "Unrecognized scheme"); } String host = uri.getHost(); @@ -620,33 +646,34 @@ public NtpConfig getNtpConfigInternal() { final ContentResolver resolver = mContext.getContentResolver(); final Resources res = mContext.getResources(); - // The Settings value has priority over static config. Check settings first. - final String serverGlobalSetting = - Settings.Global.getString(resolver, Settings.Global.NTP_SERVER); - final List settingsServerUris = parseNtpServerSetting(serverGlobalSetting); + // TODO: add a dynamic setting to switch to standard NTP + String ntpMode = res.getString(com.android.internal.R.string.config_ntpMode); - List ntpServerUris; - if (settingsServerUris != null) { - ntpServerUris = settingsServerUris; + int resConfigId; + if (NtpConfig.MODE_HTTPS.equals(ntpMode)) { + resConfigId = com.android.internal.R.array.config_httpsTimeServers; } else { - String[] configValues = - res.getStringArray(com.android.internal.R.array.config_ntpServers); - try { - List configServerUris = new ArrayList<>(); - for (String configValue : configValues) { - configServerUris.add(parseNtpUriStrict(configValue)); - } - ntpServerUris = configServerUris; - } catch (URISyntaxException e) { - ntpServerUris = null; + resConfigId = com.android.internal.R.array.config_ntpServers; + } + + String[] configValues = res.getStringArray(resConfigId); + List ntpServerUris; + try { + List configServerUris = new ArrayList<>(); + for (String configValue : configValues) { + configServerUris.add(parseNtpUriStrict(configValue)); } + ntpServerUris = configServerUris; + } catch (URISyntaxException e) { + Log.e(TAG, "", e); + ntpServerUris = null; } final int defaultTimeoutMillis = res.getInteger(com.android.internal.R.integer.config_ntpTimeout); final Duration timeout = Duration.ofMillis(Settings.Global.getInt( resolver, Settings.Global.NTP_TIMEOUT, defaultTimeoutMillis)); - return ntpServerUris == null ? null : new NtpConfig(ntpServerUris, timeout); + return ntpServerUris == null ? null : new NtpConfig(ntpServerUris, timeout, ntpMode); } @Override @@ -698,6 +725,7 @@ public TimeResult queryNtpServer( @NonNull Network network, @NonNull URI ntpServerUri, @NonNull Duration timeout) { final SntpClient client = new SntpClient(); + client.setNtpMode(getNtpConfigInternal().getNtpMode()); final String serverName = ntpServerUri.getHost(); final int port = ntpServerUri.getPort() == -1 ? SntpClient.STANDARD_NTP_PORT : ntpServerUri.getPort(); @@ -725,5 +753,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 ea7efc79de87..8531804f9f00 100644 --- a/core/java/android/util/PackageUtils.java +++ b/core/java/android/util/PackageUtils.java @@ -19,9 +19,12 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; +import android.content.Context; +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 +34,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. @@ -247,4 +252,34 @@ 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 { + return pm.getApplicationInfo(pkg, 0).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/view/inputmethod/InputMethodManagerGlobal.java b/core/java/android/view/inputmethod/InputMethodManagerGlobal.java index 5df9fd15c47a..4c59d87ced47 100644 --- a/core/java/android/view/inputmethod/InputMethodManagerGlobal.java +++ b/core/java/android/view/inputmethod/InputMethodManagerGlobal.java @@ -102,4 +102,11 @@ public static boolean isImeTraceEnabled() { public static void removeImeSurface(@Nullable Consumer exceptionHandler) { IInputMethodManagerGlobalInvoker.removeImeSurface(exceptionHandler); } + + @AnyThread + @Nullable + @RequiresPermission(value = Manifest.permission.INTERACT_ACROSS_USERS_FULL, conditional = true) + public static InputMethodSubtype getCurrentInputMethodSubtype(int userId) { + return IInputMethodManagerGlobalInvoker.getCurrentInputMethodSubtype(userId); + } } diff --git a/core/java/android/webkit/WebViewZygote.java b/core/java/android/webkit/WebViewZygote.java index bc7a5fda6f7a..8e17741de1a9 100644 --- a/core/java/android/webkit/WebViewZygote.java +++ b/core/java/android/webkit/WebViewZygote.java @@ -142,7 +142,8 @@ private static void connectToZygoteIfNeededLocked() { TextUtils.join(",", Build.SUPPORTED_ABIS), null, // instructionSet Process.FIRST_ISOLATED_UID, - Integer.MAX_VALUE); // TODO(b/123615476) deal with user-id ranges properly + Integer.MAX_VALUE, // TODO(b/123615476) deal with user-id ranges properly + null /* flatExtraArgs */); ZygoteProcess.waitForConnectionToZygote(sZygote.getPrimarySocketAddress()); sZygote.preloadApp(sPackage.applicationInfo, abi); } catch (Exception e) { diff --git a/core/java/com/android/internal/app/ChooserActivity.java b/core/java/com/android/internal/app/ChooserActivity.java index 0a726d99723a..77f523bdc82f 100644 --- a/core/java/com/android/internal/app/ChooserActivity.java +++ b/core/java/com/android/internal/app/ChooserActivity.java @@ -2853,7 +2853,7 @@ public Intent getReferrerFillInIntent() { @Override // ChooserListCommunicator public int getMaxRankedTargets() { - return mMaxTargetsPerRow; + return mMaxTargetsPerRow * 2; } @Override // ChooserListCommunicator 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 000000000000..5335b82f2808 --- /dev/null +++ b/core/java/com/android/internal/app/ContactScopes.java @@ -0,0 +1,169 @@ +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.database.ContentObserver; +import android.ext.cscopes.ContactScopesApi; +import android.net.Uri; +import android.provider.ContactsContract; +import android.provider.SimPhonebookContract; + +import com.android.internal.app.ContentProviderRedirector.ContentObserverList; + +import static android.content.pm.GosPackageState.DFLAG_HAS_GET_ACCOUNTS_DECLARATION; +import static android.content.pm.GosPackageState.DFLAG_HAS_READ_CONTACTS_DECLARATION; +import static android.content.pm.GosPackageState.DFLAG_HAS_WRITE_CONTACTS_DECLARATION; + +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(GosPackageState.FLAG_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 DFLAG_HAS_READ_CONTACTS_DECLARATION; + case Manifest.permission.WRITE_CONTACTS: + return DFLAG_HAS_WRITE_CONTACTS_DECLARATION; + case Manifest.permission.GET_ACCOUNTS: + return DFLAG_HAS_GET_ACCOUNTS_DECLARATION; + default: + return 0; + } + } + + private static int getSpoofableAppOpPermissionDflag(int op) { + switch (op) { + case AppOpsManager.OP_READ_CONTACTS: + return DFLAG_HAS_READ_CONTACTS_DECLARATION; + case AppOpsManager.OP_WRITE_CONTACTS: + return DFLAG_HAS_WRITE_CONTACTS_DECLARATION; + case AppOpsManager.OP_GET_ACCOUNTS: + return DFLAG_HAS_WRITE_CONTACTS_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 000000000000..a326ad7bd101 --- /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/RedirectedContentProvider.java b/core/java/com/android/internal/app/RedirectedContentProvider.java new file mode 100644 index 000000000000..91d2a6121f6c --- /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 000000000000..1445eb271b91 --- /dev/null +++ b/core/java/com/android/internal/app/StorageScopesAppHooks.java @@ -0,0 +1,207 @@ +/* + * 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.net.Uri; +import android.os.Environment; +import android.provider.MediaStore; +import android.provider.Settings; + +import static android.content.pm.GosPackageState.*; + +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(FLAG_STORAGE_SCOPES_ENABLED)) { + gosPsDerivedFlags = ps.derivedFlags; + isEnabled = true; + } + } + + public static boolean shouldSkipPermissionCheckSpoof(int gosPsDflags, int permDerivedFlag) { + if ((gosPsDflags & DFLAG_HAS_READ_MEDIA_VISUAL_USER_SELECTED_DECLARATION) != 0) { + switch (permDerivedFlag) { + case DFLAG_HAS_READ_MEDIA_AUDIO_DECLARATION: + case DFLAG_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 DFLAG_HAS_READ_EXTERNAL_STORAGE_DECLARATION; + + case Manifest.permission.WRITE_EXTERNAL_STORAGE: + return DFLAG_HAS_WRITE_EXTERNAL_STORAGE_DECLARATION; + + case Manifest.permission.ACCESS_MEDIA_LOCATION: + return DFLAG_HAS_ACCESS_MEDIA_LOCATION_DECLARATION; + + case Manifest.permission.READ_MEDIA_AUDIO: + return DFLAG_HAS_READ_MEDIA_AUDIO_DECLARATION; + + case Manifest.permission.READ_MEDIA_IMAGES: + return DFLAG_HAS_READ_MEDIA_IMAGES_DECLARATION; + + case Manifest.permission.READ_MEDIA_VIDEO: + return DFLAG_HAS_READ_MEDIA_VIDEO_DECLARATION; + + case Manifest.permission.READ_MEDIA_VISUAL_USER_SELECTED: + return DFLAG_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 DFLAG_HAS_READ_EXTERNAL_STORAGE_DECLARATION; + + case AppOpsManager.OP_WRITE_EXTERNAL_STORAGE: + return DFLAG_HAS_WRITE_EXTERNAL_STORAGE_DECLARATION; + + case AppOpsManager.OP_READ_MEDIA_AUDIO: + return DFLAG_HAS_READ_MEDIA_AUDIO_DECLARATION; + + case AppOpsManager.OP_READ_MEDIA_IMAGES: + return DFLAG_HAS_READ_MEDIA_IMAGES_DECLARATION; + + case AppOpsManager.OP_READ_MEDIA_VIDEO: + return DFLAG_HAS_READ_MEDIA_VIDEO_DECLARATION; + + case AppOpsManager.OP_MANAGE_EXTERNAL_STORAGE: + return DFLAG_HAS_MANAGE_EXTERNAL_STORAGE_DECLARATION; + + case AppOpsManager.OP_MANAGE_MEDIA: + return DFLAG_HAS_MANAGE_MEDIA_DECLARATION; + + case AppOpsManager.OP_ACCESS_MEDIA_LOCATION: + return DFLAG_HAS_ACCESS_MEDIA_LOCATION_DECLARATION; + + case AppOpsManager.OP_READ_MEDIA_VISUAL_USER_SELECTED: + return DFLAG_HAS_READ_MEDIA_VISUAL_USER_SELECTED_DECLARATION; + + default: + return 0; + } + } + + private StorageScopesAppHooks() {} +} 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 000000000000..0fd6cd98f76a --- /dev/null +++ b/core/java/com/android/internal/gmscompat/BinderGca2Gms.java @@ -0,0 +1,118 @@ +package com.android.internal.gmscompat; + +import android.app.Activity; +import android.app.compat.gms.GmsCompat; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Intent; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.net.Uri; +import android.util.ArraySet; +import android.util.Log; + +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 { + updatePhenotype(phenotypeDb, GmsHooks.config()); + } + + // Gservices flags are hosted by GservicesProvider ContentProvider in + // "Google Services Framework" app. + // Its clients register change listeners via + // BroadcastReceivers (com.google.gservices.intent.action.GSERVICES_CHANGED) and + // ContentResolver#registerContentObserver(). + // Code below performs a delete of a non-existing key (key is chosen randomly). + // GSF will notify all of its listeners after this operation, despite database remaining + // unchanged. There's no other simple way to achieve the same effect (AFAIK). + // + // Additional values from GmsCompatConfig will be added only inside client's processes, + // database inside GSF remains unchanged. + + ContentResolver cr = GmsCompat.appContext().getContentResolver(); + ContentValues cv = new ContentValues(); + cv.put("iquee6jo8ooquoomaeraip7gah4shee8phiet0Ahng0yeipei3", (String) null); + + Uri gservicesUri = Uri.parse("content://" + GmsInfo.PACKAGE_GSF + ".gservices/override"); + cr.update(gservicesUri, cv, null, null); + } + + private static void updatePhenotype(SQLiteOpenHelper phenotypeDb, GmsCompatConfig newConfig) { + SQLiteDatabase db = phenotypeDb.getReadableDatabase(); + String[] columns = { "androidPackageName" }; + String selection = "packageName = ?"; + + ArraySet configPackageNames = new ArraySet<>(newConfig.flags.keySet()); + configPackageNames.addAll(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 000000000000..6b88d67855ed --- /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/GmcMediaProjectionService.java b/core/java/com/android/internal/gmscompat/GmcMediaProjectionService.java new file mode 100644 index 000000000000..f71ecb2e88a1 --- /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 000000000000..ada29f44dbae --- /dev/null +++ b/core/java/com/android/internal/gmscompat/GmsCompatApp.java @@ -0,0 +1,298 @@ +/* + * 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.Uri; +import android.os.Bundle; +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.dynamite.server.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 dynamiteFileProxyService; + + 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 Dynamite 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); + dynamiteFileProxyService = fileProxyService; + + ctx.getMainThreadHandler().postDelayed(GmsCompatApp::maybeShowContactsSyncNotification, 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; + } + + if (GmsCompat.isGmsCore()) { + throw new IllegalStateException(); + } + + 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; + } + + 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); + } + + 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 000000000000..23491f79e42c --- /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 000000000000..c46a7b55713e --- /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/GmsCoreConst.java b/core/java/com/android/internal/gmscompat/GmsCoreConst.java new file mode 100644 index 000000000000..47f737f8f9d9 --- /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 000000000000..03f50331cc2b --- /dev/null +++ b/core/java/com/android/internal/gmscompat/GmsHooks.java @@ -0,0 +1,745 @@ +/* + * 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.ContentResolver; +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.net.Uri; +import android.os.Build; +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.ClientPriorityManager; +import com.android.internal.gmscompat.client.GmsCompatClientService; +import com.android.internal.gmscompat.flags.GmsFlag; +import com.android.internal.gmscompat.gcarriersettings.GCarrierSettingsApp; +import com.android.internal.gmscompat.gcarriersettings.TestCarrierConfigService; +import com.android.internal.gmscompat.sysservice.GmcPackageManager; +import com.android.internal.gmscompat.util.GmcActivityUtils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +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 = Application.getProcessName(); + + 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); + } + + 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()); + + 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.packageVersion = ai.longVersionCode; + 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: + // requires privileged permissions + case Context.STATS_MANAGER: + 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, + Cursor origCursor) { + String uriString = uri.toString(); + + Consumer> mutator = null; + + if (GmsFlag.GSERVICES_URI.equals(uriString)) { + if (queryArgs == null) { + return null; + } + String[] selectionArgs = queryArgs.getStringArray(ContentResolver.QUERY_ARG_SQL_SELECTION_ARGS); + if (selectionArgs == null) { + return null; + } + + ArrayMap flags = config().gservicesFlags; + if (flags == null) { + return null; + } + + mutator = map -> { + for (GmsFlag f : flags.values()) { + for (String sel : selectionArgs) { + if (f.name.startsWith(sel)) { + f.applyToGservicesMap(map); + break; + } + } + } + }; + } else 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); + + ArrayMap nsFlags = config.flags.get(namespace); + + if (forceDefaultFlagsRegexes == null && nsFlags == 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 (nsFlags != null) { + for (GmsFlag f : nsFlags.values()) { + f.applyToPhenotypeMap(map); + } + } + }; + } + + if (mutator != null) { + return modifyKvCursor(origCursor, mutator); + } + + return null; + } + + private static Cursor modifyKvCursor(Cursor origCursor, Consumer> mutator) { + final int keyIndex = 0; + final int valueIndex = 1; + final int projectionLength = 2; + + String[] 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; + } + + ArrayMap 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; + } + + // SharedPreferencesImpl#getAll + public static void maybeModifySharedPreferencesValues(String name, HashMap map) { + // some PhenotypeFlags are stored in SharedPreferences instead of phenotype.db database + ArrayMap flags = GmsHooks.config().flags.get(name); + if (flags == null) { + return; + } + + for (GmsFlag f : flags.values()) { + f.applyToPhenotypeMap(map); + } + } + + // 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); + } + + // 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); + }); + } + } + + // 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; + } + + ClientPriorityManager.raiseToForeground(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); + + if (Build.isDebuggable()) { + Log.i(TAG, 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 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 000000000000..986e106b3778 --- /dev/null +++ b/core/java/com/android/internal/gmscompat/GmsInfo.java @@ -0,0 +1,34 @@ +/* + * 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_GSF = PackageId.GSF_NAME; // "Google Services Framework" + 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 GSF, GmsCore, 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 000000000000..5919f6ba579c --- /dev/null +++ b/core/java/com/android/internal/gmscompat/IClientOfGmsCore2Gca.aidl @@ -0,0 +1,14 @@ +package com.android.internal.gmscompat; + +import android.os.BinderDef; + +import com.android.internal.gmscompat.dynamite.server.IFileProxyService; + +// calls from clients of GMS Core to GmsCompatApp +interface IClientOfGmsCore2Gca { + @nullable BinderDef maybeGetBinderDef(String callerPkg, int processState, String ifaceName); + + IFileProxyService getDynamiteFileProxyService(); + + oneway void showMissingAppNotification(String pkgName); +} 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 000000000000..455a638ab699 --- /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 000000000000..6fa24261a2d7 --- /dev/null +++ b/core/java/com/android/internal/gmscompat/IGms2Gca.aidl @@ -0,0 +1,46 @@ +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.dynamite.server.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(); + + 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(); +} 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 000000000000..d557ee040aa1 --- /dev/null +++ b/core/java/com/android/internal/gmscompat/PlayStoreHooks.java @@ -0,0 +1,340 @@ +/* + * 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.app.Activity; +import android.app.PendingIntent; +import android.app.compat.gms.GmsCompat; +import android.app.usage.StorageStats; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.IntentSender; +import android.content.pm.GosPackageState; +import android.content.pm.IPackageDataObserver; +import android.content.pm.IPackageDeleteObserver; +import android.content.pm.PackageInstaller; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Bundle; +import android.os.Environment; +import android.os.RemoteException; +import android.os.storage.StorageManager; +import android.provider.Settings; +import android.util.Log; + +import com.android.internal.gmscompat.util.GmcActivityUtils; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayDeque; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicLong; +import java.util.function.BiConsumer; + +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; + packageManager = GmsCompat.appContext().getPackageManager(); + } + + // PackageInstaller#createSession + public static void adjustSessionParams(PackageInstaller.SessionParams params) { + String pkg = Objects.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 + } + + // 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... broadcasts) { + Context context = GmsCompat.appContext(); + + // default ClassLoader fails to load the needed class + ClassLoader cl = context.getClassLoader(); + + // Depending on Play Store version, target class can be in packagemonitor or in + // packagemanager package, support both + String[] classNames = { + "com.google.android.finsky.packagemonitor.impl.PackageMonitorReceiverImpl$RegisteredReceiver", + "com.google.android.finsky.packagemanager.impl.PackageMonitorReceiverImpl$RegisteredReceiver", + }; + + for (String className : classNames) { + try { + Class cls = Class.forName(className, true, cl); + + for (String action : broadcasts) { + // don't reuse BroadcastReceiver, it's expected that a new instance is made each time + BroadcastReceiver br = (BroadcastReceiver) cls.newInstance(); + br.onReceive(context, new Intent(action, packageUri(packageName))); + } + } catch (ReflectiveOperationException e) { + Log.d(TAG, "", e); + continue; + } + break; + } + } + + // 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.get(GmsCompat.appContext().getPackageName()); + boolean hasObbAccess = ps != null && ps.hasFlag(GosPackageState.FLAG_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); + } + } + + 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 000000000000..c4ea421bb498 --- /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/ClientPriorityManager.java b/core/java/com/android/internal/gmscompat/client/ClientPriorityManager.java new file mode 100644 index 000000000000..361a396de2da --- /dev/null +++ b/core/java/com/android/internal/gmscompat/client/ClientPriorityManager.java @@ -0,0 +1,121 @@ +/* + * 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.client; + +import android.annotation.Nullable; +import android.app.compat.gms.GmsCompat; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.IBinder; +import android.os.PowerExemptionManager; +import android.util.Log; + +import com.android.internal.gmscompat.GmsInfo; + +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +// See GmsCompatClientService for an explanation +public class ClientPriorityManager implements ServiceConnection, Runnable { + private static final String TAG = "ClientPriorityManager"; + private static final boolean LOGV = false; + + private static final ScheduledExecutorService unbindExecutor = Executors.newSingleThreadScheduledExecutor(); + + private boolean unbound; + + private ClientPriorityManager() {} + + public static void raiseToForeground(String targetPkg, long durationMs, @Nullable String reason, int reasonCode) { + if (durationMs <= 0) { + return; + } + + if (targetPkg.equals(GmsInfo.PACKAGE_GMS_CORE)) { + // always foreground, and doesn't have the GmsCompatClientService + return; + } + + Log.d(TAG, "emulating temporary PowerExemptionManager allowlist for " + targetPkg + + ", duration: " + durationMs + + ", reason: " + reason + + ", reasonCode: " + PowerExemptionManager.reasonCodeToString(reasonCode)); + + raiseToForeground(targetPkg, durationMs); + } + + public static void raiseToForeground(String targetPkg, long durationMs) { + Intent intent = new Intent(); + intent.setClassName(targetPkg, GmsCompatClientService.class.getName()); + + ClientPriorityManager csc = new ClientPriorityManager(); + + if (GmsCompat.appContext().bindService(intent, csc, Context.BIND_AUTO_CREATE)) { + unbindExecutor.schedule(csc, durationMs, TimeUnit.MILLISECONDS); + + if (LOGV) { + Log.d(TAG, "bound to " + targetPkg); + } + } else { + Log.e(TAG, "unable to bind to " + targetPkg, new Exception()); + } + } + + @Override + public void run() { + if (LOGV) { + Log.d(TAG, "timeout expired, unbinding"); + } + unbind(); + } + + private void unbind() { + synchronized (this) { + if (!unbound) { + GmsCompat.appContext().unbindService(this); + unbound = true; + } + } + } + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + if (LOGV) { + Log.d(TAG, "onServiceConnected " + name); + } + } + + @Override + public void onServiceDisconnected(ComponentName name) { + Log.e(TAG, "onServiceDisconnected " + name); + } + + @Override + public void onBindingDied(ComponentName name) { + Log.d(TAG, "onBindingDied " + name); + unbind(); + } + + @Override + public void onNullBinding(ComponentName name) { + Log.e(TAG, "onNullBinding " + name); + unbind(); + } +} 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 000000000000..0bb4c73b6399 --- /dev/null +++ b/core/java/com/android/internal/gmscompat/client/GmsCompatClientService.java @@ -0,0 +1,41 @@ +/* + * 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.client; + +import android.app.Service; +import android.content.Intent; +import android.os.Binder; +import android.os.IBinder; + +/* +The sole purpose of this service is to provide a way to move a client of GMS Core +out of the background state by binding to it from a foreground process. + +Privileged GMS Core achieves this by sending a privileged broadcast with +BroadcastOptions#setTemporaryAppAllowlist() option set. + +A declaration of this service is added to all clients of GMS Core during package parsing, +see GmsClientHooks#maybeAddServiceDuringParsing() + */ +public class GmsCompatClientService extends Service { + private static final Binder dummyBinder = new Binder(); + + @Override + public IBinder onBind(Intent intent) { + return dummyBinder; + } +} 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 000000000000..099adab2357e --- /dev/null +++ b/core/java/com/android/internal/gmscompat/dynamite/GmsDynamiteClientHooks.java @@ -0,0 +1,203 @@ +/* + * 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.dynamite; + +import android.app.compat.gms.GmsCompat; +import android.content.Context; +import android.content.res.ApkAssets; +import android.content.res.loader.AssetsProvider; +import android.os.Environment; +import android.os.IBinder; +import android.os.ParcelFileDescriptor; +import android.os.RemoteException; +import android.util.ArrayMap; +import android.util.Log; + +import com.android.internal.gmscompat.GmsCompatApp; +import com.android.internal.gmscompat.GmsInfo; +import com.android.internal.gmscompat.dynamite.server.IFileProxyService; + +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; + private static String gmsCoreDataPrefix; + private static ArrayMap pfdCache; + private static IFileProxyService fileProxyService; + + 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; + } + // faster than ctx.createPackageContext().createDeviceProtectedStorageContext().getDataDir() + int userId = GmsCompat.appContext().getUserId(); + String deDataDirectory = Environment.getDataUserDeDirectory(null, userId).getPath(); + gmsCoreDataPrefix = deDataDirectory + '/' + GmsInfo.PACKAGE_GMS_CORE + '/'; + pfdCache = new ArrayMap<>(20); + + try { + IFileProxyService service = GmsCompatApp.iClientOfGmsCore2Gca().getDynamiteFileProxyService(); + service.asBinder().linkToDeath(() -> { + // When GMS Core gets terminated (including package updates and crashes), + // processes of Dynamite clients get terminated too (same behavior on stock OS, + // likely to avoid hard-to-resolve situation when client starts to load + // modules from one GMS Core version and then GMS Core gets updated before the rest of the + // modules are loaded). + // This ensures that pfdCache never returns stale file descriptors, + // because there's only two types of Dynamite modules: + // - "core", included with the GMS Core package and always extracted + // to the app_chimera/m directory, may have the same name on different GMS Core versions + // - on-demand, downloaded on first use, each version has a unique file name + + Log.d(TAG, "FileProxyService died"); + // isn't reached in practice, at least on current versions (2022 Q1) + System.exit(0); + }, 0); + + fileProxyService = service; + } catch (Throwable e) { + // linkToDeath() failed, + // most likely because GMS Core crashed very shortly before getDynamiteFileProxyService(), + // which should be very rare in practice. + // Waiting for GMS Core to respawn is hard to do correctly, not worth the complexity increase + Log.e(TAG, "unable to obtain the FileProxyService", e); + System.exit(1); + } + + File.lastModifiedHook = GmsDynamiteClientHooks::getFileLastModified; + DelegateLastClassLoader.modifyClassLoaderPathHook = GmsDynamiteClientHooks::maybeModifyClassLoaderPath; + enabled = true; + } + } + + // ApkAssets#loadFromPath(String, int, AssetsProvider) + public static ApkAssets loadAssetsFromPath(String path, int flags, AssetsProvider assets) throws IOException { + if (!path.startsWith(gmsCoreDataPrefix)) { + return null; + } + FileDescriptor fd = modulePathToFd(path); + // no need to dup the fd, ApkAssets does it itself + return ApkAssets.loadFromFd(fd, path, flags, assets); + } + + // To fix false-positive "Module APK has been modified" check + // File#lastModified() + public static long getFileLastModified(File file) { + final String path = file.getPath(); + + if (enabled && path.startsWith(gmsCoreDataPrefix)) { + String fdPath = "/proc/self/fd/" + modulePathToFd(path).getInt$(); + return new File(fdPath).lastModified(); + } + return 0L; + } + + // Replaces file paths of Dynamite modules with "/proc/self/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 (!pathPart.startsWith(gmsCoreDataPrefix)) { + 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; + } + String fdFilePath = "/gmscompat_fd_" + modulePathToFd(filePath).getInt$(); + + pathParts[i] = nativeLibsPath ? + fdFilePath + zipFileSeparator + nativeLibRelPath : + fdFilePath; + + modified = true; + } + if (!modified) { + return path; + } + return String.join(File.pathSeparator, pathParts); + } + + // Returned file descriptor should never be closed, because it may be dup()-ed at any time by the native code + private static FileDescriptor modulePathToFd(String path) { + if (DEBUG) { + new Exception("path " + path).printStackTrace(); + } + 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 = fileProxyService.openFile(path); + if (pfd == null) { + throw new IllegalStateException("unable to open " + path); + } + // 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 GmsDynamiteClientHooks() {} +} diff --git a/core/java/com/android/internal/gmscompat/dynamite/server/FileProxyService.java b/core/java/com/android/internal/gmscompat/dynamite/server/FileProxyService.java new file mode 100644 index 000000000000..d626f4de2934 --- /dev/null +++ b/core/java/com/android/internal/gmscompat/dynamite/server/FileProxyService.java @@ -0,0 +1,99 @@ +/* + * 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.dynamite.server; + +import android.content.Context; +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/DynamiteServer"; + private static final String CHIMERA_REL_PATH = "app_chimera/m/"; + + private final String chimeraRoot; + + public FileProxyService(Context context) { + File deDataRoot = context.createDeviceProtectedStorageContext().getDataDir(); + chimeraRoot = deDataRoot.getPath() + "/" + CHIMERA_REL_PATH; + } + + @Override + public ParcelFileDescriptor openFile(String rawPath) { + try { + String path = sanitizeModulePath(rawPath); + if (path != null) { + FileDescriptor fd = Os.open(path, OsConstants.O_RDONLY | OsConstants.O_CLOEXEC, 0); +// Log.d(TAG, "Opened " + rawPath + " for remote, fd " + fd.getInt$()); + return new ParcelFileDescriptor(fd); + } + } catch (IOException | ErrnoException e) { + Log.d(TAG, "failed security check", e); + } catch (Throwable t) { + Log.d(TAG, "unexpected error", t); + } + // don't forward exceptions to the untrusted caller to minimize the information leaks + return null; + } + + private String sanitizeModulePath(String rawPath) throws IOException, ErrnoException { + // Normalize path for security checks + String path = new File(rawPath).getCanonicalPath(); + + // Modules can only be in DE Chimera storage + if (!path.startsWith(chimeraRoot)) { + Log.d(TAG, "Path " + rawPath + " is not in " + chimeraRoot); + return null; + } + + if (!path.endsWith(".apk")) { + Log.d(TAG, "Path " + rawPath + " is not an APK file"); + 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 = chimeraRoot.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/dynamite/server/IFileProxyService.aidl b/core/java/com/android/internal/gmscompat/dynamite/server/IFileProxyService.aidl new file mode 100644 index 000000000000..678d21f8fe4f --- /dev/null +++ b/core/java/com/android/internal/gmscompat/dynamite/server/IFileProxyService.aidl @@ -0,0 +1,5 @@ +package com.android.internal.gmscompat.dynamite.server; + +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 000000000000..b1e0be535ec3 --- /dev/null +++ b/core/java/com/android/internal/gmscompat/flags/GmsFlag.java @@ -0,0 +1,300 @@ +/* + * 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; +import java.util.function.Supplier; + +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 @Nullable Supplier valueSupplier; + + 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"; + + public static final String GSERVICES_URI = "content://" + + GmsInfo.PACKAGE_GSF + '.' + NAMESPACE_GSERVICES + "/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; + } + + if (valueSupplier != null) { + Object val = valueSupplier.get(); + + String s; + if (type == TYPE_BYTES) { + s = Base64.encodeToString((byte[]) val, PHENOTYPE_BASE64_FLAGS); + } else { + s = val.toString(); + } + + map.put(name, s); + 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 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; + } + if (valueSupplier != null) { + boolean v = ((Boolean) valueSupplier.get()).booleanValue(); + return v ? 1 : 0; + } + return boolArg ? 1 : 0; + } + + public long intVal(long orig) { + if (type != TYPE_INT) { + logTypeMismatch(); + return orig; + } + if (valueSupplier != null) { + return ((Long) valueSupplier.get()).longValue(); + } + return integerArg; + } + + public double floatVal(double orig) { + if (type != TYPE_FLOAT) { + logTypeMismatch(); + return orig; + } + if (valueSupplier != null) { + return ((Double) valueSupplier.get()).doubleValue(); + } + return floatArg; + } + + public void maybeOverrideString(Map map) { + if (type != TYPE_STRING) { + logTypeMismatch(); + return; + } + if (valueSupplier != null) { + map.put(name, valueSupplier.get()); + 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; + } + if (valueSupplier != null) { + return (byte[]) valueSupplier.get(); + } + 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/gcarriersettings/GCarrierSettingsApp.java b/core/java/com/android/internal/gmscompat/gcarriersettings/GCarrierSettingsApp.java new file mode 100644 index 000000000000..9c69cbaf1d64 --- /dev/null +++ b/core/java/com/android/internal/gmscompat/gcarriersettings/GCarrierSettingsApp.java @@ -0,0 +1,83 @@ +package com.android.internal.gmscompat.gcarriersettings; + +import android.content.Context; +import android.service.carrier.CarrierIdentifier; +import android.telephony.TelephonyManager; + +import com.android.internal.util.PackageSpec; + +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 PackageSpec getPackageSpec() { + return new PackageSpec(PKG_NAME, 37L, + new String[] { "c00409b6524658c2e8eb48975a5952959ea3707dd57bc50fd74d6249262f0e82" }); + } + + 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 000000000000..c28bde2351c5 --- /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 000000000000..7b4431232a70 --- /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 000000000000..67d43895a00f --- /dev/null +++ b/core/java/com/android/internal/gmscompat/sysservice/GmcPackageManager.java @@ -0,0 +1,589 @@ +/* + * 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.Context; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.IPackageDataObserver; +import android.content.pm.IPackageDeleteObserver; +import android.content.pm.IPackageManager; +import android.content.pm.InstallSourceInfo; +import android.content.pm.PackageInfo; +import android.content.pm.SharedLibraryInfo; +import android.content.pm.VersionedPackage; +import android.ext.PackageId; +import android.os.Process; +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 com.android.internal.util.GoogleEuicc; + +import java.util.ArrayList; +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; + hiddenPkgs.add(GoogleEuicc.LPA_PKG_NAME); + } + } + + 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 + removePseudoDisabledPackage(packageName); + GmsCompat.appContext().getMainThreadHandler().post(() -> + PlayStoreHooks.updatePackageState(packageName, Intent.ACTION_PACKAGE_REMOVED)); + return; + } + } + PlayStoreHooks.setApplicationEnabledSetting(packageName, newState); + return; + } + + super.setApplicationEnabledSetting(packageName, newState, flags); + } + + @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; + } + + if (PackageId.ANDROID_AUTO_NAME.equals(packageName)) { + // Android Auto needs to be exempted from updates via Play Store to prevent breaking the + // compatibility layer support for Android Auto. + // + // Play Store respects the value of InstallSourceInfo#getUpdateOwnerPackageName(): + // packages that have non-Play Store update owners are not updated by Play Store + String updateOwnerPackage = PackageUtils.getFirstPartyAppSourcePackageName(GmsCompat.appContext()); + res = new InstallSourceInfo( + res.getInitiatingPackageName(), + res.getInitiatingPackageSigningInfo(), + res.getOriginatingPackageName(), + res.getInstallingPackageName(), + updateOwnerPackage, + res.getPackageSource() + ); + } + + 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.packageName = pkgName; + pi.applicationInfo.packageName = pkgName; + pi.applicationInfo.enabled = false; + 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.packageName = pkgName; + ai.enabled = false; + ai.longVersionCode = Integer.MAX_VALUE; + ai.versionCode = Integer.MAX_VALUE; + return ai; + } + + 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"); + } + + 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); + } + } + + private 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_GSF: + 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); + } + } + +} 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 000000000000..0fc7d81ca810 --- /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 000000000000..2b1a8db750b0 --- /dev/null +++ b/core/java/com/android/internal/gmscompat/sysservice/GmcUserManager.java @@ -0,0 +1,190 @@ +/* + * 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 excludePartial, boolean excludeDying, boolean excludePreCreated) { + 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 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 000000000000..f7424df0db93 --- /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 000000000000..627721946bab --- /dev/null +++ b/core/java/com/android/internal/gmscompat/util/GmcActivityUtils.java @@ -0,0 +1,142 @@ +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; + } + + 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 fef5e83cecca..204c1ed8843a 100644 --- a/core/java/com/android/internal/notification/SystemNotificationChannels.java +++ b/core/java/com/android/internal/notification/SystemNotificationChannels.java @@ -66,6 +66,7 @@ public class SystemNotificationChannels { @Deprecated public static String SYSTEM_CHANGES_DEPRECATED = "SYSTEM_CHANGES"; public static String SYSTEM_CHANGES = "SYSTEM_CHANGES_ALERTS"; public static String DO_NOT_DISTURB = "DO_NOT_DISTURB"; + public static String OTHER_USERS = "OTHER_USERS"; public static String ACCESSIBILITY_MAGNIFICATION = "ACCESSIBILITY_MAGNIFICATION"; public static String ACCESSIBILITY_SECURITY_POLICY = "ACCESSIBILITY_SECURITY_POLICY"; public static String ABUSIVE_BACKGROUND_APPS = "ABUSIVE_BACKGROUND_APPS"; @@ -198,6 +199,13 @@ public static void createAll(Context context) { NotificationManager.IMPORTANCE_LOW); channelsList.add(dndChanges); + 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), @@ -217,6 +225,8 @@ public static void createAll(Context context) { NotificationManager.IMPORTANCE_LOW); channelsList.add(abusiveBackgroundAppsChannel); + extraChannels(context, channelsList); + nm.createNotificationChannels(channelsList); } @@ -252,4 +262,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/BinderfsStatsReader.java b/core/java/com/android/internal/os/BinderfsStatsReader.java new file mode 100644 index 000000000000..9cc4a35b5c65 --- /dev/null +++ b/core/java/com/android/internal/os/BinderfsStatsReader.java @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2023 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.os; + +import com.android.internal.util.ProcFileReader; + +import java.io.FileInputStream; +import java.io.IOException; +import java.util.function.BiConsumer; +import java.util.function.Consumer; +import java.util.function.Predicate; + +/** + * Reads and parses {@code binder_logs/stats} file in the {@code binderfs} filesystem. + * Reuse procFileReader as the contents are generated by Linux kernel in the same way. + * + * A typical example of binderfs stats log + * + * binder stats: + * BC_TRANSACTION: 378004 + * BC_REPLY: 268352 + * BC_FREE_BUFFER: 665854 + * ... + * proc 12645 + * context binder + * threads: 12 + * requested threads: 0+5/15 + * ready threads 0 + * free async space 520192 + * ... + */ +public class BinderfsStatsReader { + private final String mPath; + + public BinderfsStatsReader() { + mPath = "/dev/binderfs/binder_logs/stats"; + } + + public BinderfsStatsReader(String path) { + mPath = path; + } + + /** + * Read binderfs stats and call the consumer(pid, free) function for each valid process + * + * @param predicate Test if the pid is valid. + * @param biConsumer Callback function for each valid pid and its free async space + * @param consumer The error function to deal with exceptions + */ + public void handleFreeAsyncSpace(Predicate predicate, + BiConsumer biConsumer, Consumer consumer) { + try (ProcFileReader mReader = new ProcFileReader(new FileInputStream(mPath))) { + while (mReader.hasMoreData()) { + // find the next process + if (!mReader.nextString().equals("proc")) { + mReader.finishLine(); + continue; + } + + // read pid + int pid = mReader.nextInt(); + mReader.finishLine(); + + // check if we have interest in this process + if (!predicate.test(pid)) { + continue; + } + + // read free async space + mReader.finishLine(); // context binder + mReader.finishLine(); // threads: + mReader.finishLine(); // requested threads: + mReader.finishLine(); // ready threads + if (!mReader.nextString().equals("free")) { + mReader.finishLine(); + continue; + } + if (!mReader.nextString().equals("async")) { + mReader.finishLine(); + continue; + } + if (!mReader.nextString().equals("space")) { + mReader.finishLine(); + continue; + } + int free = mReader.nextInt(); + mReader.finishLine(); + biConsumer.accept(pid, free); + } + } catch (IOException | NumberFormatException e) { + consumer.accept(e); + } + } +} diff --git a/core/java/com/android/internal/os/ExecInit.java b/core/java/com/android/internal/os/ExecInit.java new file mode 100644 index 000000000000..a24bbf94f698 --- /dev/null +++ b/core/java/com/android/internal/os/ExecInit.java @@ -0,0 +1,145 @@ +package com.android.internal.os; + +import android.os.Trace; +import android.system.ErrnoException; +import android.system.Os; +import android.system.OsConstants; +import android.util.Slog; +import android.util.TimingsTraceLog; +import dalvik.system.VMRuntime; + +/** + * Startup class for the process. + * @hide + */ +public class ExecInit { + /** + * Class not instantiable. + */ + private ExecInit() { + } + + /** + * The main function called when starting a runtime application. + * + * The first argument is the target SDK version for the app. + * + * The remaining arguments are passed to the runtime. + * + * @param args The command-line arguments. + */ + public static void main(String[] args) { + // Parse our mandatory argument. + int targetSdkVersion = Integer.parseInt(args[0], 10); + + // Parse the runtime_flags. + int runtimeFlags = Integer.parseInt(args[1], 10); + + // Mimic system Zygote preloading. + ZygoteInit.preload(new TimingsTraceLog("ExecInitTiming", + Trace.TRACE_TAG_DALVIK), false); + + // Launch the application. + String[] runtimeArgs = new String[args.length - 2]; + System.arraycopy(args, 2, runtimeArgs, 0, runtimeArgs.length); + Runnable r = execInit(targetSdkVersion, runtimeArgs); + + Zygote.nativeHandleRuntimeFlags(runtimeFlags); + + r.run(); + } + + /** + * Executes a runtime application with exec-based spawning. + * This method never returns. + * + * @param niceName The nice name for the application, or null if none. + * @param targetSdkVersion The target SDK version for the app. + * @param args Arguments for {@link RuntimeInit#main}. + */ + public static void execApplication(String niceName, int targetSdkVersion, + String instructionSet, int runtimeFlags, String[] args) { + int niceArgs = niceName == null ? 0 : 1; + int baseArgs = 6 + niceArgs; + String[] argv = new String[baseArgs + args.length]; + if (VMRuntime.is64BitInstructionSet(instructionSet)) { + argv[0] = "/system/bin/app_process64"; + } else { + argv[0] = "/system/bin/app_process32"; + } + argv[1] = "/system/bin"; + argv[2] = "--application"; + if (niceName != null) { + argv[3] = "--nice-name=" + niceName; + } + argv[3 + niceArgs] = "com.android.internal.os.ExecInit"; + argv[4 + niceArgs] = Integer.toString(targetSdkVersion); + argv[5 + niceArgs] = Integer.toString(runtimeFlags); + System.arraycopy(args, 0, argv, baseArgs, args.length); + + WrapperInit.preserveCapabilities(); + try { + if ((runtimeFlags & Zygote.DISABLE_HARDENED_MALLOC) != 0) { + // checked by bionic during early init + Os.setenv("DISABLE_HARDENED_MALLOC", "1", true); + } + + if ((runtimeFlags & Zygote.ENABLE_COMPAT_VA_39_BIT) != 0) { + final int FLAG_COMPAT_VA_39_BIT = 1 << 30; + + int errno = Zygote.execveatWrapper(-1, argv[0], argv, FLAG_COMPAT_VA_39_BIT); + + if (errno == OsConstants.EINVAL) { + // kernel doesn't support FLAG_COMPAT_VA_39_BIT, or a different error that will + // be thrown by execv() anyway + Os.execv(argv[0], argv); + } else { + throw new ErrnoException("execveat", errno); + } + } else { + Os.execv(argv[0], argv); + } + } catch (ErrnoException e) { + throw new RuntimeException(e); + } + } + + public static boolean isExecSpawned; + + /** + * The main function called when an application is started with exec-based spawning. + * + * When the app starts, the runtime starts {@link RuntimeInit#main} + * which calls {@link main} which then calls this method. + * So we don't need to call commonInit() here. + * + * @param targetSdkVersion target SDK version + * @param argv arg strings + */ + private static Runnable execInit(int targetSdkVersion, String[] argv) { + if (RuntimeInit.DEBUG) { + Slog.d(RuntimeInit.TAG, "RuntimeInit: Starting application from exec"); + } + + isExecSpawned = true; + + // Check whether the first argument is a "-cp" in argv, and assume the next argument is the + // classpath. If found, create a PathClassLoader and use it for applicationInit. + ClassLoader classLoader = null; + if (argv != null && argv.length > 2 && argv[0].equals("-cp")) { + classLoader = ZygoteInit.createPathClassLoader(argv[1], targetSdkVersion); + + // Install this classloader as the context classloader, too. + Thread.currentThread().setContextClassLoader(classLoader); + + // Remove the classpath from the arguments. + String removedArgs[] = new String[argv.length - 2]; + System.arraycopy(argv, 2, removedArgs, 0, argv.length - 2); + argv = removedArgs; + } + + // Perform the same initialization that would happen after the Zygote forks. + Zygote.nativePreApplicationInit(); + return RuntimeInit.applicationInit(targetSdkVersion, /*disabledCompatChanges*/ null, argv, classLoader); + } +} 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 000000000000..ca162045f741 --- /dev/null +++ b/core/java/com/android/internal/os/SELinuxFlags.java @@ -0,0 +1,150 @@ +package com.android.internal.os; + +import android.annotation.Nullable; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.GosPackageStateBase; +import android.ext.BrowserUtils; +import android.ext.settings.app.AswDenyNativeDebug; +import android.ext.settings.app.AswRestrictMemoryDynCodeExec; +import android.ext.settings.app.AswRestrictStorageDynCodeExec; +import android.ext.settings.app.AswRestrictWebViewDynamicCodeExecution; +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, + @Nullable GosPackageStateBase callerPs) { + if (Build.IS_EMULATOR) { + if (shouldSkipOnEmulator()) { + return 0L; + } + } + + long res = ALL_RESTRICTIONS; + + if (!AswRestrictWebViewDynamicCodeExecution.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, + @Nullable GosPackageStateBase ps, boolean isIsolatedProcess) { + if (Build.IS_EMULATOR) { + if (shouldSkipOnEmulator()) { + return 0L; + } + } + + long res = ALL_RESTRICTIONS; + + if (!AswDenyNativeDebug.I.get(ctx, userId, appInfo, ps)) { + res &= ~DENY_PROCESS_PTRACE; + } + + if (!AswRestrictMemoryDynCodeExec.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 (!AswRestrictStorageDynCodeExec.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 skipOnEmulator; + + // needed to prevent breaking emulator builds that don't have the necessary kernel changes + private static boolean shouldSkipOnEmulator() { + if (!Build.IS_EMULATOR) { + return false; + } + + Boolean skip = skipOnEmulator; + if (skip == null) { + var f = new File(getSelfProcAttrPath()); + skip = !f.exists(); + skipOnEmulator = skip; + } + + return skip; + } +} diff --git a/core/java/com/android/internal/os/WrapperInit.java b/core/java/com/android/internal/os/WrapperInit.java index 6860759eea8a..a2eef62f80be 100644 --- a/core/java/com/android/internal/os/WrapperInit.java +++ b/core/java/com/android/internal/os/WrapperInit.java @@ -186,7 +186,7 @@ private static Runnable wrapperInit(int targetSdkVersion, String[] argv) { * This is acceptable here as failure will leave the wrapped app with strictly less * capabilities, which may make it crash, but not exceed its allowances. */ - private static void preserveCapabilities() { + public static void preserveCapabilities() { StructCapUserHeader header = new StructCapUserHeader( OsConstants._LINUX_CAPABILITY_VERSION_3, 0); StructCapUserData[] data; diff --git a/core/java/com/android/internal/os/Zygote.java b/core/java/com/android/internal/os/Zygote.java index 965277c4635e..8fb7f1b6d88b 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,6 +41,7 @@ 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; @@ -201,6 +206,15 @@ public final class Zygote { */ public static final int DEBUG_ENABLE_PTRACE = 1 << 25; + public static final int FORCIBLY_ENABLE_MEMORY_TAGGING = 1 << 28; + public static final int DISABLE_HARDENED_MALLOC = 1 << 29; + public static final int ENABLE_COMPAT_VA_39_BIT = 1 << 30; + + // make sure to update isSimpleForkCommand() in core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp + // when adding new flags that depend on exec spawning + public static final int RUNTIME_FLAGS_DEPENDENT_ON_EXEC_SPAWNING = DISABLE_HARDENED_MALLOC | ENABLE_COMPAT_VA_39_BIT; + public static final int CUSTOM_RUNTIME_FLAGS = DISABLE_HARDENED_MALLOC | ENABLE_COMPAT_VA_39_BIT | FORCIBLY_ENABLE_MEMORY_TAGGING; + /** No external storage should be mounted. */ public static final int MOUNT_EXTERNAL_NONE = IVold.REMOUNT_MODE_NONE; /** Default external storage should be mounted. */ @@ -361,14 +375,14 @@ static int forkAndSpecialize(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 bindMountAppDataDirs, boolean bindMountAppStorageDirs, ZygoteExtraArgs extraArgs) { ZygoteHooks.preFork(); int pid = nativeForkAndSpecialize( uid, gid, gids, runtimeFlags, rlimits, mountExternal, seInfo, niceName, fdsToClose, fdsToIgnore, startChildZygote, instructionSet, appDataDir, isTopApp, pkgDataInfoList, allowlistedDataInfoList, bindMountAppDataDirs, - bindMountAppStorageDirs); + bindMountAppStorageDirs, extraArgs.makeJniLongArray()); if (pid == 0) { // Note that this event ends at the end of handleChildProc, Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "PostFork"); @@ -391,7 +405,7 @@ private static native int nativeForkAndSpecialize(int uid, int gid, int[] gids, int[] fdsToClose, int[] fdsToIgnore, boolean startChildZygote, String instructionSet, String appDataDir, boolean isTopApp, String[] pkgDataInfoList, String[] allowlistedDataInfoList, boolean bindMountAppDataDirs, - boolean bindMountAppStorageDirs); + boolean bindMountAppStorageDirs, long[] extraLongArgs); /** * Specialize an unspecialized app process. The current VM must have been started @@ -426,11 +440,11 @@ private static void specializeAppProcess(int uid, int gid, int[] gids, int runti 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 bindMountAppDataDirs, boolean bindMountAppStorageDirs, ZygoteExtraArgs extraArgs) { nativeSpecializeAppProcess(uid, gid, gids, runtimeFlags, rlimits, mountExternal, seInfo, niceName, startChildZygote, instructionSet, appDataDir, isTopApp, pkgDataInfoList, allowlistedDataInfoList, - bindMountAppDataDirs, bindMountAppStorageDirs); + bindMountAppDataDirs, bindMountAppStorageDirs, extraArgs.makeJniLongArray()); // Note that this event ends at the end of handleChildProc. Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "PostFork"); @@ -455,7 +469,7 @@ private static native void nativeSpecializeAppProcess(int uid, int gid, int[] gi 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 bindMountAppDataDirs, boolean bindMountAppStorageDirs, long[] extraLongArgs); /** * Called to do any initialization before starting an application. @@ -866,7 +880,8 @@ private static Runnable childMain(@Nullable ZygoteCommandBuffer argBuffer, args.mSeInfo, args.mNiceName, args.mStartChildZygote, args.mInstructionSet, args.mAppDataDir, args.mIsTopApp, args.mPkgDataInfoList, args.mAllowlistedDataInfoList, - args.mBindMountAppDataDirs, args.mBindMountAppStorageDirs); + args.mBindMountAppDataDirs, args.mBindMountAppStorageDirs, + args.mExtraArgs); Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER); @@ -1151,6 +1166,7 @@ private static void callPostForkSystemServerHooks(int runtimeFlags) { @SuppressWarnings("unused") private static void callPostForkChildHooks(int runtimeFlags, boolean isSystemServer, boolean isZygote, String instructionSet) { + runtimeFlags &= ~CUSTOM_RUNTIME_FLAGS; // a warning is printed when an unknown flag is passed ZygoteHooks.postForkChild(runtimeFlags, isSystemServer, isZygote, instructionSet); } @@ -1352,6 +1368,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()) { + level |= FORCIBLY_ENABLE_MEMORY_TAGGING; + // This flag 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) { @@ -1468,6 +1514,10 @@ public static int getMemorySafetyRuntimeFlagsForSecondaryZygote( getMemorySafetyRuntimeFlags( info, processInfo, null /*instructionSet*/, platformCompat); + // Memory tagging can be forcibly enabled only in immediate children of the primary zygote + // (which includes secondary zygotes) + runtimeFlags &= ~FORCIBLY_ENABLE_MEMORY_TAGGING; + // TBI ("fake" pointer tagging) in AppZygote is controlled by a separate compat feature. if ((runtimeFlags & MEMORY_TAG_LEVEL_MASK) == MEMORY_TAG_LEVEL_TBI && isCompatChangeEnabled( @@ -1481,4 +1531,15 @@ && 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); + + public static native int execveatWrapper(int dirFd, String filename, String[] argv, int flags); } diff --git a/core/java/com/android/internal/os/ZygoteArguments.java b/core/java/com/android/internal/os/ZygoteArguments.java index ef8398294c5b..169569ec8f40 100644 --- a/core/java/com/android/internal/os/ZygoteArguments.java +++ b/core/java/com/android/internal/os/ZygoteArguments.java @@ -71,6 +71,11 @@ class ZygoteArguments { */ int mRuntimeFlags; + /** + * From --flat-extra-args + */ + ZygoteExtraArgs mExtraArgs = ZygoteExtraArgs.DEFAULT; + /** * From --mount-external */ @@ -319,6 +324,8 @@ private void parseArgs(ZygoteCommandBuffer args, int argCount) seenRuntimeArgs = true; } else if (arg.startsWith("--runtime-flags=")) { mRuntimeFlags = Integer.parseInt(getAssignmentValue(arg)); + } else if (arg.startsWith(ZygoteExtraArgs.PREFIX)) { + mExtraArgs = ZygoteExtraArgs.parse(getAssignmentValue(arg)); } else if (arg.startsWith("--seinfo=")) { if (mSeInfoSpecified) { throw new IllegalArgumentException( diff --git a/core/java/com/android/internal/os/ZygoteConnection.java b/core/java/com/android/internal/os/ZygoteConnection.java index 993e4e7b4b3d..99131fcc6ce0 100644 --- a/core/java/com/android/internal/os/ZygoteConnection.java +++ b/core/java/com/android/internal/os/ZygoteConnection.java @@ -25,6 +25,7 @@ import android.compat.annotation.UnsupportedAppUsage; import android.content.pm.ApplicationInfo; +import android.ext.settings.ExtSettings; import android.net.Credentials; import android.net.LocalSocket; import android.os.Parcel; @@ -247,8 +248,9 @@ Runnable processCommand(ZygoteServer zygoteServer, boolean multipleOK) { fdsToClose[1] = zygoteFd.getInt$(); } - if (parsedArgs.mInvokeWith != null || parsedArgs.mStartChildZygote - || !multipleOK || peer.getUid() != Process.SYSTEM_UID) { + if (parsedArgs.mInvokeWith != null || ExtSettings.EXEC_SPAWNING.get() || parsedArgs.mStartChildZygote + || !multipleOK || peer.getUid() != Process.SYSTEM_UID + || (parsedArgs.mRuntimeFlags & Zygote.RUNTIME_FLAGS_DEPENDENT_ON_EXEC_SPAWNING) != 0) { // 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, @@ -257,7 +259,7 @@ Runnable processCommand(ZygoteServer zygoteServer, boolean multipleOK) { parsedArgs.mInstructionSet, parsedArgs.mAppDataDir, parsedArgs.mIsTopApp, parsedArgs.mPkgDataInfoList, parsedArgs.mAllowlistedDataInfoList, parsedArgs.mBindMountAppDataDirs, - parsedArgs.mBindMountAppStorageDirs); + parsedArgs.mBindMountAppStorageDirs, parsedArgs.mExtraArgs); try { if (pid == 0) { @@ -535,6 +537,20 @@ private Runnable handleChildProc(ZygoteArguments parsedArgs, throw new IllegalStateException("WrapperInit.execApplication unexpectedly returned"); } else { if (!isZygote) { + final int runtimeFlags = parsedArgs.mRuntimeFlags; + boolean useExecInit = + ((runtimeFlags & Zygote.RUNTIME_FLAGS_DEPENDENT_ON_EXEC_SPAWNING) != 0 + || ExtSettings.EXEC_SPAWNING.get()) + && + (runtimeFlags & ApplicationInfo.FLAG_DEBUGGABLE) == 0; + + if (useExecInit) { + ExecInit.execApplication(parsedArgs.mNiceName, parsedArgs.mTargetSdkVersion, + VMRuntime.getCurrentInstructionSet(), runtimeFlags, parsedArgs.mRemainingArgs); + + // Should not get here. + throw new IllegalStateException("ExecInit.execApplication unexpectedly returned"); + } return ZygoteInit.zygoteInit(parsedArgs.mTargetSdkVersion, parsedArgs.mDisabledCompatChanges, parsedArgs.mRemainingArgs, null /* classLoader */); 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 000000000000..b21444bb2f45 --- /dev/null +++ b/core/java/com/android/internal/os/ZygoteExtraArgs.java @@ -0,0 +1,60 @@ +package com.android.internal.os; + +import android.annotation.Nullable; +import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.GosPackageStateBase; + +// Extra args for: +// - children of main zygote{,64}, including AppZygotes, but excluding WebViewZygote +// - children of WebViewZygote +// +// AppZygote is treated differently from WebViewZygote because the former runs untrusted app code +// (see android.app.ZygotePreload). +public class ZygoteExtraArgs { + public final long selinuxFlags; + + public static final String PREFIX = "--flat-extra-args="; + + public static final ZygoteExtraArgs DEFAULT = new ZygoteExtraArgs(0L); + + public ZygoteExtraArgs(long selinuxFlags) { + this.selinuxFlags = selinuxFlags; + } + + private static final int IDX_SELINUX_FLAGS = 0; + private static final int ARR_LEN = 1; + private static final String SEPARATOR = "\t"; + + public static String createFlat(Context ctx, int userId, ApplicationInfo appInfo, + @Nullable GosPackageStateBase ps, + boolean isIsolatedProcess) { + String[] arr = new String[ARR_LEN]; + arr[IDX_SELINUX_FLAGS] = Long.toHexString( + SELinuxFlags.get(ctx, userId, appInfo, ps, isIsolatedProcess) + ); + return PREFIX + String.join(SEPARATOR, arr); + } + + public static String createFlatForWebviewProcess(Context ctx, int userId, + ApplicationInfo callerAppInfo, @Nullable GosPackageStateBase callerPs) { + String[] arr = new String[ARR_LEN]; + arr[IDX_SELINUX_FLAGS] = Long.toHexString( + SELinuxFlags.getForWebViewProcess(ctx, userId, callerAppInfo, callerPs) + ); + return PREFIX + String.join(SEPARATOR, arr); + } + + static ZygoteExtraArgs parse(String flat) { + String[] arr = flat.split(SEPARATOR); + long selinuxFlags = Long.parseLong(arr[IDX_SELINUX_FLAGS], 16); + return new ZygoteExtraArgs(selinuxFlags); + } + + // keep in sync with ExtraArgs struct in core/jni/com_android_internal_os_Zygote.cpp + public long[] makeJniLongArray() { + long[] res = new long[ARR_LEN]; + res[IDX_SELINUX_FLAGS] = selinuxFlags; + return res; + } +} diff --git a/core/java/com/android/internal/os/ZygoteInit.java b/core/java/com/android/internal/os/ZygoteInit.java index 7f53cb433c98..dffd1daea770 100644 --- a/core/java/com/android/internal/os/ZygoteInit.java +++ b/core/java/com/android/internal/os/ZygoteInit.java @@ -134,38 +134,52 @@ 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"); - 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"); + 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); preloadSharedLibraries(); preloadTextResources(); - // 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); 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."); @@ -173,14 +187,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()"); } @@ -225,7 +239,7 @@ private static void preloadTextResources() { * 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"); @@ -235,15 +249,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) { 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 000000000000..250f64908593 --- /dev/null +++ b/core/java/com/android/internal/util/GoogleCameraUtils.java @@ -0,0 +1,21 @@ +package com.android.internal.util; + +import android.content.Context; +import android.content.res.Resources; + +import com.android.internal.R; + +public class GoogleCameraUtils { + public static final String PACKAGE_NAME = "com.google.android.GoogleCamera"; + + public static final PackageSpec PACKAGE_SPEC = new PackageSpec( + PACKAGE_NAME, 65820000L, new String[] { + "f0fd6c5b410f25cb25c3b53346c8972fae30f8ee7411df910480ad6b2d60db83", + "1975b2f17177bc89a5dff31f9e64a6cae281a53dc1d1d59b1d147fe1c82afa00", + }); + + 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/util/GoogleEuicc.java b/core/java/com/android/internal/util/GoogleEuicc.java new file mode 100644 index 000000000000..530caf276c11 --- /dev/null +++ b/core/java/com/android/internal/util/GoogleEuicc.java @@ -0,0 +1,32 @@ +package com.android.internal.util; + +import android.app.compat.gms.GmsCompat; +import android.os.UserHandle; + +import com.android.internal.gmscompat.GmsInfo; + +public class GoogleEuicc { + // handles firmware updates of embedded secure element that is used for eSIM, NFC, Felica etc + public static final String EUICC_SUPPORT_PIXEL_PKG_NAME = "com.google.euiccpixel"; + public static final String LPA_PKG_NAME = "com.google.android.euicc"; + + public static String[] getLpaDependencies() { + return new String[] { + GmsInfo.PACKAGE_GSF, GmsInfo.PACKAGE_GMS_CORE, GmsInfo.PACKAGE_PLAY_STORE, + }; + } + + public static boolean checkLpaDependencies() { + for (String pkg : getLpaDependencies()) { + try { + if (!GmsCompat.isEnabledFor(pkg, UserHandle.USER_SYSTEM)) { + return false; + } + } catch (Exception e) { + e.printStackTrace(); + return false; + } + } + return true; + } +} diff --git a/core/java/com/android/internal/util/PackageSpec.java b/core/java/com/android/internal/util/PackageSpec.java new file mode 100644 index 000000000000..85ec0bb61cdb --- /dev/null +++ b/core/java/com/android/internal/util/PackageSpec.java @@ -0,0 +1,117 @@ +package com.android.internal.util; + +import android.annotation.Nullable; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.PackageInfoFlags; +import android.content.pm.SigningDetails; + +import static android.content.pm.PackageManager.GET_SIGNING_CERTIFICATES; + +public class PackageSpec { + public final String packageName; + private final long minVersionCode; + private final byte[][] validCertificatesSha256; + + public PackageSpec(String packageName, long minVersionCode, byte[][] validCertificatesSha256) { + this.packageName = packageName; + this.minVersionCode = minVersionCode; + this.validCertificatesSha256 = validCertificatesSha256; + } + + public PackageSpec(String packageName, long minVersionCode, String[] validCertificatesSha256) { + this(packageName, minVersionCode, decodeHexStrings(validCertificatesSha256, 64)); + } + + public interface Validator { + boolean validatePackageSpec(PackageSpec s); + } + + public static PackageSpec.Validator getValidator(PackageManager pm) { + return (PackageSpec spec) -> spec.validate(pm, 0L); + } + + public boolean validate(String packageName, Validator validator) { + if (!this.packageName.equals(packageName)) { + return false; + } + + return validator.validatePackageSpec(this); + } + + public boolean validate(PackageManager pm, String packageName, long flags) { + if (!this.packageName.equals(packageName)) { + return false; + } + + return validate(pm, flags); + } + + public boolean validate(PackageManager pm, long flags) { + PackageInfo pi; + try { + pi = pm.getPackageInfo(packageName, PackageInfoFlags.of(GET_SIGNING_CERTIFICATES | flags)); + } catch (PackageManager.NameNotFoundException e) { + return false; + } + + return validate(pi); + } + + public boolean validate(PackageInfo pi) { + return validate(pi.packageName, pi.getLongVersionCode(), pi.signingInfo.getSigningDetails()); + } + + public boolean validate(String packageName, long versionCode, @Nullable SigningDetails signingDetails) { + if (signingDetails == null) { + return false; + } + + if (!this.packageName.equals(packageName)) { + return false; + } + + if (versionCode < minVersionCode) { + return false; + } + + for (byte[] hash : validCertificatesSha256) { + if (signingDetails.hasSha256Certificate(hash)) { + return true; + } + } + + return false; + } + + private static byte[][] decodeHexStrings(String[] arr, int requiredLength) { + int len = arr.length; + byte[][] res = new byte[len][]; + for (int i = 0; i < len; ++i) { + String s = arr[i]; + if (s.length() != requiredLength) { + throw new IllegalArgumentException(s); + } + res[i] = decodeHexString(s); + } + return res; + } + + private static byte[] decodeHexString(String s) { + // each byte takes 2 characters, so length must be even + int strLen = s.length(); + if ((strLen & 1) != 0) { + throw new IllegalArgumentException(s); + } + + int arrLen = strLen / 2; + byte[] arr = new byte[arrLen]; + for (int i = 0; i < arrLen; ++i) { + int off = i << 1; + int top = Character.digit(s.charAt(off), 16); + int bot = Character.digit(s.charAt(off + 1), 16); + arr[i] = (byte) ((top << 4) | bot); + } + return arr; + } +} diff --git a/core/java/com/android/internal/util/PixelCameraServicesUtils.java b/core/java/com/android/internal/util/PixelCameraServicesUtils.java new file mode 100644 index 000000000000..354538f29c01 --- /dev/null +++ b/core/java/com/android/internal/util/PixelCameraServicesUtils.java @@ -0,0 +1,17 @@ +package com.android.internal.util; + +import android.content.Context; + +public class PixelCameraServicesUtils { + public static final String PACKAGE_NAME = "com.google.android.apps.camera.services"; + + public static final PackageSpec PACKAGE_SPEC = new PackageSpec( + PACKAGE_NAME, 124000L, new String[] { + "226bb0439d6baeaa5a397c586e7031d8addfaec73c65be212f4a5dbfbf621b92", + "340eed953b55f59c65ca8fb6c132974383e9435292639e279aa5e1169ed1ef0d", + }); + + public static boolean validatePackage(Context ctx, String vendorProxyPackage) { + return PACKAGE_SPEC.validate(ctx.getPackageManager(), vendorProxyPackage, 0L); + } +} diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java index 1b1efeed5ff2..68d5820d474e 100644 --- a/core/java/com/android/internal/widget/LockPatternUtils.java +++ b/core/java/com/android/internal/widget/LockPatternUtils.java @@ -1103,7 +1103,7 @@ public boolean isVisiblePatternEverChosen(int userId) { * @return Whether enhanced pin privacy is enabled. */ public boolean isPinEnhancedPrivacyEnabled(int userId) { - return getBoolean(LOCK_PIN_ENHANCED_PRIVACY, false, userId); + return getBoolean(LOCK_PIN_ENHANCED_PRIVACY, true, userId); } /** diff --git a/core/jni/Android.bp b/core/jni/Android.bp index 42d68960cafd..b82159dd8c73 100644 --- a/core/jni/Android.bp +++ b/core/jni/Android.bp @@ -247,6 +247,7 @@ cc_library_shared { "android_window_WindowInfosListener.cpp", "android_window_ScreenCapture.cpp", "jni_common.cpp", + "ExecStrings.cpp", ], static_libs: [ diff --git a/core/jni/ExecStrings.cpp b/core/jni/ExecStrings.cpp new file mode 100644 index 000000000000..6fdca3a39c63 --- /dev/null +++ b/core/jni/ExecStrings.cpp @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2011 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. + */ + +// copied from libcore/luni/src/main/native/ExecStrings.cpp, commit cab01ac294bb8ded259851673baa4c6ca226f828 + +#define LOG_TAG "ExecStrings" + +#include "ExecStrings.h" + +#include + +#include + +#include + +ExecStrings::ExecStrings(JNIEnv* env, jobjectArray java_string_array) + : env_(env), java_array_(java_string_array), array_(NULL) { + if (java_array_ == NULL) { + return; + } + + jsize length = env_->GetArrayLength(java_array_); + array_ = new char*[length + 1]; + array_[length] = NULL; + for (jsize i = 0; i < length; ++i) { + ScopedLocalRef java_string(env_, reinterpret_cast(env_->GetObjectArrayElement(java_array_, i))); + // We need to pass these strings to const-unfriendly code. + char* string = const_cast(env_->GetStringUTFChars(java_string.get(), NULL)); + array_[i] = string; + } +} + +ExecStrings::~ExecStrings() { + if (array_ == NULL) { + return; + } + + // Temporarily clear any pending exception so we can clean up. + jthrowable pending_exception = env_->ExceptionOccurred(); + if (pending_exception != NULL) { + env_->ExceptionClear(); + } + + jsize length = env_->GetArrayLength(java_array_); + for (jsize i = 0; i < length; ++i) { + ScopedLocalRef java_string(env_, reinterpret_cast(env_->GetObjectArrayElement(java_array_, i))); + env_->ReleaseStringUTFChars(java_string.get(), array_[i]); + } + delete[] array_; + + // Re-throw any pending exception. + if (pending_exception != NULL) { + if (env_->Throw(pending_exception) < 0) { + ALOGE("Error rethrowing exception!"); + } + } +} + +char** ExecStrings::get() { + return array_; +} diff --git a/core/jni/ExecStrings.h b/core/jni/ExecStrings.h new file mode 100644 index 000000000000..7a161b589741 --- /dev/null +++ b/core/jni/ExecStrings.h @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2011 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. + */ + +// copied from libcore/luni/src/main/native/ExecStrings.h, commit cab01ac294bb8ded259851673baa4c6ca226f828 + +#include "jni.h" + +class ExecStrings { + public: + ExecStrings(JNIEnv* env, jobjectArray java_string_array); + + ~ExecStrings(); + + char** get(); + + private: + JNIEnv* env_; + jobjectArray java_array_; + char** array_; + + // Disallow copy and assignment. + ExecStrings(const ExecStrings&); + void operator=(const ExecStrings&); +}; diff --git a/core/jni/LayoutlibLoader.cpp b/core/jni/LayoutlibLoader.cpp index 200ddefc3bbc..e7acbdfbddf6 100644 --- a/core/jni/LayoutlibLoader.cpp +++ b/core/jni/LayoutlibLoader.cpp @@ -69,7 +69,7 @@ static void NativeAllocationRegistry_Delegate_nativeApplyFreeFunction(JNIEnv*, j nativeFreeFunction(nativePtr); } -static JNINativeMethod gMethods[] = { +static const JNINativeMethod gMethods[] = { NATIVE_METHOD(NativeAllocationRegistry_Delegate, nativeApplyFreeFunction, "(JJ)V"), }; diff --git a/core/jni/android_app_ActivityThread.cpp b/core/jni/android_app_ActivityThread.cpp index e25ba76cbbeb..16a8d4656179 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_os_HidlMemory.cpp b/core/jni/android_os_HidlMemory.cpp index 69e48184c0ad..612fc95776a5 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 781895eeeaba..cbec79144826 100644 --- a/core/jni/android_os_HwBinder.cpp +++ b/core/jni/android_os_HwBinder.cpp @@ -352,7 +352,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 a9db91be1d5b..ba4cf6101449 100644 --- a/core/jni/android_os_HwBlob.cpp +++ b/core/jni/android_os_HwBlob.cpp @@ -599,7 +599,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 4c4443fc29c3..78fd6d90691b 100644 --- a/core/jni/android_os_HwParcel.cpp +++ b/core/jni/android_os_HwParcel.cpp @@ -1068,7 +1068,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 d2d7213e5761..497aa193eb4d 100644 --- a/core/jni/android_os_HwRemoteBinder.cpp +++ b/core/jni/android_os_HwRemoteBinder.cpp @@ -452,7 +452,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_util_Binder.cpp b/core/jni/android_util_Binder.cpp index b24dc8a9e63b..3b7ba68ed4a2 100644 --- a/core/jni/android_util_Binder.cpp +++ b/core/jni/android_util_Binder.cpp @@ -75,6 +75,7 @@ static struct bindernative_offsets_t jclass mClass; jmethodID mExecTransact; jmethodID mGetInterfaceDescriptor; + jmethodID mTransactionCallback; // Object state. jfieldID mObject; @@ -1121,6 +1122,8 @@ static int int_register_android_os_Binder(JNIEnv* env) gBinderOffsets.mExecTransact = GetMethodIDOrDie(env, clazz, "execTransact", "(IJJI)Z"); gBinderOffsets.mGetInterfaceDescriptor = GetMethodIDOrDie(env, clazz, "getInterfaceDescriptor", "()Ljava/lang/String;"); + gBinderOffsets.mTransactionCallback = + GetStaticMethodIDOrDie(env, clazz, "transactionCallback", "(IIII)V"); gBinderOffsets.mObject = GetFieldIDOrDie(env, clazz, "mObject", "J"); return RegisterMethodsOrDie( @@ -1452,7 +1455,12 @@ static jboolean android_os_BinderProxy_transact(JNIEnv* env, jobject obj, if (err == NO_ERROR) { return JNI_TRUE; - } else if (err == UNKNOWN_TRANSACTION) { + } + + env->CallStaticVoidMethod(gBinderOffsets.mClass, gBinderOffsets.mTransactionCallback, getpid(), + code, flags, err); + + if (err == UNKNOWN_TRANSACTION) { return JNI_FALSE; } diff --git a/core/jni/com_android_internal_os_Zygote.cpp b/core/jni/com_android_internal_os_Zygote.cpp index 56066b2d813c..668bf89f134c 100644 --- a/core/jni/com_android_internal_os_Zygote.cpp +++ b/core/jni/com_android_internal_os_Zygote.cpp @@ -94,6 +94,8 @@ #include "nativebridge/native_bridge.h" +#include "ExecStrings.h" + #if defined(__BIONIC__) extern "C" void android_reset_stack_guards(); #endif @@ -359,6 +361,19 @@ enum RuntimeFlags : uint32_t { DEBUG_ENABLE_PTRACE = 1 << 25, }; +struct ExtraArgs { + uint64_t selinux_flags = 0; + + ExtraArgs() {} + + ExtraArgs(JNIEnv* env, jlongArray jlongArgs) { + const size_t num_jlong_args = 1; + jlong jlong_arr[num_jlong_args]; + env->GetLongArrayRegion(jlongArgs, 0, num_jlong_args, (jlong *) &jlong_arr); + selinux_flags = (uint64_t) jlong_arr[0]; + } +}; + enum UnsolicitedZygoteMessageTypes : uint32_t { UNSOLICITED_ZYGOTE_MESSAGE_TYPE_RESERVED = 0, UNSOLICITED_ZYGOTE_MESSAGE_TYPE_SIGCHLD = 1, @@ -1739,6 +1754,97 @@ static void BindMountStorageDirs(JNIEnv* env, jobjectArray pkg_data_info_list, } } +static void HandleRuntimeFlags(JNIEnv* env, jint& runtime_flags, const char* process_name, const char* nice_name_ptr) { + // Set process properties to enable debugging if required. + if ((runtime_flags & RuntimeFlags::DEBUG_ENABLE_PTRACE) != 0) { + EnableDebugger(); + // Don't pass unknown flag to the ART runtime. + runtime_flags &= ~RuntimeFlags::DEBUG_ENABLE_PTRACE; + } + if ((runtime_flags & RuntimeFlags::PROFILE_FROM_SHELL) != 0) { + // simpleperf needs the process to be dumpable to profile it. + if (prctl(PR_SET_DUMPABLE, 1, 0, 0, 0) == -1) { + ALOGE("prctl(PR_SET_DUMPABLE) failed: %s", strerror(errno)); + RuntimeAbort(env, __LINE__, "prctl(PR_SET_DUMPABLE, 1) failed"); + } + } + + HeapTaggingLevel heap_tagging_level; + switch (runtime_flags & RuntimeFlags::MEMORY_TAG_LEVEL_MASK) { + case RuntimeFlags::MEMORY_TAG_LEVEL_TBI: + heap_tagging_level = M_HEAP_TAGGING_LEVEL_TBI; + break; + case RuntimeFlags::MEMORY_TAG_LEVEL_ASYNC: + heap_tagging_level = M_HEAP_TAGGING_LEVEL_ASYNC; + break; + case RuntimeFlags::MEMORY_TAG_LEVEL_SYNC: + heap_tagging_level = M_HEAP_TAGGING_LEVEL_SYNC; + break; + default: + heap_tagging_level = M_HEAP_TAGGING_LEVEL_NONE; + break; + } + mallopt(M_BIONIC_SET_HEAP_TAGGING_LEVEL, heap_tagging_level); + + const int FORCIBLY_ENABLE_MEMORY_TAGGING = 1 << 28; + if (runtime_flags & FORCIBLY_ENABLE_MEMORY_TAGGING) { + 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"); + } + runtime_flags &= ~FORCIBLY_ENABLE_MEMORY_TAGGING; + } + + // 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; + + // Avoid heap zero initialization for applications without MTE. Zero init may + // cause app compat problems, use more memory, or reduce performance. While it + // would be nice to have them for apps, we will have to wait until they are + // proven out, have more efficient hardware, and/or apply them only to new + // applications. + if (!(runtime_flags & RuntimeFlags::NATIVE_HEAP_ZERO_INIT_ENABLED)) { + mallopt(M_BIONIC_ZERO_INIT, 0); + } + + // Now that we've used the flag, clear it so that we don't pass unknown flags to the ART + // runtime. + runtime_flags &= ~RuntimeFlags::NATIVE_HEAP_ZERO_INIT_ENABLED; + + android_mallopt_gwp_asan_options_t gwp_asan_options; + const char* kGwpAsanAppRecoverableSysprop = + "persist.device_config.memory_safety_native.gwp_asan_recoverable_apps"; + // The system server doesn't have its nice name set by the time SpecializeCommon is called. + gwp_asan_options.program_name = nice_name_ptr ?: process_name; + switch (runtime_flags & RuntimeFlags::GWP_ASAN_LEVEL_MASK) { + default: + case RuntimeFlags::GWP_ASAN_LEVEL_DEFAULT: + gwp_asan_options.desire = GetBoolProperty(kGwpAsanAppRecoverableSysprop, true) + ? Action::TURN_ON_FOR_APP_SAMPLED_NON_CRASHING + : Action::DONT_TURN_ON_UNLESS_OVERRIDDEN; + android_mallopt(M_INITIALIZE_GWP_ASAN, &gwp_asan_options, sizeof(gwp_asan_options)); + break; + case RuntimeFlags::GWP_ASAN_LEVEL_NEVER: + gwp_asan_options.desire = Action::DONT_TURN_ON_UNLESS_OVERRIDDEN; + android_mallopt(M_INITIALIZE_GWP_ASAN, &gwp_asan_options, sizeof(gwp_asan_options)); + break; + case RuntimeFlags::GWP_ASAN_LEVEL_ALWAYS: + gwp_asan_options.desire = Action::TURN_ON_FOR_APP; + android_mallopt(M_INITIALIZE_GWP_ASAN, &gwp_asan_options, sizeof(gwp_asan_options)); + break; + case RuntimeFlags::GWP_ASAN_LEVEL_LOTTERY: + gwp_asan_options.desire = Action::TURN_ON_WITH_SAMPLING; + android_mallopt(M_INITIALIZE_GWP_ASAN, &gwp_asan_options, sizeof(gwp_asan_options)); + break; + } + // Now that we've used the flag, clear it so that we don't pass unknown flags to the ART + // runtime. + runtime_flags &= ~RuntimeFlags::GWP_ASAN_LEVEL_MASK; +} + // Utility routine to specialize a zygote child process. static void SpecializeCommon(JNIEnv* env, uid_t uid, gid_t gid, jintArray gids, jint runtime_flags, jobjectArray rlimits, jlong permitted_capabilities, @@ -1748,7 +1854,7 @@ static void SpecializeCommon(JNIEnv* env, uid_t uid, gid_t gid, jintArray gids, jstring managed_instruction_set, jstring managed_app_data_dir, bool is_top_app, jobjectArray pkg_data_info_list, jobjectArray allowlisted_data_info_list, bool mount_data_dirs, - bool mount_storage_dirs) { + bool mount_storage_dirs, ExtraArgs& extra_args) { const char* process_name = is_system_server ? "system_server" : "zygote"; auto fail_fn = std::bind(ZygoteFailure, env, process_name, managed_nice_name, _1); auto extract_fn = std::bind(ExtractJString, env, process_name, managed_nice_name, _1); @@ -1758,6 +1864,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))); + } + } + // Keep capabilities across UID change, unless we're staying root. if (uid != 0) { EnableKeepCapabilities(fail_fn); @@ -1882,84 +2001,9 @@ static void SpecializeCommon(JNIEnv* env, uid_t uid, gid_t gid, jintArray gids, } } - // Set process properties to enable debugging if required. - if ((runtime_flags & RuntimeFlags::DEBUG_ENABLE_PTRACE) != 0) { - EnableDebugger(); - // Don't pass unknown flag to the ART runtime. - runtime_flags &= ~RuntimeFlags::DEBUG_ENABLE_PTRACE; - } - if ((runtime_flags & RuntimeFlags::PROFILE_FROM_SHELL) != 0) { - // simpleperf needs the process to be dumpable to profile it. - if (prctl(PR_SET_DUMPABLE, 1, 0, 0, 0) == -1) { - ALOGE("prctl(PR_SET_DUMPABLE) failed: %s", strerror(errno)); - RuntimeAbort(env, __LINE__, "prctl(PR_SET_DUMPABLE, 1) failed"); - } - } - - HeapTaggingLevel heap_tagging_level; - switch (runtime_flags & RuntimeFlags::MEMORY_TAG_LEVEL_MASK) { - case RuntimeFlags::MEMORY_TAG_LEVEL_TBI: - heap_tagging_level = M_HEAP_TAGGING_LEVEL_TBI; - break; - case RuntimeFlags::MEMORY_TAG_LEVEL_ASYNC: - heap_tagging_level = M_HEAP_TAGGING_LEVEL_ASYNC; - break; - case RuntimeFlags::MEMORY_TAG_LEVEL_SYNC: - heap_tagging_level = M_HEAP_TAGGING_LEVEL_SYNC; - break; - default: - heap_tagging_level = M_HEAP_TAGGING_LEVEL_NONE; - break; - } - mallopt(M_BIONIC_SET_HEAP_TAGGING_LEVEL, heap_tagging_level); - - // 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; - - // Avoid heap zero initialization for applications without MTE. Zero init may - // cause app compat problems, use more memory, or reduce performance. While it - // would be nice to have them for apps, we will have to wait until they are - // proven out, have more efficient hardware, and/or apply them only to new - // applications. - if (!(runtime_flags & RuntimeFlags::NATIVE_HEAP_ZERO_INIT_ENABLED)) { - mallopt(M_BIONIC_ZERO_INIT, 0); - } - - // Now that we've used the flag, clear it so that we don't pass unknown flags to the ART - // runtime. - runtime_flags &= ~RuntimeFlags::NATIVE_HEAP_ZERO_INIT_ENABLED; - const char* nice_name_ptr = nice_name.has_value() ? nice_name.value().c_str() : nullptr; - android_mallopt_gwp_asan_options_t gwp_asan_options; - const char* kGwpAsanAppRecoverableSysprop = - "persist.device_config.memory_safety_native.gwp_asan_recoverable_apps"; - // The system server doesn't have its nice name set by the time SpecializeCommon is called. - gwp_asan_options.program_name = nice_name_ptr ?: process_name; - switch (runtime_flags & RuntimeFlags::GWP_ASAN_LEVEL_MASK) { - default: - case RuntimeFlags::GWP_ASAN_LEVEL_DEFAULT: - gwp_asan_options.desire = GetBoolProperty(kGwpAsanAppRecoverableSysprop, true) - ? Action::TURN_ON_FOR_APP_SAMPLED_NON_CRASHING - : Action::DONT_TURN_ON_UNLESS_OVERRIDDEN; - android_mallopt(M_INITIALIZE_GWP_ASAN, &gwp_asan_options, sizeof(gwp_asan_options)); - break; - case RuntimeFlags::GWP_ASAN_LEVEL_NEVER: - gwp_asan_options.desire = Action::DONT_TURN_ON_UNLESS_OVERRIDDEN; - android_mallopt(M_INITIALIZE_GWP_ASAN, &gwp_asan_options, sizeof(gwp_asan_options)); - break; - case RuntimeFlags::GWP_ASAN_LEVEL_ALWAYS: - gwp_asan_options.desire = Action::TURN_ON_FOR_APP; - android_mallopt(M_INITIALIZE_GWP_ASAN, &gwp_asan_options, sizeof(gwp_asan_options)); - break; - case RuntimeFlags::GWP_ASAN_LEVEL_LOTTERY: - gwp_asan_options.desire = Action::TURN_ON_WITH_SAMPLING; - android_mallopt(M_INITIALIZE_GWP_ASAN, &gwp_asan_options, sizeof(gwp_asan_options)); - break; - } - // Now that we've used the flag, clear it so that we don't pass unknown flags to the ART - // runtime. - runtime_flags &= ~RuntimeFlags::GWP_ASAN_LEVEL_MASK; + + HandleRuntimeFlags(env, runtime_flags, process_name, nice_name_ptr); SetCapabilities(permitted_capabilities, effective_capabilities, permitted_capabilities, fail_fn); @@ -1969,9 +2013,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)); } // Make it easier to debug audit logs by setting the main thread's name to the @@ -2354,7 +2398,8 @@ static jint com_android_internal_os_Zygote_nativeForkAndSpecialize( 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, jobjectArray pkg_data_info_list, jobjectArray allowlisted_data_info_list, - jboolean mount_data_dirs, jboolean mount_storage_dirs) { + jboolean mount_data_dirs, jboolean mount_storage_dirs, jlongArray extra_jlong_args) { + ExtraArgs extra_args(env, extra_jlong_args); jlong capabilities = CalculateCapabilities(env, uid, gid, gids, is_child_zygote); if (UNLIKELY(managed_fds_to_close == nullptr)) { @@ -2397,7 +2442,7 @@ static jint com_android_internal_os_Zygote_nativeForkAndSpecialize( 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, mount_data_dirs == JNI_TRUE, - mount_storage_dirs == JNI_TRUE); + mount_storage_dirs == JNI_TRUE, extra_args); } return pid; } @@ -2429,11 +2474,12 @@ 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. + ExtraArgs extra_args; SpecializeCommon(env, uid, gid, gids, runtime_flags, rlimits, permitted_capabilities, effective_capabilities, MOUNT_EXTERNAL_DEFAULT, nullptr, nullptr, true, false, nullptr, nullptr, /* is_top_app= */ false, /* pkg_data_info_list */ nullptr, - /* allowlisted_data_info_list */ nullptr, false, false); + /* allowlisted_data_info_list */ nullptr, false, false, extra_args); } else if (pid > 0) { // The zygote process checks whether the child process has died or not. ALOGI("System server process %d has been created", pid); @@ -2585,14 +2631,15 @@ static void com_android_internal_os_Zygote_nativeSpecializeAppProcess( jboolean is_child_zygote, jstring instruction_set, jstring app_data_dir, jboolean is_top_app, jobjectArray pkg_data_info_list, jobjectArray allowlisted_data_info_list, jboolean mount_data_dirs, - jboolean mount_storage_dirs) { + jboolean mount_storage_dirs, jlongArray extra_jlong_args) { + ExtraArgs extra_args(env, extra_jlong_args); jlong capabilities = CalculateCapabilities(env, uid, gid, gids, is_child_zygote); SpecializeCommon(env, uid, gid, gids, runtime_flags, rlimits, capabilities, 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, mount_data_dirs == JNI_TRUE, - mount_storage_dirs == JNI_TRUE); + mount_storage_dirs == JNI_TRUE, extra_args); } /** @@ -2867,10 +2914,26 @@ static void com_android_internal_os_Zygote_nativeAllowFilesOpenedByPreload(JNIEn gPreloadFdsExtracted = true; } +static void nativeHandleRuntimeFlagsWrapper(JNIEnv* env, jclass, jint runtime_flags) { + HandleRuntimeFlags(env, runtime_flags, nullptr, nullptr); +} + +static jint execveatWrapper(JNIEnv* env, jclass, jint dirFd, jstring javaFilename, jobjectArray javaArgv, jint flags) { + ScopedUtfChars path(env, javaFilename); + if (path.c_str() == NULL) { + return EINVAL; + } + + ExecStrings argv(env, javaArgv); + TEMP_FAILURE_RETRY(execveat(dirFd, path.c_str(), argv.get(), environ, flags)); + // execveat never returns on success + return errno; +} + static const JNINativeMethod gMethods[] = { {"nativeForkAndSpecialize", "(II[II[[IILjava/lang/String;Ljava/lang/String;[I[IZLjava/lang/String;Ljava/lang/" - "String;Z[Ljava/lang/String;[Ljava/lang/String;ZZ)I", + "String;Z[Ljava/lang/String;[Ljava/lang/String;ZZ[J)I", (void*)com_android_internal_os_Zygote_nativeForkAndSpecialize}, {"nativeForkSystemServer", "(II[II[[IJJ)I", (void*)com_android_internal_os_Zygote_nativeForkSystemServer}, @@ -2886,7 +2949,7 @@ static const JNINativeMethod gMethods[] = { (void*)com_android_internal_os_Zygote_nativeAddUsapTableEntry}, {"nativeSpecializeAppProcess", "(II[II[[IILjava/lang/String;Ljava/lang/String;ZLjava/lang/String;Ljava/lang/" - "String;Z[Ljava/lang/String;[Ljava/lang/String;ZZ)V", + "String;Z[Ljava/lang/String;[Ljava/lang/String;ZZ[J)V", (void*)com_android_internal_os_Zygote_nativeSpecializeAppProcess}, {"nativeInitNativeState", "(Z)V", (void*)com_android_internal_os_Zygote_nativeInitNativeState}, @@ -2919,6 +2982,8 @@ static const JNINativeMethod gMethods[] = { (void*)com_android_internal_os_Zygote_nativeMarkOpenedFilesBeforePreload}, {"nativeAllowFilesOpenedByPreload", "()V", (void*)com_android_internal_os_Zygote_nativeAllowFilesOpenedByPreload}, + {"nativeHandleRuntimeFlags", "(I)V", (void*)nativeHandleRuntimeFlagsWrapper}, + {"execveatWrapper", "(ILjava/lang/String;[Ljava/lang/String;I)I", (void*)execveatWrapper}, }; int register_com_android_internal_os_Zygote(JNIEnv* env) { diff --git a/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp b/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp index 2b5b8f7a108e..4057c49d6c65 100644 --- a/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp +++ b/core/jni/com_android_internal_os_ZygoteCommandBuffer.cpp @@ -171,6 +171,9 @@ class NativeCommandBuffer { static const size_t CA_LENGTH = strlen(CAPABILITIES); static const size_t NN_LENGTH = strlen(NICE_NAME); + static const char* RUNTIME_FLAGS = "--runtime-flags="; + static const size_t RF_LENGTH = strlen(RUNTIME_FLAGS); + bool saw_setuid = false, saw_setgid = false; bool saw_runtime_args = false; @@ -185,6 +188,17 @@ class NativeCommandBuffer { saw_runtime_args = true; continue; } + if (arg_end - arg_start >= RF_LENGTH + && strncmp(arg_start, RUNTIME_FLAGS, RF_LENGTH) == 0) { + int flags = digitsVal(arg_start + RF_LENGTH, arg_end); + const int DISABLE_HARDENED_MALLOC = 1 << 29; + const int ENABLE_COMPAT_VA_39_BIT = 1 << 30; + if (flags & (DISABLE_HARDENED_MALLOC | ENABLE_COMPAT_VA_39_BIT)) { + // fallback to the slow path that calls ExecInit + return false; + } + continue; + } if (arg_end - arg_start >= NN_LENGTH && strncmp(arg_start, NICE_NAME, NN_LENGTH) == 0) { size_t name_len = arg_end - (arg_start + NN_LENGTH); diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 8c9a3ff5149f..57511f500336 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -578,6 +578,7 @@ + @@ -1792,6 +1793,18 @@ android:protectionLevel="dangerous|instant" /> + + + + @@ -2031,13 +2044,21 @@ + + + + android:protectionLevel="dangerous|instant" /> + + + + @@ -2612,6 +2641,10 @@ + + + @@ -2761,6 +2794,10 @@ + + + + + @@ -5628,6 +5669,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 000000000000..410b2d75018f --- /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 000000000000..8574d79d58a6 --- /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 a8cdef6d05f1..34987394b2ec 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 5ad0f4c0f6cc..ad3a2d2991de 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 c3b149a1e295..a47b82018377 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" />