diff options
195 files changed, 5718 insertions, 2192 deletions
diff --git a/apex/jobscheduler/service/aconfig/job.aconfig b/apex/jobscheduler/service/aconfig/job.aconfig index 11c5b51e23ae..98e53ab97872 100644 --- a/apex/jobscheduler/service/aconfig/job.aconfig +++ b/apex/jobscheduler/service/aconfig/job.aconfig @@ -82,3 +82,10 @@ flag { description: "Applies the normal quota policy to FGS jobs" bug: "341201311" } + +flag { + name: "adjust_quota_default_constants" + namespace: "backstage_power" + description: "Adjust quota default parameters" + bug: "347058927" +}
\ No newline at end of file diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java index 46cc3f01d261..885bad5e31c8 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java @@ -394,13 +394,13 @@ public final class QuotaController extends StateController { * minutes to run its jobs. */ private final long[] mBucketPeriodsMs = new long[]{ - QcConstants.DEFAULT_WINDOW_SIZE_ACTIVE_MS, - QcConstants.DEFAULT_WINDOW_SIZE_WORKING_MS, - QcConstants.DEFAULT_WINDOW_SIZE_FREQUENT_MS, + QcConstants.DEFAULT_LEGACY_WINDOW_SIZE_ACTIVE_MS, + QcConstants.DEFAULT_LEGACY_WINDOW_SIZE_WORKING_MS, + QcConstants.DEFAULT_LEGACY_WINDOW_SIZE_FREQUENT_MS, QcConstants.DEFAULT_WINDOW_SIZE_RARE_MS, 0, // NEVER QcConstants.DEFAULT_WINDOW_SIZE_RESTRICTED_MS, - QcConstants.DEFAULT_WINDOW_SIZE_EXEMPTED_MS + QcConstants.DEFAULT_LEGACY_WINDOW_SIZE_EXEMPTED_MS }; /** The maximum period any bucket can have. */ @@ -454,7 +454,7 @@ public final class QuotaController extends StateController { */ private final long[] mEJLimitsMs = new long[]{ QcConstants.DEFAULT_EJ_LIMIT_ACTIVE_MS, - QcConstants.DEFAULT_EJ_LIMIT_WORKING_MS, + QcConstants.DEFAULT_LEGACY_EJ_LIMIT_WORKING_MS, QcConstants.DEFAULT_EJ_LIMIT_FREQUENT_MS, QcConstants.DEFAULT_EJ_LIMIT_RARE_MS, 0, // NEVER @@ -476,7 +476,8 @@ public final class QuotaController extends StateController { /** * Length of time used to split an app's top time into chunks. */ - private long mEJTopAppTimeChunkSizeMs = QcConstants.DEFAULT_EJ_TOP_APP_TIME_CHUNK_SIZE_MS; + private long mEJTopAppTimeChunkSizeMs = + QcConstants.DEFAULT_LEGACY_EJ_TOP_APP_TIME_CHUNK_SIZE_MS; /** * How much EJ quota to give back to an app based on the number of top app time chunks it had. @@ -486,7 +487,7 @@ public final class QuotaController extends StateController { /** * How much EJ quota to give back to an app based on each non-top user interaction. */ - private long mEJRewardInteractionMs = QcConstants.DEFAULT_EJ_REWARD_INTERACTION_MS; + private long mEJRewardInteractionMs = QcConstants.DEFAULT_LEGACY_EJ_REWARD_INTERACTION_MS; /** * How much EJ quota to give back to an app based on each notification seen event. @@ -570,6 +571,8 @@ public final class QuotaController extends StateController { } catch (RemoteException e) { // ignored; both services live in system_server } + + processQuotaConstantsAdjustment(); } @Override @@ -1411,6 +1414,13 @@ public final class QuotaController extends StateController { } } + void processQuotaConstantsAdjustment() { + if (Flags.adjustQuotaDefaultConstants()) { + mQcConstants.adjustDefaultBucketWindowSizes(); + mQcConstants.adjustDefaultEjLimits(); + } + } + @VisibleForTesting void incrementJobCountLocked(final int userId, @NonNull final String packageName, int count) { final long now = sElapsedRealtimeClock.millis(); @@ -3112,14 +3122,28 @@ public final class QuotaController extends StateController { 10 * 60 * 1000L; // 10 minutes private static final long DEFAULT_IN_QUOTA_BUFFER_MS = 30 * 1000L; // 30 seconds - private static final long DEFAULT_WINDOW_SIZE_EXEMPTED_MS = + // Legacy default window size for EXEMPTED bucket + private static final long DEFAULT_LEGACY_WINDOW_SIZE_EXEMPTED_MS = DEFAULT_ALLOWED_TIME_PER_PERIOD_EXEMPTED_MS; // EXEMPT apps can run jobs at any time - private static final long DEFAULT_WINDOW_SIZE_ACTIVE_MS = + // Legacy default window size for ACTIVE bucket + private static final long DEFAULT_LEGACY_WINDOW_SIZE_ACTIVE_MS = DEFAULT_ALLOWED_TIME_PER_PERIOD_ACTIVE_MS; // ACTIVE apps can run jobs at any time - private static final long DEFAULT_WINDOW_SIZE_WORKING_MS = + // Legacy default window size for WORKING bucket + private static final long DEFAULT_LEGACY_WINDOW_SIZE_WORKING_MS = 2 * 60 * 60 * 1000L; // 2 hours - private static final long DEFAULT_WINDOW_SIZE_FREQUENT_MS = + // Legacy default window size for FREQUENT bucket + private static final long DEFAULT_LEGACY_WINDOW_SIZE_FREQUENT_MS = 8 * 60 * 60 * 1000L; // 8 hours + + private static final long DEFAULT_CURRENT_WINDOW_SIZE_EXEMPTED_MS = + 20 * 60 * 1000L; // 20 minutes. + private static final long DEFAULT_CURRENT_WINDOW_SIZE_ACTIVE_MS = + 30 * 60 * 1000L; // 30 minutes. + private static final long DEFAULT_CURRENT_WINDOW_SIZE_WORKING_MS = + 4 * 60 * 60 * 1000L; // 4 hours + private static final long DEFAULT_CURRENT_WINDOW_SIZE_FREQUENT_MS = + 12 * 60 * 60 * 1000L; // 12 hours + private static final long DEFAULT_WINDOW_SIZE_RARE_MS = 24 * 60 * 60 * 1000L; // 24 hours private static final long DEFAULT_WINDOW_SIZE_RESTRICTED_MS = @@ -3133,9 +3157,9 @@ public final class QuotaController extends StateController { 75; // 75/window = 450/hr = 1/session private static final int DEFAULT_MAX_JOB_COUNT_ACTIVE = DEFAULT_MAX_JOB_COUNT_EXEMPTED; private static final int DEFAULT_MAX_JOB_COUNT_WORKING = // 120/window = 60/hr = 12/session - (int) (60.0 * DEFAULT_WINDOW_SIZE_WORKING_MS / HOUR_IN_MILLIS); + (int) (60.0 * DEFAULT_LEGACY_WINDOW_SIZE_WORKING_MS / HOUR_IN_MILLIS); private static final int DEFAULT_MAX_JOB_COUNT_FREQUENT = // 200/window = 25/hr = 25/session - (int) (25.0 * DEFAULT_WINDOW_SIZE_FREQUENT_MS / HOUR_IN_MILLIS); + (int) (25.0 * DEFAULT_LEGACY_WINDOW_SIZE_FREQUENT_MS / HOUR_IN_MILLIS); private static final int DEFAULT_MAX_JOB_COUNT_RARE = // 48/window = 2/hr = 16/session (int) (2.0 * DEFAULT_WINDOW_SIZE_RARE_MS / HOUR_IN_MILLIS); private static final int DEFAULT_MAX_JOB_COUNT_RESTRICTED = 10; @@ -3156,16 +3180,21 @@ public final class QuotaController extends StateController { // TODO(267949143): set a different limit for headless system apps private static final long DEFAULT_EJ_LIMIT_EXEMPTED_MS = 60 * MINUTE_IN_MILLIS; private static final long DEFAULT_EJ_LIMIT_ACTIVE_MS = 30 * MINUTE_IN_MILLIS; - private static final long DEFAULT_EJ_LIMIT_WORKING_MS = DEFAULT_EJ_LIMIT_ACTIVE_MS; + private static final long DEFAULT_LEGACY_EJ_LIMIT_WORKING_MS = DEFAULT_EJ_LIMIT_ACTIVE_MS; + private static final long DEFAULT_CURRENT_EJ_LIMIT_WORKING_MS = 15 * MINUTE_IN_MILLIS; private static final long DEFAULT_EJ_LIMIT_FREQUENT_MS = 10 * MINUTE_IN_MILLIS; private static final long DEFAULT_EJ_LIMIT_RARE_MS = DEFAULT_EJ_LIMIT_FREQUENT_MS; private static final long DEFAULT_EJ_LIMIT_RESTRICTED_MS = 5 * MINUTE_IN_MILLIS; private static final long DEFAULT_EJ_LIMIT_ADDITION_SPECIAL_MS = 15 * MINUTE_IN_MILLIS; private static final long DEFAULT_EJ_LIMIT_ADDITION_INSTALLER_MS = 30 * MINUTE_IN_MILLIS; private static final long DEFAULT_EJ_WINDOW_SIZE_MS = 24 * HOUR_IN_MILLIS; - private static final long DEFAULT_EJ_TOP_APP_TIME_CHUNK_SIZE_MS = 30 * SECOND_IN_MILLIS; + private static final long DEFAULT_LEGACY_EJ_TOP_APP_TIME_CHUNK_SIZE_MS = + 30 * SECOND_IN_MILLIS; + private static final long DEFAULT_CURRENT_EJ_TOP_APP_TIME_CHUNK_SIZE_MS = + 5 * MINUTE_IN_MILLIS; private static final long DEFAULT_EJ_REWARD_TOP_APP_MS = 10 * SECOND_IN_MILLIS; - private static final long DEFAULT_EJ_REWARD_INTERACTION_MS = 15 * SECOND_IN_MILLIS; + private static final long DEFAULT_LEGACY_EJ_REWARD_INTERACTION_MS = 15 * SECOND_IN_MILLIS; + private static final long DEFAULT_CURRENT_EJ_REWARD_INTERACTION_MS = 5 * SECOND_IN_MILLIS; private static final long DEFAULT_EJ_REWARD_NOTIFICATION_SEEN_MS = 0; private static final long DEFAULT_EJ_GRACE_PERIOD_TEMP_ALLOWLIST_MS = 3 * MINUTE_IN_MILLIS; private static final long DEFAULT_EJ_GRACE_PERIOD_TOP_APP_MS = 1 * MINUTE_IN_MILLIS; @@ -3215,28 +3244,28 @@ public final class QuotaController extends StateController { * expected to run only {@link #ALLOWED_TIME_PER_PERIOD_EXEMPTED_MS} within the past * WINDOW_SIZE_MS. */ - public long WINDOW_SIZE_EXEMPTED_MS = DEFAULT_WINDOW_SIZE_EXEMPTED_MS; + public long WINDOW_SIZE_EXEMPTED_MS = DEFAULT_LEGACY_WINDOW_SIZE_EXEMPTED_MS; /** * The quota window size of the particular standby bucket. Apps in this standby bucket are * expected to run only {@link #ALLOWED_TIME_PER_PERIOD_ACTIVE_MS} within the past * WINDOW_SIZE_MS. */ - public long WINDOW_SIZE_ACTIVE_MS = DEFAULT_WINDOW_SIZE_ACTIVE_MS; + public long WINDOW_SIZE_ACTIVE_MS = DEFAULT_LEGACY_WINDOW_SIZE_ACTIVE_MS; /** * The quota window size of the particular standby bucket. Apps in this standby bucket are * expected to run only {@link #ALLOWED_TIME_PER_PERIOD_WORKING_MS} within the past * WINDOW_SIZE_MS. */ - public long WINDOW_SIZE_WORKING_MS = DEFAULT_WINDOW_SIZE_WORKING_MS; + public long WINDOW_SIZE_WORKING_MS = DEFAULT_LEGACY_WINDOW_SIZE_WORKING_MS; /** * The quota window size of the particular standby bucket. Apps in this standby bucket are * expected to run only {@link #ALLOWED_TIME_PER_PERIOD_FREQUENT_MS} within the past * WINDOW_SIZE_MS. */ - public long WINDOW_SIZE_FREQUENT_MS = DEFAULT_WINDOW_SIZE_FREQUENT_MS; + public long WINDOW_SIZE_FREQUENT_MS = DEFAULT_LEGACY_WINDOW_SIZE_FREQUENT_MS; /** * The quota window size of the particular standby bucket. Apps in this standby bucket are @@ -3397,7 +3426,7 @@ public final class QuotaController extends StateController { * standby bucket can only have expedited job sessions totalling EJ_LIMIT (without factoring * in any rewards or free EJs). */ - public long EJ_LIMIT_WORKING_MS = DEFAULT_EJ_LIMIT_WORKING_MS; + public long EJ_LIMIT_WORKING_MS = DEFAULT_LEGACY_EJ_LIMIT_WORKING_MS; /** * The total expedited job session limit of the particular standby bucket. Apps in this @@ -3441,7 +3470,7 @@ public final class QuotaController extends StateController { /** * Length of time used to split an app's top time into chunks. */ - public long EJ_TOP_APP_TIME_CHUNK_SIZE_MS = DEFAULT_EJ_TOP_APP_TIME_CHUNK_SIZE_MS; + public long EJ_TOP_APP_TIME_CHUNK_SIZE_MS = DEFAULT_LEGACY_EJ_TOP_APP_TIME_CHUNK_SIZE_MS; /** * How much EJ quota to give back to an app based on the number of top app time chunks it @@ -3452,7 +3481,7 @@ public final class QuotaController extends StateController { /** * How much EJ quota to give back to an app based on each non-top user interaction. */ - public long EJ_REWARD_INTERACTION_MS = DEFAULT_EJ_REWARD_INTERACTION_MS; + public long EJ_REWARD_INTERACTION_MS = DEFAULT_LEGACY_EJ_REWARD_INTERACTION_MS; /** * How much EJ quota to give back to an app based on each notification seen event. @@ -3470,6 +3499,52 @@ public final class QuotaController extends StateController { */ public long EJ_GRACE_PERIOD_TOP_APP_MS = DEFAULT_EJ_GRACE_PERIOD_TOP_APP_MS; + void adjustDefaultBucketWindowSizes() { + WINDOW_SIZE_EXEMPTED_MS = DEFAULT_CURRENT_WINDOW_SIZE_EXEMPTED_MS; + WINDOW_SIZE_ACTIVE_MS = DEFAULT_CURRENT_WINDOW_SIZE_ACTIVE_MS; + WINDOW_SIZE_WORKING_MS = DEFAULT_CURRENT_WINDOW_SIZE_WORKING_MS; + WINDOW_SIZE_FREQUENT_MS = DEFAULT_CURRENT_WINDOW_SIZE_FREQUENT_MS; + + mBucketPeriodsMs[EXEMPTED_INDEX] = Math.max( + mAllowedTimePerPeriodMs[EXEMPTED_INDEX], + Math.min(MAX_PERIOD_MS, WINDOW_SIZE_EXEMPTED_MS)); + mBucketPeriodsMs[ACTIVE_INDEX] = Math.max( + mAllowedTimePerPeriodMs[ACTIVE_INDEX], + Math.min(MAX_PERIOD_MS, WINDOW_SIZE_ACTIVE_MS)); + mBucketPeriodsMs[WORKING_INDEX] = Math.max( + mAllowedTimePerPeriodMs[WORKING_INDEX], + Math.min(MAX_PERIOD_MS, WINDOW_SIZE_WORKING_MS)); + mBucketPeriodsMs[FREQUENT_INDEX] = Math.max( + mAllowedTimePerPeriodMs[FREQUENT_INDEX], + Math.min(MAX_PERIOD_MS, WINDOW_SIZE_FREQUENT_MS)); + } + + void adjustDefaultEjLimits() { + EJ_LIMIT_WORKING_MS = DEFAULT_CURRENT_EJ_LIMIT_WORKING_MS; + EJ_TOP_APP_TIME_CHUNK_SIZE_MS = DEFAULT_CURRENT_EJ_TOP_APP_TIME_CHUNK_SIZE_MS; + EJ_REWARD_INTERACTION_MS = DEFAULT_CURRENT_EJ_REWARD_INTERACTION_MS; + + // The limit must be in the range [15 minutes, active limit]. + mEJLimitsMs[WORKING_INDEX] = Math.max(15 * MINUTE_IN_MILLIS, + Math.min(mEJLimitsMs[ACTIVE_INDEX], EJ_LIMIT_WORKING_MS)); + + // Limit interaction reward to be in the range [5 seconds, 15 minutes] per event. + mEJRewardInteractionMs = Math.min(15 * MINUTE_IN_MILLIS, + Math.max(5 * SECOND_IN_MILLIS, EJ_REWARD_INTERACTION_MS)); + + // Limit chunking to be in the range [1 millisecond, 15 minutes] per event. + long newChunkSizeMs = Math.min(15 * MINUTE_IN_MILLIS, + Math.max(1, EJ_TOP_APP_TIME_CHUNK_SIZE_MS)); + mEJTopAppTimeChunkSizeMs = newChunkSizeMs; + if (mEJTopAppTimeChunkSizeMs < mEJRewardTopAppMs) { + // Not making chunk sizes and top rewards to be the upper/lower + // limits of the other to allow trying different policies. Just log + // the discrepancy. + Slog.w(TAG, "EJ top app time chunk less than reward: " + + mEJTopAppTimeChunkSizeMs + " vs " + mEJRewardTopAppMs); + } + } + public void processConstantLocked(@NonNull DeviceConfig.Properties properties, @NonNull String key) { switch (key) { @@ -3638,7 +3713,9 @@ public final class QuotaController extends StateController { case KEY_EJ_TOP_APP_TIME_CHUNK_SIZE_MS: // We don't need to re-evaluate execution stats or constraint status for this. EJ_TOP_APP_TIME_CHUNK_SIZE_MS = - properties.getLong(key, DEFAULT_EJ_TOP_APP_TIME_CHUNK_SIZE_MS); + properties.getLong(key, Flags.adjustQuotaDefaultConstants() + ? DEFAULT_CURRENT_EJ_TOP_APP_TIME_CHUNK_SIZE_MS : + DEFAULT_LEGACY_EJ_TOP_APP_TIME_CHUNK_SIZE_MS); // Limit chunking to be in the range [1 millisecond, 15 minutes] per event. long newChunkSizeMs = Math.min(15 * MINUTE_IN_MILLIS, Math.max(1, EJ_TOP_APP_TIME_CHUNK_SIZE_MS)); @@ -3674,7 +3751,9 @@ public final class QuotaController extends StateController { case KEY_EJ_REWARD_INTERACTION_MS: // We don't need to re-evaluate execution stats or constraint status for this. EJ_REWARD_INTERACTION_MS = - properties.getLong(key, DEFAULT_EJ_REWARD_INTERACTION_MS); + properties.getLong(key, Flags.adjustQuotaDefaultConstants() + ? DEFAULT_CURRENT_EJ_REWARD_INTERACTION_MS : + DEFAULT_LEGACY_EJ_REWARD_INTERACTION_MS); // Limit interaction reward to be in the range [5 seconds, 15 minutes] per // event. mEJRewardInteractionMs = Math.min(15 * MINUTE_IN_MILLIS, @@ -3748,14 +3827,23 @@ public final class QuotaController extends StateController { MAX_EXECUTION_TIME_MS = properties.getLong(KEY_MAX_EXECUTION_TIME_MS, DEFAULT_MAX_EXECUTION_TIME_MS); WINDOW_SIZE_EXEMPTED_MS = properties.getLong(KEY_WINDOW_SIZE_EXEMPTED_MS, - DEFAULT_WINDOW_SIZE_EXEMPTED_MS); + Flags.adjustQuotaDefaultConstants() + ? DEFAULT_CURRENT_WINDOW_SIZE_EXEMPTED_MS : + DEFAULT_LEGACY_WINDOW_SIZE_EXEMPTED_MS); WINDOW_SIZE_ACTIVE_MS = properties.getLong(KEY_WINDOW_SIZE_ACTIVE_MS, - DEFAULT_WINDOW_SIZE_ACTIVE_MS); + Flags.adjustQuotaDefaultConstants() + ? DEFAULT_CURRENT_WINDOW_SIZE_ACTIVE_MS : + DEFAULT_LEGACY_WINDOW_SIZE_ACTIVE_MS); WINDOW_SIZE_WORKING_MS = - properties.getLong(KEY_WINDOW_SIZE_WORKING_MS, DEFAULT_WINDOW_SIZE_WORKING_MS); + properties.getLong(KEY_WINDOW_SIZE_WORKING_MS, + Flags.adjustQuotaDefaultConstants() + ? DEFAULT_CURRENT_WINDOW_SIZE_WORKING_MS : + DEFAULT_LEGACY_WINDOW_SIZE_WORKING_MS); WINDOW_SIZE_FREQUENT_MS = properties.getLong(KEY_WINDOW_SIZE_FREQUENT_MS, - DEFAULT_WINDOW_SIZE_FREQUENT_MS); + Flags.adjustQuotaDefaultConstants() + ? DEFAULT_CURRENT_WINDOW_SIZE_FREQUENT_MS : + DEFAULT_LEGACY_WINDOW_SIZE_FREQUENT_MS); WINDOW_SIZE_RARE_MS = properties.getLong(KEY_WINDOW_SIZE_RARE_MS, DEFAULT_WINDOW_SIZE_RARE_MS); WINDOW_SIZE_RESTRICTED_MS = @@ -3926,7 +4014,9 @@ public final class QuotaController extends StateController { EJ_LIMIT_ACTIVE_MS = properties.getLong( KEY_EJ_LIMIT_ACTIVE_MS, DEFAULT_EJ_LIMIT_ACTIVE_MS); EJ_LIMIT_WORKING_MS = properties.getLong( - KEY_EJ_LIMIT_WORKING_MS, DEFAULT_EJ_LIMIT_WORKING_MS); + KEY_EJ_LIMIT_WORKING_MS, Flags.adjustQuotaDefaultConstants() + ? DEFAULT_CURRENT_EJ_LIMIT_WORKING_MS : + DEFAULT_LEGACY_EJ_LIMIT_WORKING_MS); EJ_LIMIT_FREQUENT_MS = properties.getLong( KEY_EJ_LIMIT_FREQUENT_MS, DEFAULT_EJ_LIMIT_FREQUENT_MS); EJ_LIMIT_RARE_MS = properties.getLong( @@ -4289,6 +4379,13 @@ public final class QuotaController extends StateController { @Override public void dumpControllerStateLocked(final IndentingPrintWriter pw, final Predicate<JobStatus> predicate) { + pw.println("Flags: "); + pw.println(" " + Flags.FLAG_ADJUST_QUOTA_DEFAULT_CONSTANTS + + ": " + Flags.adjustQuotaDefaultConstants()); + pw.println(" " + Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_FGS_JOBS + + ": " + Flags.enforceQuotaPolicyToFgsJobs()); + pw.println(); + pw.println("Current elapsed time: " + sElapsedRealtimeClock.millis()); pw.println(); diff --git a/core/api/current.txt b/core/api/current.txt index f59b57528ef6..14a91c9033c5 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -8781,7 +8781,6 @@ package android.app.admin { package android.app.appfunctions { @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public final class AppFunctionManager { - method @Deprecated @RequiresPermission(anyOf={"android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED", "android.permission.EXECUTE_APP_FUNCTIONS"}, conditional=true) public void executeAppFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>); method @RequiresPermission(anyOf={"android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED", "android.permission.EXECUTE_APP_FUNCTIONS"}, conditional=true) public void executeAppFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>); method public void isAppFunctionEnabled(@NonNull String, @NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,java.lang.Exception>); method public void setAppFunctionEnabled(@NonNull String, int, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,java.lang.Exception>); @@ -8793,9 +8792,7 @@ package android.app.appfunctions { @FlaggedApi("android.app.appfunctions.flags.enable_app_function_manager") public abstract class AppFunctionService extends android.app.Service { ctor public AppFunctionService(); method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent); - method @Deprecated @MainThread public void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>); - method @Deprecated @MainThread public void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>); - method @MainThread public void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull String, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>); + method @MainThread public abstract void onExecuteFunction(@NonNull android.app.appfunctions.ExecuteAppFunctionRequest, @NonNull String, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<android.app.appfunctions.ExecuteAppFunctionResponse>); field @NonNull public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService"; } diff --git a/core/api/system-current.txt b/core/api/system-current.txt index fa4fc43c3418..349d06ca8ad6 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -4170,9 +4170,11 @@ package android.content.pm { } public class PackageInstaller { + method @FlaggedApi("android.content.pm.verification_service") @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) public final int getVerificationPolicy(); method @NonNull public android.content.pm.PackageInstaller.InstallInfo readInstallInfo(@NonNull java.io.File, int) throws android.content.pm.PackageInstaller.PackageParsingException; method @FlaggedApi("android.content.pm.read_install_info") @NonNull public android.content.pm.PackageInstaller.InstallInfo readInstallInfo(@NonNull android.os.ParcelFileDescriptor, @Nullable String, int) throws android.content.pm.PackageInstaller.PackageParsingException; method @RequiresPermission(android.Manifest.permission.INSTALL_PACKAGES) public void setPermissionsResult(int, boolean); + method @FlaggedApi("android.content.pm.verification_service") @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) public final boolean setVerificationPolicy(int); field public static final String ACTION_CONFIRM_INSTALL = "android.content.pm.action.CONFIRM_INSTALL"; field public static final String ACTION_CONFIRM_PRE_APPROVAL = "android.content.pm.action.CONFIRM_PRE_APPROVAL"; field public static final int DATA_LOADER_TYPE_INCREMENTAL = 2; // 0x2 @@ -4183,12 +4185,20 @@ package android.content.pm { field @FlaggedApi("android.content.pm.archiving") public static final String EXTRA_DELETE_FLAGS = "android.content.pm.extra.DELETE_FLAGS"; field public static final String EXTRA_LEGACY_STATUS = "android.content.pm.extra.LEGACY_STATUS"; field @Deprecated public static final String EXTRA_RESOLVED_BASE_PATH = "android.content.pm.extra.RESOLVED_BASE_PATH"; + field @FlaggedApi("android.content.pm.verification_service") public static final String EXTRA_VERIFICATION_FAILURE_REASON = "android.content.pm.extra.VERIFICATION_FAILURE_REASON"; field public static final int LOCATION_DATA_APP = 0; // 0x0 field public static final int LOCATION_MEDIA_DATA = 2; // 0x2 field public static final int LOCATION_MEDIA_OBB = 1; // 0x1 field public static final int REASON_CONFIRM_PACKAGE_CHANGE = 0; // 0x0 field public static final int REASON_OWNERSHIP_CHANGED = 1; // 0x1 field public static final int REASON_REMIND_OWNERSHIP = 2; // 0x2 + field @FlaggedApi("android.content.pm.verification_service") public static final int VERIFICATION_FAILED_REASON_NETWORK_UNAVAILABLE = 1; // 0x1 + field @FlaggedApi("android.content.pm.verification_service") public static final int VERIFICATION_FAILED_REASON_PACKAGE_BLOCKED = 2; // 0x2 + field @FlaggedApi("android.content.pm.verification_service") public static final int VERIFICATION_FAILED_REASON_UNKNOWN = 0; // 0x0 + field @FlaggedApi("android.content.pm.verification_service") public static final int VERIFICATION_POLICY_BLOCK_FAIL_CLOSED = 3; // 0x3 + field @FlaggedApi("android.content.pm.verification_service") public static final int VERIFICATION_POLICY_BLOCK_FAIL_OPEN = 1; // 0x1 + field @FlaggedApi("android.content.pm.verification_service") public static final int VERIFICATION_POLICY_BLOCK_FAIL_WARN = 2; // 0x2 + field @FlaggedApi("android.content.pm.verification_service") public static final int VERIFICATION_POLICY_NONE = 0; // 0x0 } public static class PackageInstaller.InstallInfo { @@ -4635,12 +4645,13 @@ package android.content.pm.verify.pkg { method @NonNull public android.content.pm.SigningInfo getSigningInfo(); method @NonNull public android.net.Uri getStagedPackageUri(); method @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) public long getTimeoutTime(); + method public int getVerificationPolicy(); method @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) public void reportVerificationComplete(@NonNull android.content.pm.verify.pkg.VerificationStatus); method @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) public void reportVerificationComplete(@NonNull android.content.pm.verify.pkg.VerificationStatus, @NonNull android.os.PersistableBundle); method @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) public void reportVerificationIncomplete(int); + method @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) public boolean setVerificationPolicy(int); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.content.pm.verify.pkg.VerificationSession> CREATOR; - field public static final int VERIFICATION_INCOMPLETE_NETWORK_LIMITED = 2; // 0x2 field public static final int VERIFICATION_INCOMPLETE_NETWORK_UNAVAILABLE = 1; // 0x1 field public static final int VERIFICATION_INCOMPLETE_UNKNOWN = 0; // 0x0 } diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index 7273e64846c0..36fc65a76d53 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -1031,7 +1031,9 @@ public class ActivityManager { | PROCESS_CAPABILITY_FOREGROUND_AUDIO_CONTROL; /** - * All implicit capabilities. There are capabilities that process automatically have. + * All implicit capabilities. This capability set is currently only used for processes under + * active instrumentation. The intent is to allow CTS tests to always have these capabilities + * so that every test doesn't need to launch FGS. * @hide */ @TestApi diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java index 3bd121a4a19b..f80121d0c9b6 100644 --- a/core/java/android/app/ActivityManagerInternal.java +++ b/core/java/android/app/ActivityManagerInternal.java @@ -1328,7 +1328,8 @@ public abstract class ActivityManagerInternal { * Add a creator token for all embedded intents (stored as extra) of the given intent. * * @param intent The given intent + * @param creatorPackage the package name of the creator app. * @hide */ - public abstract void addCreatorToken(Intent intent); + public abstract void addCreatorToken(Intent intent, String creatorPackage); } diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 99625ac20e60..e7f4dbc24022 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -7346,6 +7346,8 @@ public final class ActivityThread extends ClientTransactionHandler } } + VMDebug.setUserId(UserHandle.myUserId()); + VMDebug.addApplication(data.appInfo.packageName); // send up app name; do this *before* waiting for debugger Process.setArgV0(data.processName); android.ddm.DdmHandleAppName.setAppName(data.processName, @@ -7868,9 +7870,20 @@ public final class ActivityThread extends ClientTransactionHandler file.getParentFile().mkdirs(); Debug.startMethodTracing(file.toString(), 8 * 1024 * 1024); } + + if (ii.packageName != null) { + VMDebug.addApplication(ii.packageName); + } } private void handleFinishInstrumentationWithoutRestart() { + LoadedApk loadedApk = getApplication().mLoadedApk; + // Only remove instrumentation app if this was not a self-testing app. + if (mInstrumentationPackageName != null && loadedApk != null && !mInstrumentationPackageName + .equals(loadedApk.mPackageName)) { + VMDebug.removeApplication(mInstrumentationPackageName); + } + mInstrumentation.onDestroy(); mInstrumentationPackageName = null; mInstrumentationAppDir = null; @@ -8904,6 +8917,11 @@ public final class ActivityThread extends ClientTransactionHandler return false; } + void addApplication(@NonNull Application app) { + mAllApplications.add(app); + VMDebug.addApplication(app.mLoadedApk.mPackageName); + } + @Override public boolean isInDensityCompatMode() { return mDensityCompatMode; diff --git a/core/java/android/app/LoadedApk.java b/core/java/android/app/LoadedApk.java index 1df8f63aa402..1e45d6fd1674 100644 --- a/core/java/android/app/LoadedApk.java +++ b/core/java/android/app/LoadedApk.java @@ -1478,7 +1478,7 @@ public final class LoadedApk { + " package " + mPackageName + ": " + e.toString(), e); } } - mActivityThread.mAllApplications.add(app); + mActivityThread.addApplication(app); mApplication = app; if (!allowDuplicateInstances) { synchronized (sApplications) { diff --git a/core/java/android/app/PropertyInvalidatedCache.java b/core/java/android/app/PropertyInvalidatedCache.java index 55296ebbf18e..c17da249f322 100644 --- a/core/java/android/app/PropertyInvalidatedCache.java +++ b/core/java/android/app/PropertyInvalidatedCache.java @@ -16,8 +16,6 @@ package android.app; -import static android.text.TextUtils.formatSimple; - import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.TestApi; @@ -32,10 +30,11 @@ import android.text.TextUtils; import android.util.Log; import com.android.internal.annotations.GuardedBy; -import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.BackgroundThread; import com.android.internal.util.FastPrintWriter; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.io.ByteArrayOutputStream; import java.io.FileOutputStream; import java.io.IOException; @@ -43,14 +42,12 @@ import java.io.PrintWriter; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; -import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; import java.util.Objects; import java.util.Random; import java.util.Set; import java.util.WeakHashMap; -import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicLong; /** @@ -227,24 +224,12 @@ public class PropertyInvalidatedCache<Query, Result> { } /** - * Reserved nonce values. Use isReservedNonce() to test for a reserved value. Note that all - * reserved values cause the cache to be skipped. + * Reserved nonce values. Use isReservedNonce() to test for a reserved value. Note + * that all values cause the cache to be skipped. */ - // This is the initial value of all cache keys. It is changed when a cache is invalidated. private static final int NONCE_UNSET = 0; - // This value is used in two ways. First, it is used internally to indicate that the cache is - // disabled for the current query. Secondly, it is used to global disable the cache across the - // entire system. Once a cache is disabled, there is no way to enable it again. The global - // behavior is unused and will likely be removed in the future. private static final int NONCE_DISABLED = 1; - // The cache is corked, which means that clients must act as though the cache is always - // invalid. This is used when the server is processing updates that continuously invalidate - // caches. Rather than issuing individual invalidations (which has a performance penalty), - // the server corks the caches at the start of the process and uncorks at the end of the - // process. private static final int NONCE_CORKED = 2; - // The cache is bypassed for the current query. Unlike UNSET and CORKED, this value is never - // written to global store. private static final int NONCE_BYPASS = 3; private static boolean isReservedNonce(long n) { @@ -252,7 +237,7 @@ public class PropertyInvalidatedCache<Query, Result> { } /** - * The names of the reserved nonces. + * The names of the nonces */ private static final String[] sNonceName = new String[]{ "unset", "disabled", "corked", "bypass" }; @@ -292,17 +277,32 @@ public class PropertyInvalidatedCache<Query, Result> { private static final Object sCorkLock = new Object(); /** + * Record the number of invalidate or cork calls that were nops because the cache was already + * corked. This is static because invalidation is done in a static context. Entries are + * indexed by the cache property. + */ + @GuardedBy("sCorkLock") + private static final HashMap<String, Long> sCorkedInvalidates = new HashMap<>(); + + /** + * A map of cache keys that we've "corked". (The values are counts.) When a cache key is + * corked, we skip the cache invalidate when the cache key is in the unset state --- that + * is, when a cache key is corked, an invalidation does not enable the cache if somebody + * else hasn't disabled it. + */ + @GuardedBy("sCorkLock") + private static final HashMap<String, Integer> sCorks = new HashMap<>(); + + /** * A lock for the global list of caches and cache keys. This must never be taken inside mLock * or sCorkLock. */ private static final Object sGlobalLock = new Object(); /** - * A map of cache keys that have been disabled in the local process. When a key is disabled - * locally, existing caches are disabled and the key is saved in this map. Future cache - * instances that use the same key will be disabled in their constructor. Note that "disabled" - * means the cache is not used in this process. Invalidation still proceeds normally, because - * the cache may be used in other processes. + * A map of cache keys that have been disabled in the local process. When a key is + * disabled locally, existing caches are disabled and the key is saved in this map. + * Future cache instances that use the same key will be disabled in their constructor. */ @GuardedBy("sGlobalLock") private static final HashSet<String> sDisabledKeys = new HashSet<>(); @@ -315,6 +315,14 @@ public class PropertyInvalidatedCache<Query, Result> { private static final WeakHashMap<PropertyInvalidatedCache, Void> sCaches = new WeakHashMap<>(); /** + * Counts of the number of times a cache key was invalidated. Invalidation occurs in a static + * context with no cache object available, so this is a static map. Entries are indexed by + * the cache property. + */ + @GuardedBy("sGlobalLock") + private static final HashMap<String, Long> sInvalidates = new HashMap<>(); + + /** * If sEnabled is false then all cache operations are stubbed out. Set * it to false inside test processes. */ @@ -326,6 +334,12 @@ public class PropertyInvalidatedCache<Query, Result> { private final String mPropertyName; /** + * Handle to the {@code mPropertyName} property, transitioning to non-{@code null} once the + * property exists on the system. + */ + private volatile SystemProperties.Handle mPropertyHandle; + + /** * The name by which this cache is known. This should normally be the * binder call that is being cached, but the constructors default it to * the property name. @@ -355,13 +369,7 @@ public class PropertyInvalidatedCache<Query, Result> { private final LinkedHashMap<Query, Result> mCache; /** - * The nonce handler for this cache. - */ - @GuardedBy("mLock") - private final NonceHandler mNonce; - - /** - * The last nonce value that was observed. + * The last value of the {@code mPropertyHandle} that we observed. */ @GuardedBy("mLock") private long mLastSeenNonce = NONCE_UNSET; @@ -377,297 +385,6 @@ public class PropertyInvalidatedCache<Query, Result> { private final int mMaxEntries; /** - * A class to manage cache keys. There is a single instance of this class for each unique key - * that is shared by all cache instances that use that key. This class is abstract; subclasses - * use different storage mechanisms for the nonces. - */ - private static abstract class NonceHandler { - // The name of the nonce. - final String mName; - - // A lock to synchronize corking and invalidation. - protected final Object mLock = new Object(); - - // Count the number of times the property name was invalidated. - @GuardedBy("mLock") - private int mInvalidated = 0; - - // Count the number of times invalidate or cork calls were nops because the cache was - // already corked. - @GuardedBy("mLock") - private int mCorkedInvalidates = 0; - - // Count the number of corks against this property name. This is not a statistic. It - // increases when the property is corked and decreases when the property is uncorked. - // Invalidation requests are ignored when the cork count is greater than zero. - @GuardedBy("mLock") - private int mCorks = 0; - - // The methods to get and set a nonce from whatever storage is being used. - abstract long getNonce(); - abstract void setNonce(long value); - - NonceHandler(@NonNull String name) { - mName = name; - } - - /** - * Write the invalidation nonce for the property. - */ - void invalidate() { - if (!sEnabled) { - if (DEBUG) { - Log.d(TAG, formatSimple("cache invalidate %s suppressed", mName)); - } - return; - } - - synchronized (mLock) { - if (mCorks > 0) { - if (DEBUG) { - Log.d(TAG, "ignoring invalidation due to cork: " + mName); - } - mCorkedInvalidates++; - return; - } - - final long nonce = getNonce(); - if (nonce == NONCE_DISABLED) { - if (DEBUG) { - Log.d(TAG, "refusing to invalidate disabled cache: " + mName); - } - return; - } - - long newValue; - do { - newValue = NoPreloadHolder.next(); - } while (isReservedNonce(newValue)); - if (DEBUG) { - Log.d(TAG, formatSimple( - "invalidating cache [%s]: [%s] -> [%s]", - mName, nonce, Long.toString(newValue))); - } - // There is a small race with concurrent disables here. A compare-and-exchange - // property operation would be required to eliminate the race condition. - setNonce(newValue); - mInvalidated++; - } - } - - void cork() { - if (!sEnabled) { - if (DEBUG) { - Log.d(TAG, formatSimple("cache corking %s suppressed", mName)); - } - return; - } - - synchronized (mLock) { - int numberCorks = mCorks; - if (DEBUG) { - Log.d(TAG, formatSimple( - "corking %s: numberCorks=%s", mName, numberCorks)); - } - - // If we're the first ones to cork this cache, set the cache to the corked state so - // existing caches talk directly to their services while we've corked updates. - // Make sure we don't clobber a disabled cache value. - - // TODO: we can skip this property write and leave the cache enabled if the - // caller promises not to make observable changes to the cache backing state before - // uncorking the cache, e.g., by holding a read lock across the cork-uncork pair. - // Implement this more dangerous mode of operation if necessary. - if (numberCorks == 0) { - final long nonce = getNonce(); - if (nonce != NONCE_UNSET && nonce != NONCE_DISABLED) { - setNonce(NONCE_CORKED); - } - } else { - mCorkedInvalidates++; - } - mCorks++; - if (DEBUG) { - Log.d(TAG, "corked: " + mName); - } - } - } - - void uncork() { - if (!sEnabled) { - if (DEBUG) { - Log.d(TAG, formatSimple("cache uncorking %s suppressed", mName)); - } - return; - } - - synchronized (mLock) { - int numberCorks = --mCorks; - if (DEBUG) { - Log.d(TAG, formatSimple( - "uncorking %s: numberCorks=%s", mName, numberCorks)); - } - - if (numberCorks < 0) { - throw new AssertionError("cork underflow: " + mName); - } - if (numberCorks == 0) { - // The property is fully uncorked and can be invalidated normally. - invalidate(); - if (DEBUG) { - Log.d(TAG, "uncorked: " + mName); - } - } - } - } - - void disable() { - if (!sEnabled) { - return; - } - synchronized (mLock) { - setNonce(NONCE_DISABLED); - } - } - - record Stats(int invalidated, int corkedInvalidates) {} - Stats getStats() { - synchronized (mLock) { - return new Stats(mInvalidated, mCorkedInvalidates); - } - } - } - - /** - * Manage nonces that are stored in a system property. - */ - private static final class NonceSysprop extends NonceHandler { - // A handle to the property, for fast lookups. - private volatile SystemProperties.Handle mHandle; - - NonceSysprop(@NonNull String name) { - super(name); - } - - @Override - long getNonce() { - if (mHandle == null) { - synchronized (mLock) { - mHandle = SystemProperties.find(mName); - if (mHandle == null) { - return NONCE_UNSET; - } - } - } - return mHandle.getLong(NONCE_UNSET); - } - - @Override - void setNonce(long value) { - // Failing to set the nonce is a fatal error. Failures setting a system property have - // been reported; given that the failure is probably transient, this function includes - // a retry. - final String str = Long.toString(value); - RuntimeException failure = null; - for (int attempt = 0; attempt < PROPERTY_FAILURE_RETRY_LIMIT; attempt++) { - try { - SystemProperties.set(mName, str); - if (attempt > 0) { - // This log is not guarded. Based on known bug reports, it should - // occur once a week or less. The purpose of the log message is to - // identify the retries as a source of delay that might be otherwise - // be attributed to the cache itself. - Log.w(TAG, "Nonce set after " + attempt + " tries"); - } - return; - } catch (RuntimeException e) { - if (failure == null) { - failure = e; - } - try { - Thread.sleep(PROPERTY_FAILURE_RETRY_DELAY_MILLIS); - } catch (InterruptedException x) { - // Ignore this exception. The desired delay is only approximate and - // there is no issue if the sleep sometimes terminates early. - } - } - } - // This point is reached only if SystemProperties.set() fails at least once. - // Rethrow the first exception that was received. - throw failure; - } - } - - /** - * SystemProperties and shared storage are protected and cannot be written by random - * processes. So, for testing purposes, the NonceTest handler stores the nonce locally. - */ - private static class NonceTest extends NonceHandler { - // The saved nonce. - private long mValue; - - // If this flag is false, the handler has been shutdown during a test. Access to the - // handler in this state is an error. - private boolean mIsActive = true; - - NonceTest(@NonNull String name) { - super(name); - } - - void shutdown() { - // The handler has been discarded as part of test cleanup. Further access is an - // error. - mIsActive = false; - } - - @Override - long getNonce() { - if (!mIsActive) { - throw new IllegalStateException("handler " + mName + " is shutdown"); - } - return mValue; - } - - @Override - void setNonce(long value) { - if (!mIsActive) { - throw new IllegalStateException("handler " + mName + " is shutdown"); - } - mValue = value; - } - } - - /** - * A static list of nonce handlers, indexed by name. NonceHandlers can be safely shared by - * multiple threads, and can therefore be shared by multiple instances of the same cache, and - * with static calls (see {@link #invalidateCache}. Addition and removal are guarded by the - * global lock, to ensure that duplicates are not created. - */ - private static final ConcurrentHashMap<String, NonceHandler> sHandlers - = new ConcurrentHashMap<>(); - - /** - * Return the proper nonce handler, based on the property name. - */ - private static NonceHandler getNonceHandler(@NonNull String name) { - NonceHandler h = sHandlers.get(name); - if (h == null) { - synchronized (sGlobalLock) { - h = sHandlers.get(name); - if (h == null) { - if (name.startsWith("cache_key.test.")) { - h = new NonceTest(name); - } else { - h = new NonceSysprop(name); - } - sHandlers.put(name, h); - } - } - } - return h; - } - - /** * Make a new property invalidated cache. This constructor names the cache after the * property name. New clients should prefer the constructor that takes an explicit * cache name. @@ -700,7 +417,6 @@ public class PropertyInvalidatedCache<Query, Result> { mPropertyName = propertyName; validateCacheKey(mPropertyName); mCacheName = cacheName; - mNonce = getNonceHandler(mPropertyName); mMaxEntries = maxEntries; mComputer = new DefaultComputer<>(this); mCache = createMap(); @@ -725,7 +441,6 @@ public class PropertyInvalidatedCache<Query, Result> { mPropertyName = createPropertyName(module, api); validateCacheKey(mPropertyName); mCacheName = cacheName; - mNonce = getNonceHandler(mPropertyName); mMaxEntries = maxEntries; mComputer = computer; mCache = createMap(); @@ -769,58 +484,130 @@ public class PropertyInvalidatedCache<Query, Result> { } /** - * Enable or disable testing. At this time, no action is taken when testing begins. + * SystemProperties are protected and cannot be written (or read, usually) by random + * processes. So, for testing purposes, the methods have a bypass mode that reads and + * writes to a HashMap and does not go out to the SystemProperties at all. + */ + + // If true, the cache might be under test. If false, there is no testing in progress. + private static volatile boolean sTesting = false; + + // If sTesting is true then keys that are under test are in this map. + private static final HashMap<String, Long> sTestingPropertyMap = new HashMap<>(); + + /** + * Enable or disable testing. The testing property map is cleared every time this + * method is called. * @hide */ @TestApi public static void setTestMode(boolean mode) { - if (mode) { - // No action when testing begins. - } else { - resetAfterTest(); + sTesting = mode; + synchronized (sTestingPropertyMap) { + sTestingPropertyMap.clear(); } } /** - * Enable testing the specific cache key. This is a legacy API that will be removed as part of - * b/360897450. - * @hide + * Enable testing the specific cache key. Only keys in the map are subject to testing. + * There is no method to stop testing a property name. Just disable the test mode. */ - @TestApi - public void testPropertyName() { + private static void testPropertyName(@NonNull String name) { + synchronized (sTestingPropertyMap) { + sTestingPropertyMap.put(name, (long) NONCE_UNSET); + } } /** - * Clean up when testing ends. All NonceTest handlers are erased from the global list and are - * poisoned, just in case the test program has retained a handle to one of the associated - * caches. + * Enable testing the specific cache key. Only keys in the map are subject to testing. + * There is no method to stop testing a property name. Just disable the test mode. * @hide */ - @VisibleForTesting - public static void resetAfterTest() { - synchronized (sGlobalLock) { - for (Iterator<String> e = sHandlers.keys().asIterator(); e.hasNext(); ) { - String s = e.next(); - final NonceHandler h = sHandlers.get(s); - if (h instanceof NonceTest t) { - t.shutdown(); - sHandlers.remove(s); + @TestApi + public void testPropertyName() { + testPropertyName(mPropertyName); + } + + // Read the system property associated with the current cache. This method uses the + // handle for faster reading. + private long getCurrentNonce() { + if (sTesting) { + synchronized (sTestingPropertyMap) { + Long n = sTestingPropertyMap.get(mPropertyName); + if (n != null) { + return n; + } + } + } + + SystemProperties.Handle handle = mPropertyHandle; + if (handle == null) { + handle = SystemProperties.find(mPropertyName); + if (handle == null) { + return NONCE_UNSET; + } + mPropertyHandle = handle; + } + return handle.getLong(NONCE_UNSET); + } + + // Write the nonce in a static context. No handle is available. + private static void setNonce(String name, long val) { + if (sTesting) { + synchronized (sTestingPropertyMap) { + Long n = sTestingPropertyMap.get(name); + if (n != null) { + sTestingPropertyMap.put(name, val); + return; } } } + RuntimeException failure = null; + for (int attempt = 0; attempt < PROPERTY_FAILURE_RETRY_LIMIT; attempt++) { + try { + SystemProperties.set(name, Long.toString(val)); + if (attempt > 0) { + // This log is not guarded. Based on known bug reports, it should + // occur once a week or less. The purpose of the log message is to + // identify the retries as a source of delay that might be otherwise + // be attributed to the cache itself. + Log.w(TAG, "Nonce set after " + attempt + " tries"); + } + return; + } catch (RuntimeException e) { + if (failure == null) { + failure = e; + } + try { + Thread.sleep(PROPERTY_FAILURE_RETRY_DELAY_MILLIS); + } catch (InterruptedException x) { + // Ignore this exception. The desired delay is only approximate and + // there is no issue if the sleep sometimes terminates early. + } + } + } + // This point is reached only if SystemProperties.set() fails at least once. + // Rethrow the first exception that was received. + throw failure; } - // Read the nonce associated with the current cache. - @GuardedBy("mLock") - private long getCurrentNonce() { - return mNonce.getNonce(); + // Set the nonce in a static context. No handle is available. + private static long getNonce(String name) { + if (sTesting) { + synchronized (sTestingPropertyMap) { + Long n = sTestingPropertyMap.get(name); + if (n != null) { + return n; + } + } + } + return SystemProperties.getLong(name, NONCE_UNSET); } /** - * Forget all cached values. This is used by a client when the server exits. Since the - * server has exited, the cache values are no longer valid, but the server is no longer - * present to invalidate the cache. Note that this is not necessary if the server is - * system_server, because the entire operating system reboots if that process exits. + * Forget all cached values. + * TODO(216112648) remove this as a public API. Clients should invalidate caches, not clear + * them. * @hide */ public final void clear() { @@ -887,7 +674,7 @@ public class PropertyInvalidatedCache<Query, Result> { } /** - * Disable the use of this cache in this process. This method is used internally and during + * Disable the use of this cache in this process. This method is using internally and during * testing. To disable a cache in normal code, use disableLocal(). A disabled cache cannot * be re-enabled. * @hide @@ -996,7 +783,7 @@ public class PropertyInvalidatedCache<Query, Result> { if (DEBUG) { if (!mDisabled) { - Log.d(TAG, formatSimple( + Log.d(TAG, TextUtils.formatSimple( "cache %s %s for %s", cacheName(), sNonceName[(int) currentNonce], queryToString(query))); } @@ -1011,7 +798,7 @@ public class PropertyInvalidatedCache<Query, Result> { if (cachedResult != null) mHits++; } else { if (DEBUG) { - Log.d(TAG, formatSimple( + Log.d(TAG, TextUtils.formatSimple( "clearing cache %s of %d entries because nonce changed [%s] -> [%s]", cacheName(), mCache.size(), mLastSeenNonce, currentNonce)); @@ -1037,7 +824,7 @@ public class PropertyInvalidatedCache<Query, Result> { if (currentNonce != afterRefreshNonce) { currentNonce = afterRefreshNonce; if (DEBUG) { - Log.d(TAG, formatSimple( + Log.d(TAG, TextUtils.formatSimple( "restarting %s %s because nonce changed in refresh", cacheName(), queryToString(query))); @@ -1108,7 +895,10 @@ public class PropertyInvalidatedCache<Query, Result> { * @param name Name of the cache-key property to invalidate */ private static void disableSystemWide(@NonNull String name) { - getNonceHandler(name).disable(); + if (!sEnabled) { + return; + } + setNonce(name, NONCE_DISABLED); } /** @@ -1118,7 +908,7 @@ public class PropertyInvalidatedCache<Query, Result> { */ @TestApi public void invalidateCache() { - mNonce.invalidate(); + invalidateCache(mPropertyName); } /** @@ -1141,7 +931,59 @@ public class PropertyInvalidatedCache<Query, Result> { * @hide */ public static void invalidateCache(@NonNull String name) { - getNonceHandler(name).invalidate(); + if (!sEnabled) { + if (DEBUG) { + Log.w(TAG, TextUtils.formatSimple( + "cache invalidate %s suppressed", name)); + } + return; + } + + // Take the cork lock so invalidateCache() racing against corkInvalidations() doesn't + // clobber a cork-written NONCE_UNSET with a cache key we compute before the cork. + // The property service is single-threaded anyway, so we don't lose any concurrency by + // taking the cork lock around cache invalidations. If we see contention on this lock, + // we're invalidating too often. + synchronized (sCorkLock) { + Integer numberCorks = sCorks.get(name); + if (numberCorks != null && numberCorks > 0) { + if (DEBUG) { + Log.d(TAG, "ignoring invalidation due to cork: " + name); + } + final long count = sCorkedInvalidates.getOrDefault(name, (long) 0); + sCorkedInvalidates.put(name, count + 1); + return; + } + invalidateCacheLocked(name); + } + } + + @GuardedBy("sCorkLock") + private static void invalidateCacheLocked(@NonNull String name) { + // There's no race here: we don't require that values strictly increase, but instead + // only that each is unique in a single runtime-restart session. + final long nonce = getNonce(name); + if (nonce == NONCE_DISABLED) { + if (DEBUG) { + Log.d(TAG, "refusing to invalidate disabled cache: " + name); + } + return; + } + + long newValue; + do { + newValue = NoPreloadHolder.next(); + } while (isReservedNonce(newValue)); + if (DEBUG) { + Log.d(TAG, TextUtils.formatSimple( + "invalidating cache [%s]: [%s] -> [%s]", + name, nonce, Long.toString(newValue))); + } + // There is a small race with concurrent disables here. A compare-and-exchange + // property operation would be required to eliminate the race condition. + setNonce(name, newValue); + long invalidateCount = sInvalidates.getOrDefault(name, (long) 0); + sInvalidates.put(name, ++invalidateCount); } /** @@ -1158,7 +1000,43 @@ public class PropertyInvalidatedCache<Query, Result> { * @hide */ public static void corkInvalidations(@NonNull String name) { - getNonceHandler(name).cork(); + if (!sEnabled) { + if (DEBUG) { + Log.w(TAG, TextUtils.formatSimple( + "cache cork %s suppressed", name)); + } + return; + } + + synchronized (sCorkLock) { + int numberCorks = sCorks.getOrDefault(name, 0); + if (DEBUG) { + Log.d(TAG, TextUtils.formatSimple( + "corking %s: numberCorks=%s", name, numberCorks)); + } + + // If we're the first ones to cork this cache, set the cache to the corked state so + // existing caches talk directly to their services while we've corked updates. + // Make sure we don't clobber a disabled cache value. + + // TODO(dancol): we can skip this property write and leave the cache enabled if the + // caller promises not to make observable changes to the cache backing state before + // uncorking the cache, e.g., by holding a read lock across the cork-uncork pair. + // Implement this more dangerous mode of operation if necessary. + if (numberCorks == 0) { + final long nonce = getNonce(name); + if (nonce != NONCE_UNSET && nonce != NONCE_DISABLED) { + setNonce(name, NONCE_CORKED); + } + } else { + final long count = sCorkedInvalidates.getOrDefault(name, (long) 0); + sCorkedInvalidates.put(name, count + 1); + } + sCorks.put(name, numberCorks + 1); + if (DEBUG) { + Log.d(TAG, "corked: " + name); + } + } } /** @@ -1170,7 +1048,34 @@ public class PropertyInvalidatedCache<Query, Result> { * @hide */ public static void uncorkInvalidations(@NonNull String name) { - getNonceHandler(name).uncork(); + if (!sEnabled) { + if (DEBUG) { + Log.w(TAG, TextUtils.formatSimple( + "cache uncork %s suppressed", name)); + } + return; + } + + synchronized (sCorkLock) { + int numberCorks = sCorks.getOrDefault(name, 0); + if (DEBUG) { + Log.d(TAG, TextUtils.formatSimple( + "uncorking %s: numberCorks=%s", name, numberCorks)); + } + + if (numberCorks < 1) { + throw new AssertionError("cork underflow: " + name); + } + if (numberCorks == 1) { + sCorks.remove(name); + invalidateCacheLocked(name); + if (DEBUG) { + Log.d(TAG, "uncorked: " + name); + } + } else { + sCorks.put(name, numberCorks - 1); + } + } } /** @@ -1199,8 +1104,6 @@ public class PropertyInvalidatedCache<Query, Result> { @GuardedBy("mLock") private Handler mHandler; - private NonceHandler mNonce; - public AutoCorker(@NonNull String propertyName) { this(propertyName, DEFAULT_AUTO_CORK_DELAY_MS); } @@ -1214,35 +1117,31 @@ public class PropertyInvalidatedCache<Query, Result> { } public void autoCork() { - synchronized (mLock) { - if (mNonce == null) { - mNonce = getNonceHandler(mPropertyName); - } - } - if (getLooper() == null) { // We're not ready to auto-cork yet, so just invalidate the cache immediately. if (DEBUG) { Log.w(TAG, "invalidating instead of autocorking early in init: " + mPropertyName); } - mNonce.invalidate(); + PropertyInvalidatedCache.invalidateCache(mPropertyName); return; } synchronized (mLock) { boolean alreadyQueued = mUncorkDeadlineMs >= 0; if (DEBUG) { - Log.d(TAG, formatSimple( + Log.w(TAG, TextUtils.formatSimple( "autoCork %s mUncorkDeadlineMs=%s", mPropertyName, mUncorkDeadlineMs)); } mUncorkDeadlineMs = SystemClock.uptimeMillis() + mAutoCorkDelayMs; if (!alreadyQueued) { getHandlerLocked().sendEmptyMessageAtTime(0, mUncorkDeadlineMs); - mNonce.cork(); + PropertyInvalidatedCache.corkInvalidations(mPropertyName); } else { - // Count this as a corked invalidation. - mNonce.invalidate(); + synchronized (sCorkLock) { + final long count = sCorkedInvalidates.getOrDefault(mPropertyName, (long) 0); + sCorkedInvalidates.put(mPropertyName, count + 1); + } } } } @@ -1250,7 +1149,7 @@ public class PropertyInvalidatedCache<Query, Result> { private void handleMessage(Message msg) { synchronized (mLock) { if (DEBUG) { - Log.d(TAG, formatSimple( + Log.w(TAG, TextUtils.formatSimple( "handleMsesage %s mUncorkDeadlineMs=%s", mPropertyName, mUncorkDeadlineMs)); } @@ -1262,7 +1161,7 @@ public class PropertyInvalidatedCache<Query, Result> { if (mUncorkDeadlineMs > nowMs) { mUncorkDeadlineMs = nowMs + mAutoCorkDelayMs; if (DEBUG) { - Log.d(TAG, formatSimple( + Log.w(TAG, TextUtils.formatSimple( "scheduling uncork at %s", mUncorkDeadlineMs)); } @@ -1270,10 +1169,10 @@ public class PropertyInvalidatedCache<Query, Result> { return; } if (DEBUG) { - Log.d(TAG, "automatic uncorking " + mPropertyName); + Log.w(TAG, "automatic uncorking " + mPropertyName); } mUncorkDeadlineMs = -1; - mNonce.uncork(); + PropertyInvalidatedCache.uncorkInvalidations(mPropertyName); } } @@ -1308,7 +1207,7 @@ public class PropertyInvalidatedCache<Query, Result> { Result resultToCompare = recompute(query); boolean nonceChanged = (getCurrentNonce() != mLastSeenNonce); if (!nonceChanged && !resultEquals(proposedResult, resultToCompare)) { - Log.e(TAG, formatSimple( + Log.e(TAG, TextUtils.formatSimple( "cache %s inconsistent for %s is %s should be %s", cacheName(), queryToString(query), proposedResult, resultToCompare)); @@ -1385,9 +1284,17 @@ public class PropertyInvalidatedCache<Query, Result> { /** * Returns a list of caches alive at the current time. */ + @GuardedBy("sGlobalLock") private static @NonNull ArrayList<PropertyInvalidatedCache> getActiveCaches() { - synchronized (sGlobalLock) { - return new ArrayList<PropertyInvalidatedCache>(sCaches.keySet()); + return new ArrayList<PropertyInvalidatedCache>(sCaches.keySet()); + } + + /** + * Returns a list of the active corks in a process. + */ + private static @NonNull ArrayList<Map.Entry<String, Integer>> getActiveCorks() { + synchronized (sCorkLock) { + return new ArrayList<Map.Entry<String, Integer>>(sCorks.entrySet()); } } @@ -1454,27 +1361,32 @@ public class PropertyInvalidatedCache<Query, Result> { return; } - NonceHandler.Stats stats = mNonce.getStats(); + long invalidateCount; + long corkedInvalidates; + synchronized (sCorkLock) { + invalidateCount = sInvalidates.getOrDefault(mPropertyName, (long) 0); + corkedInvalidates = sCorkedInvalidates.getOrDefault(mPropertyName, (long) 0); + } synchronized (mLock) { - pw.println(formatSimple(" Cache Name: %s", cacheName())); - pw.println(formatSimple(" Property: %s", mPropertyName)); + pw.println(TextUtils.formatSimple(" Cache Name: %s", cacheName())); + pw.println(TextUtils.formatSimple(" Property: %s", mPropertyName)); final long skips = mSkips[NONCE_CORKED] + mSkips[NONCE_UNSET] + mSkips[NONCE_DISABLED] + mSkips[NONCE_BYPASS]; - pw.println(formatSimple( + pw.println(TextUtils.formatSimple( " Hits: %d, Misses: %d, Skips: %d, Clears: %d", mHits, mMisses, skips, mClears)); - pw.println(formatSimple( + pw.println(TextUtils.formatSimple( " Skip-corked: %d, Skip-unset: %d, Skip-bypass: %d, Skip-other: %d", mSkips[NONCE_CORKED], mSkips[NONCE_UNSET], mSkips[NONCE_BYPASS], mSkips[NONCE_DISABLED])); - pw.println(formatSimple( + pw.println(TextUtils.formatSimple( " Nonce: 0x%016x, Invalidates: %d, CorkedInvalidates: %d", - mLastSeenNonce, stats.invalidated, stats.corkedInvalidates)); - pw.println(formatSimple( + mLastSeenNonce, invalidateCount, corkedInvalidates)); + pw.println(TextUtils.formatSimple( " Current Size: %d, Max Size: %d, HW Mark: %d, Overflows: %d", mCache.size(), mMaxEntries, mHighWaterMark, mMissOverflow)); - pw.println(formatSimple(" Enabled: %s", mDisabled ? "false" : "true")); + pw.println(TextUtils.formatSimple(" Enabled: %s", mDisabled ? "false" : "true")); pw.println(""); // No specific cache was requested. This is the default, and no details @@ -1492,7 +1404,23 @@ public class PropertyInvalidatedCache<Query, Result> { String key = Objects.toString(entry.getKey()); String value = Objects.toString(entry.getValue()); - pw.println(formatSimple(" Key: %s\n Value: %s\n", key, value)); + pw.println(TextUtils.formatSimple(" Key: %s\n Value: %s\n", key, value)); + } + } + } + + /** + * Dump the corking status. + */ + @GuardedBy("sCorkLock") + private static void dumpCorkInfo(PrintWriter pw) { + ArrayList<Map.Entry<String, Integer>> activeCorks = getActiveCorks(); + if (activeCorks.size() > 0) { + pw.println(" Corking Status:"); + for (int i = 0; i < activeCorks.size(); i++) { + Map.Entry<String, Integer> entry = activeCorks.get(i); + pw.println(TextUtils.formatSimple(" Property Name: %s Count: %d", + entry.getKey(), entry.getValue())); } } } @@ -1513,7 +1441,14 @@ public class PropertyInvalidatedCache<Query, Result> { // then only that cache is reported. boolean detail = anyDetailed(args); - ArrayList<PropertyInvalidatedCache> activeCaches = getActiveCaches(); + ArrayList<PropertyInvalidatedCache> activeCaches; + synchronized (sGlobalLock) { + activeCaches = getActiveCaches(); + if (!detail) { + dumpCorkInfo(pw); + } + } + for (int i = 0; i < activeCaches.size(); i++) { PropertyInvalidatedCache currentCache = activeCaches.get(i); currentCache.dumpContents(pw, detail, args); diff --git a/core/java/android/app/appfunctions/AppFunctionManager.java b/core/java/android/app/appfunctions/AppFunctionManager.java index 439d988e2588..dca433696fe7 100644 --- a/core/java/android/app/appfunctions/AppFunctionManager.java +++ b/core/java/android/app/appfunctions/AppFunctionManager.java @@ -110,40 +110,6 @@ public final class AppFunctionManager { * * @param request the request to execute the app function * @param executor the executor to run the callback - * @param callback the callback to receive the function execution result. if the calling app - * does not own the app function or does not have {@code - * android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED} or {@code - * android.permission.EXECUTE_APP_FUNCTIONS}, the execution result will contain {@code - * ExecuteAppFunctionResponse.RESULT_DENIED}. - * @deprecated Use {@link #executeAppFunction(ExecuteAppFunctionRequest, Executor, - * CancellationSignal, Consumer)} instead. This method will be removed once usage references - * are updated. - */ - @RequiresPermission( - anyOf = { - Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED, - Manifest.permission.EXECUTE_APP_FUNCTIONS - }, - conditional = true) - @UserHandleAware - @Deprecated - public void executeAppFunction( - @NonNull ExecuteAppFunctionRequest request, - @NonNull @CallbackExecutor Executor executor, - @NonNull Consumer<ExecuteAppFunctionResponse> callback) { - executeAppFunction(request, executor, new CancellationSignal(), callback); - } - - /** - * Executes the app function. - * - * <p>Note: Applications can execute functions they define. To execute functions defined in - * another component, apps would need to have {@code - * android.permission.EXECUTE_APP_FUNCTIONS_TRUSTED} or {@code - * android.permission.EXECUTE_APP_FUNCTIONS}. - * - * @param request the request to execute the app function - * @param executor the executor to run the callback * @param cancellationSignal the cancellation signal to cancel the execution. * @param callback the callback to receive the function execution result. if the calling app * does not own the app function or does not have {@code diff --git a/core/java/android/app/appfunctions/AppFunctionService.java b/core/java/android/app/appfunctions/AppFunctionService.java index ceca850a1037..63d187aa11ef 100644 --- a/core/java/android/app/appfunctions/AppFunctionService.java +++ b/core/java/android/app/appfunctions/AppFunctionService.java @@ -158,74 +158,6 @@ public abstract class AppFunctionService extends Service { * thread and dispatch the result with the given callback. You should always report back the * result using the callback, no matter if the execution was successful or not. * - * @param request The function execution request. - * @param callback A callback to report back the result. - * @deprecated Use {@link #onExecuteFunction(ExecuteAppFunctionRequest, CancellationSignal, - * Consumer)} instead. This method will be removed once usage references are updated. - */ - @MainThread - @Deprecated - public void onExecuteFunction( - @NonNull ExecuteAppFunctionRequest request, - @NonNull Consumer<ExecuteAppFunctionResponse> callback) { - Log.w( - "AppFunctionService", - "Calling deprecated default implementation of onExecuteFunction"); - } - - /** - * Called by the system to execute a specific app function. - * - * <p>This method is triggered when the system requests your AppFunctionService to handle a - * particular function you have registered and made available. - * - * <p>To ensure proper routing of function requests, assign a unique identifier to each - * function. This identifier doesn't need to be globally unique, but it must be unique within - * your app. For example, a function to order food could be identified as "orderFood". In most - * cases this identifier should come from the ID automatically generated by the AppFunctions - * SDK. You can determine the specific function to invoke by calling {@link - * ExecuteAppFunctionRequest#getFunctionIdentifier()}. - * - * <p>This method is always triggered in the main thread. You should run heavy tasks on a worker - * thread and dispatch the result with the given callback. You should always report back the - * result using the callback, no matter if the execution was successful or not. - * - * <p>This method also accepts a {@link CancellationSignal} that the app should listen to cancel - * the execution of function if requested by the system. - * - * @param request The function execution request. - * @param cancellationSignal A signal to cancel the execution. - * @param callback A callback to report back the result. - * @deprecated Use {@link #onExecuteFunction(ExecuteAppFunctionRequest, String, - * CancellationSignal, Consumer)} instead. This method will be removed once usage references - * are updated. - */ - @MainThread - @Deprecated - public void onExecuteFunction( - @NonNull ExecuteAppFunctionRequest request, - @NonNull CancellationSignal cancellationSignal, - @NonNull Consumer<ExecuteAppFunctionResponse> callback) { - onExecuteFunction(request, callback); - } - - /** - * Called by the system to execute a specific app function. - * - * <p>This method is triggered when the system requests your AppFunctionService to handle a - * particular function you have registered and made available. - * - * <p>To ensure proper routing of function requests, assign a unique identifier to each - * function. This identifier doesn't need to be globally unique, but it must be unique within - * your app. For example, a function to order food could be identified as "orderFood". In most - * cases this identifier should come from the ID automatically generated by the AppFunctions - * SDK. You can determine the specific function to invoke by calling {@link - * ExecuteAppFunctionRequest#getFunctionIdentifier()}. - * - * <p>This method is always triggered in the main thread. You should run heavy tasks on a worker - * thread and dispatch the result with the given callback. You should always report back the - * result using the callback, no matter if the execution was successful or not. - * * <p>This method also accepts a {@link CancellationSignal} that the app should listen to cancel * the execution of function if requested by the system. * @@ -235,11 +167,9 @@ public abstract class AppFunctionService extends Service { * @param callback A callback to report back the result. */ @MainThread - public void onExecuteFunction( + public abstract void onExecuteFunction( @NonNull ExecuteAppFunctionRequest request, @NonNull String callingPackage, @NonNull CancellationSignal cancellationSignal, - @NonNull Consumer<ExecuteAppFunctionResponse> callback) { - onExecuteFunction(request, cancellationSignal, callback); - } + @NonNull Consumer<ExecuteAppFunctionResponse> callback); } diff --git a/core/java/android/companion/AssociationInfo.java b/core/java/android/companion/AssociationInfo.java index 7f30d7cccb57..124973489dd1 100644 --- a/core/java/android/companion/AssociationInfo.java +++ b/core/java/android/companion/AssociationInfo.java @@ -287,7 +287,7 @@ public final class AssociationInfo implements Parcelable { /** * Get the device icon of the associated device. The device icon represents the device type. * - * @return the device icon, or {@code null} if no device icon is has been set for the + * @return the device icon, or {@code null} if no device icon has been set for the * associated device. * * @see AssociationRequest.Builder#setDeviceIcon(Icon) diff --git a/core/java/android/companion/AssociationRequest.java b/core/java/android/companion/AssociationRequest.java index 41a6791d8a7b..f368935a74c8 100644 --- a/core/java/android/companion/AssociationRequest.java +++ b/core/java/android/companion/AssociationRequest.java @@ -475,8 +475,8 @@ public final class AssociationRequest implements Parcelable { } /** - * Set the device icon for the self-managed device and this icon will be - * displayed in the self-managed association dialog. + * Set the device icon for the self-managed device and to display the icon in the + * self-managed association dialog. * * @throws IllegalArgumentException if the icon is not exactly 24dp by 24dp * or if it is {@link Icon#TYPE_URI} or {@link Icon#TYPE_URI_ADAPTIVE_BITMAP}. diff --git a/core/java/android/companion/CompanionDeviceManager.java b/core/java/android/companion/CompanionDeviceManager.java index dfad6de4ba16..4472c3d13d7c 100644 --- a/core/java/android/companion/CompanionDeviceManager.java +++ b/core/java/android/companion/CompanionDeviceManager.java @@ -478,6 +478,15 @@ public final class CompanionDeviceManager { Objects.requireNonNull(callback, "Callback cannot be null"); handler = Handler.mainIfNull(handler); + if (Flags.associationDeviceIcon()) { + final Icon deviceIcon = request.getDeviceIcon(); + + if (deviceIcon != null && !isValidIcon(deviceIcon, mContext)) { + throw new IllegalArgumentException("The size of the device icon must be " + + "24dp x 24dp to ensure proper display"); + } + } + try { mService.associate(request, new AssociationRequestCallbackProxy(handler, callback), mContext.getOpPackageName(), mContext.getUserId()); @@ -542,11 +551,13 @@ public final class CompanionDeviceManager { Objects.requireNonNull(executor, "Executor cannot be null"); Objects.requireNonNull(callback, "Callback cannot be null"); - final Icon deviceIcon = request.getDeviceIcon(); + if (Flags.associationDeviceIcon()) { + final Icon deviceIcon = request.getDeviceIcon(); - if (deviceIcon != null && !isValidIcon(deviceIcon, mContext)) { - throw new IllegalArgumentException("The size of the device icon must be 24dp x 24dp to" - + "ensure proper display"); + if (deviceIcon != null && !isValidIcon(deviceIcon, mContext)) { + throw new IllegalArgumentException("The size of the device icon must be " + + "24dp x 24dp to ensure proper display"); + } } try { diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index 91f7a8bae163..628435da3a37 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -5658,6 +5658,14 @@ public abstract class Context { public static final String BINARY_TRANSPARENCY_SERVICE = "transparency"; /** + * System service name for ForensicService. + * The service manages the forensic info on device. + * @hide + */ + @FlaggedApi(android.security.Flags.FLAG_AFL_API) + public static final String FORENSIC_SERVICE = "forensic"; + + /** * System service name for the DeviceIdleManager. * @see #getSystemService(String) * @hide diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java index 0bb0027fb0c3..f71952849872 100644 --- a/core/java/android/content/Intent.java +++ b/core/java/android/content/Intent.java @@ -888,6 +888,22 @@ public class Intent implements Parcelable, Cloneable { public static final String ACTION_ACTIVITY_RECOGNIZER = "android.intent.action.ACTIVITY_RECOGNIZER"; + /** @hide */ + public static void maybeMarkAsMissingCreatorToken(Object object) { + if (object instanceof Intent intent) { + maybeMarkAsMissingCreatorTokenInternal(intent); + } + } + + private static void maybeMarkAsMissingCreatorTokenInternal(Intent intent) { + boolean isForeign = (intent.mLocalFlags & LOCAL_FLAG_FROM_PARCEL) != 0; + boolean isWithoutTrustedCreatorToken = + (intent.mLocalFlags & Intent.LOCAL_FLAG_TRUSTED_CREATOR_TOKEN_PRESENT) == 0; + if (isForeign && isWithoutTrustedCreatorToken) { + intent.addExtendedFlags(EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN); + } + } + /** * Represents a shortcut/live folder icon resource. * @@ -7684,10 +7700,8 @@ public class Intent implements Parcelable, Cloneable { /** * This flag indicates the creator token of this intent has been verified. - * - * @hide */ - public static final int LOCAL_FLAG_CREATOR_TOKEN_VERIFIED = 1 << 6; + private static final int LOCAL_FLAG_TRUSTED_CREATOR_TOKEN_PRESENT = 1 << 6; /** @hide */ @IntDef(flag = true, prefix = { "EXTENDED_FLAG_" }, value = { @@ -12243,6 +12257,30 @@ public class Intent implements Parcelable, Cloneable { } } + /** @hide */ + public void checkCreatorToken() { + if (mExtras == null) return; + if (mCreatorTokenInfo != null && mCreatorTokenInfo.mExtraIntentKeys != null) { + for (String key : mCreatorTokenInfo.mExtraIntentKeys) { + try { + Intent extraIntent = mExtras.getParcelable(key, Intent.class); + if (extraIntent == null) { + Log.w(TAG, "The key {" + key + + "} does not correspond to an intent in the bundle."); + continue; + } + extraIntent.mLocalFlags |= LOCAL_FLAG_TRUSTED_CREATOR_TOKEN_PRESENT; + } catch (Exception e) { + Log.e(TAG, "Failed to validate creator token. key: " + key + ".", e); + } + } + } + // mark the bundle as intent extras after calls to getParcelable. + // otherwise, the logic to mark missing token would run before + // mark trusted creator token present. + mExtras.setIsIntentExtra(); + } + public void writeToParcel(Parcel out, int flags) { out.writeString8(mAction); Uri.writeToParcel(out, mData); @@ -12730,6 +12768,7 @@ public class Intent implements Parcelable, Cloneable { } mLocalFlags |= localFlags; + checkCreatorToken(); // Special attribution fix-up logic for any BluetoothDevice extras // passed via Bluetooth intents diff --git a/core/java/android/content/pm/IPackageInstaller.aidl b/core/java/android/content/pm/IPackageInstaller.aidl index 451c0e5e079a..c911326ccffd 100644 --- a/core/java/android/content/pm/IPackageInstaller.aidl +++ b/core/java/android/content/pm/IPackageInstaller.aidl @@ -93,4 +93,10 @@ interface IPackageInstaller { @JavaPassthrough(annotation="@android.annotation.RequiresPermission(anyOf={android.Manifest.permission.INSTALL_PACKAGES,android.Manifest.permission.REQUEST_INSTALL_PACKAGES})") void reportUnarchivalStatus(int unarchiveId, int status, long requiredStorageBytes, in PendingIntent userActionIntent, in UserHandle userHandle); + + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)") + int getVerificationPolicy(); + + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)") + boolean setVerificationPolicy(int policy); } diff --git a/core/java/android/content/pm/PackageInstaller.java b/core/java/android/content/pm/PackageInstaller.java index c673d5846d5d..cba7bc912666 100644 --- a/core/java/android/content/pm/PackageInstaller.java +++ b/core/java/android/content/pm/PackageInstaller.java @@ -62,6 +62,8 @@ import android.content.pm.parsing.PackageLite; import android.content.pm.parsing.result.ParseResult; import android.content.pm.parsing.result.ParseTypeImpl; import android.content.pm.verify.domain.DomainSet; +import android.content.pm.verify.pkg.VerificationSession; +import android.content.pm.verify.pkg.VerificationStatus; import android.graphics.Bitmap; import android.icu.util.ULocale; import android.net.Uri; @@ -418,6 +420,21 @@ public class PackageInstaller { public static final String EXTRA_WARNINGS = "android.content.pm.extra.WARNINGS"; /** + * When verification is blocked as part of the installation, additional reason for the block + * will be provided to the installer with a {@link VerificationFailedReason} as part of the + * installation result returned via the {@link IntentSender} in + * {@link Session#commit(IntentSender)}. This extra is provided only when the installation has + * failed. Installers can use this extra to check if the installation failure was caused by a + * verification failure. + * + * @hide + */ + @FlaggedApi(Flags.FLAG_VERIFICATION_SERVICE) + @SystemApi + public static final String EXTRA_VERIFICATION_FAILURE_REASON = + "android.content.pm.extra.VERIFICATION_FAILURE_REASON"; + + /** * Streaming installation pending. * Caller should make sure DataLoader is able to prepare image and reinitiate the operation. * @@ -760,6 +777,90 @@ public class PackageInstaller { @Retention(RetentionPolicy.SOURCE) public @interface UnarchivalStatus {} + /** + * Verification failed because of unknown reasons, such as when the verifier times out or cannot + * be connected. It can also corresponds to the status of + * {@link VerificationSession#VERIFICATION_INCOMPLETE_UNKNOWN} reported by the verifier via + * {@link VerificationSession#reportVerificationIncomplete(int)}. + * @hide + */ + @FlaggedApi(Flags.FLAG_VERIFICATION_SERVICE) + @SystemApi + public static final int VERIFICATION_FAILED_REASON_UNKNOWN = 0; + + /** + * Verification failed because the network is unavailable. This corresponds to the status of + * {@link VerificationSession#VERIFICATION_INCOMPLETE_NETWORK_UNAVAILABLE} reported by the + * verifier via {@link VerificationSession#reportVerificationIncomplete(int)}. + * + * @hide + */ + @FlaggedApi(Flags.FLAG_VERIFICATION_SERVICE) + @SystemApi + public static final int VERIFICATION_FAILED_REASON_NETWORK_UNAVAILABLE = 1; + + /** + * Verification failed because the package is blocked, as reported by the verifier via + * {@link VerificationSession#reportVerificationComplete(VerificationStatus)} or + * {@link VerificationSession#reportVerificationComplete(VerificationStatus, PersistableBundle)} + * @hide + */ + @FlaggedApi(Flags.FLAG_VERIFICATION_SERVICE) + @SystemApi + public static final int VERIFICATION_FAILED_REASON_PACKAGE_BLOCKED = 2; + + /** + * @hide + */ + @IntDef(value = { + VERIFICATION_FAILED_REASON_UNKNOWN, + VERIFICATION_FAILED_REASON_NETWORK_UNAVAILABLE, + VERIFICATION_FAILED_REASON_PACKAGE_BLOCKED, + }) + public @interface VerificationFailedReason { + } + + /** + * Do not block installs, regardless of verification status. + * @hide + */ + @FlaggedApi(Flags.FLAG_VERIFICATION_SERVICE) + @SystemApi + public static final int VERIFICATION_POLICY_NONE = 0; // platform default + /** + * Only block installations on {@link #VERIFICATION_FAILED_REASON_PACKAGE_BLOCKED}. + * @hide + */ + @FlaggedApi(Flags.FLAG_VERIFICATION_SERVICE) + @SystemApi + public static final int VERIFICATION_POLICY_BLOCK_FAIL_OPEN = 1; + /** + * Only block installations on {@link #VERIFICATION_FAILED_REASON_PACKAGE_BLOCKED} and ask the + * user if they'd like to install anyway when the verification is blocked for other reason. + * @hide + */ + @FlaggedApi(Flags.FLAG_VERIFICATION_SERVICE) + @SystemApi + public static final int VERIFICATION_POLICY_BLOCK_FAIL_WARN = 2; + /** + * Block installations whose verification status is blocked for any reason. + * @hide + */ + @FlaggedApi(Flags.FLAG_VERIFICATION_SERVICE) + @SystemApi + public static final int VERIFICATION_POLICY_BLOCK_FAIL_CLOSED = 3; + /** + * @hide + */ + @IntDef(value = { + VERIFICATION_POLICY_NONE, + VERIFICATION_POLICY_BLOCK_FAIL_OPEN, + VERIFICATION_POLICY_BLOCK_FAIL_WARN, + VERIFICATION_POLICY_BLOCK_FAIL_CLOSED, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface VerificationPolicy { + } /** Default set of checksums - includes all available checksums. * @see Session#requestChecksums */ @@ -1503,6 +1604,40 @@ public class PackageInstaller { } /** + * Return the current verification enforcement policy. This may only be called by the + * package currently set by the system as the verifier agent. + * @hide + */ + @FlaggedApi(Flags.FLAG_VERIFICATION_SERVICE) + @SystemApi + @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) + public final @VerificationPolicy int getVerificationPolicy() { + try { + return mInstaller.getVerificationPolicy(); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + * Set the current verification enforcement policy which will be applied to all the future + * installation sessions. This may only be called by the package currently set by the system as + * the verifier agent. + * @hide + * @return whether the new policy was successfully set. + */ + @FlaggedApi(Flags.FLAG_VERIFICATION_SERVICE) + @SystemApi + @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) + public final boolean setVerificationPolicy(@VerificationPolicy int policy) { + try { + return mInstaller.setVerificationPolicy(policy); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * An installation that is being actively staged. For an install to succeed, * all existing and new packages must have identical package names, version * codes, and signing certificates. diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig index 52d733314eb6..5b38942d468d 100644 --- a/core/java/android/content/pm/flags.aconfig +++ b/core/java/android/content/pm/flags.aconfig @@ -334,3 +334,11 @@ flag { bug: "364771256" is_fixed_read_only: true } + +flag { + name: "reduce_broadcasts_for_component_state_changes" + namespace: "package_manager_service" + description: "Feature flag to limit sending of the PACKAGE_CHANGED broadcast to only the system and the application itself during component state changes." + bug: "292261144" + is_fixed_read_only: true +} diff --git a/core/java/android/content/pm/verify/pkg/IVerificationSessionInterface.aidl b/core/java/android/content/pm/verify/pkg/IVerificationSessionInterface.aidl index 7a9484abd1b1..036c1e69cb0d 100644 --- a/core/java/android/content/pm/verify/pkg/IVerificationSessionInterface.aidl +++ b/core/java/android/content/pm/verify/pkg/IVerificationSessionInterface.aidl @@ -25,4 +25,6 @@ interface IVerificationSessionInterface { long getTimeoutTime(int verificationId); @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)") long extendTimeRemaining(int verificationId, long additionalMs); + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT)") + boolean setVerificationPolicy(int verificationId, int policy); }
\ No newline at end of file diff --git a/core/java/android/content/pm/verify/pkg/VerificationSession.java b/core/java/android/content/pm/verify/pkg/VerificationSession.java index 70b4a022f521..f393be829aed 100644 --- a/core/java/android/content/pm/verify/pkg/VerificationSession.java +++ b/core/java/android/content/pm/verify/pkg/VerificationSession.java @@ -22,6 +22,7 @@ import android.annotation.NonNull; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.content.pm.Flags; +import android.content.pm.PackageInstaller; import android.content.pm.SharedLibraryInfo; import android.content.pm.SigningInfo; import android.net.Uri; @@ -52,17 +53,12 @@ public final class VerificationSession implements Parcelable { * The verification cannot be completed because the network is unavailable. */ public static final int VERIFICATION_INCOMPLETE_NETWORK_UNAVAILABLE = 1; - /** - * The verification cannot be completed because the network is limited. - */ - public static final int VERIFICATION_INCOMPLETE_NETWORK_LIMITED = 2; /** * @hide */ @IntDef(prefix = {"VERIFICATION_INCOMPLETE_"}, value = { VERIFICATION_INCOMPLETE_NETWORK_UNAVAILABLE, - VERIFICATION_INCOMPLETE_NETWORK_LIMITED, VERIFICATION_INCOMPLETE_UNKNOWN, }) @Retention(RetentionPolicy.SOURCE) @@ -85,6 +81,15 @@ public final class VerificationSession implements Parcelable { private final IVerificationSessionInterface mSession; @NonNull private final IVerificationSessionCallback mCallback; + /** + * The current policy that is active for the session. It might not be + * the same as the original policy that was initially assigned for this verification session, + * because the active policy can be overridden by {@link #setVerificationPolicy(int)}. + * <p>To improve the latency, store the original policy value and any changes made to it, + * so that {@link #getVerificationPolicy()} does not need to make a binder call to retrieve the + * currently active policy.</p> + */ + private volatile @PackageInstaller.VerificationPolicy int mVerificationPolicy; /** * Constructor used by the system to describe the details of a verification session. @@ -94,6 +99,7 @@ public final class VerificationSession implements Parcelable { @NonNull Uri stagedPackageUri, @NonNull SigningInfo signingInfo, @NonNull List<SharedLibraryInfo> declaredLibraries, @NonNull PersistableBundle extensionParams, + @PackageInstaller.VerificationPolicy int defaultPolicy, @NonNull IVerificationSessionInterface session, @NonNull IVerificationSessionCallback callback) { mId = id; @@ -103,6 +109,7 @@ public final class VerificationSession implements Parcelable { mSigningInfo = signingInfo; mDeclaredLibraries = declaredLibraries; mExtensionParams = extensionParams; + mVerificationPolicy = defaultPolicy; mSession = session; mCallback = callback; } @@ -144,7 +151,7 @@ public final class VerificationSession implements Parcelable { /** * Returns a mapping of any shared libraries declared in the manifest - * to the {@link SharedLibraryInfo#Type} that is declared. This will be an empty + * to the {@link SharedLibraryInfo.Type} that is declared. This will be an empty * map if no shared libraries are declared by the package. */ @NonNull @@ -174,6 +181,39 @@ public final class VerificationSession implements Parcelable { } /** + * Return the current policy that is active for this session. + * <p>If the policy for this session has been changed by {@link #setVerificationPolicy}, + * the return value of this method is the current policy that is active for this session. + * Otherwise, the return value is the same as the initial policy that was assigned to the + * session when it was first created.</p> + */ + public @PackageInstaller.VerificationPolicy int getVerificationPolicy() { + return mVerificationPolicy; + } + + /** + * Override the verification policy for this session. + * @return True if the override was successful, False otherwise. + */ + @RequiresPermission(android.Manifest.permission.VERIFICATION_AGENT) + public boolean setVerificationPolicy(@PackageInstaller.VerificationPolicy int policy) { + if (mVerificationPolicy == policy) { + // No effective policy change + return true; + } + try { + if (mSession.setVerificationPolicy(mId, policy)) { + mVerificationPolicy = policy; + return true; + } else { + return false; + } + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** * Extend the timeout for this session by the provided additionalMs to * fetch relevant information over the network or wait for the network. * This may be called multiple times. If the request would bypass any max @@ -239,6 +279,7 @@ public final class VerificationSession implements Parcelable { mSigningInfo = SigningInfo.CREATOR.createFromParcel(in); mDeclaredLibraries = in.createTypedArrayList(SharedLibraryInfo.CREATOR); mExtensionParams = in.readPersistableBundle(getClass().getClassLoader()); + mVerificationPolicy = in.readInt(); mSession = IVerificationSessionInterface.Stub.asInterface(in.readStrongBinder()); mCallback = IVerificationSessionCallback.Stub.asInterface(in.readStrongBinder()); } @@ -257,6 +298,7 @@ public final class VerificationSession implements Parcelable { mSigningInfo.writeToParcel(dest, flags); dest.writeTypedList(mDeclaredLibraries); dest.writePersistableBundle(mExtensionParams); + dest.writeInt(mVerificationPolicy); dest.writeStrongBinder(mSession.asBinder()); dest.writeStrongBinder(mCallback.asBinder()); } diff --git a/core/java/android/os/BaseBundle.java b/core/java/android/os/BaseBundle.java index 49ab15a40a8e..50121278f0e6 100644 --- a/core/java/android/os/BaseBundle.java +++ b/core/java/android/os/BaseBundle.java @@ -21,6 +21,7 @@ import static java.util.Objects.requireNonNull; import android.annotation.NonNull; import android.annotation.Nullable; import android.compat.annotation.UnsupportedAppUsage; +import android.content.Intent; import android.util.ArrayMap; import android.util.Log; import android.util.MathUtils; @@ -401,6 +402,9 @@ public class BaseBundle { synchronized (this) { object = unwrapLazyValueFromMapLocked(i, clazz, itemTypes); } + if ((mFlags & Bundle.FLAG_VERIFY_TOKENS_PRESENT) != 0) { + Intent.maybeMarkAsMissingCreatorToken(object); + } } return (clazz != null) ? clazz.cast(object) : (T) object; } diff --git a/core/java/android/os/Bundle.java b/core/java/android/os/Bundle.java index ed4037c7d246..c18fb0c38b82 100644 --- a/core/java/android/os/Bundle.java +++ b/core/java/android/os/Bundle.java @@ -62,6 +62,12 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { @VisibleForTesting static final int FLAG_HAS_BINDERS = 1 << 12; + /** + * Indicates there may be intents with creator tokens contained in this bundle. When unparceled, + * they should be verified if tokens are missing or invalid. + */ + static final int FLAG_VERIFY_TOKENS_PRESENT = 1 << 13; + /** * Status when the Bundle can <b>assert</b> that the underlying Parcel DOES NOT contain @@ -274,6 +280,11 @@ public final class Bundle extends BaseBundle implements Cloneable, Parcelable { return orig; } + /** {@hide} */ + public void setIsIntentExtra() { + mFlags |= FLAG_VERIFY_TOKENS_PRESENT; + } + /** * Mark if this Bundle is okay to "defuse." That is, it's okay for system * processes to ignore any {@link BadParcelableException} encountered when diff --git a/core/java/android/os/Debug.java b/core/java/android/os/Debug.java index a55398ac9752..ef1e6c9405f3 100644 --- a/core/java/android/os/Debug.java +++ b/core/java/android/os/Debug.java @@ -114,6 +114,7 @@ public final class Debug "opengl-tracing", "view-hierarchy", "support_boot_stages", + "app_info", }; /** @@ -1016,14 +1017,14 @@ public final class Debug // send VM_START. System.out.println("Waiting for debugger first packet"); - mWaiting = true; + setWaitingForDebugger(true); while (!isDebuggerConnected()) { try { Thread.sleep(100); } catch (InterruptedException ie) { } } - mWaiting = false; + setWaitingForDebugger(false); System.out.println("Debug.suspendAllAndSentVmStart"); VMDebug.suspendAllAndSendVmStart(); @@ -1049,12 +1050,12 @@ public final class Debug Chunk waitChunk = new Chunk(ChunkHandler.type("WAIT"), data, 0, 1); DdmServer.sendChunk(waitChunk); - mWaiting = true; + setWaitingForDebugger(true); while (!isDebuggerConnected()) { try { Thread.sleep(SPIN_DELAY); } catch (InterruptedException ie) {} } - mWaiting = false; + setWaitingForDebugger(false); System.out.println("Debugger has connected"); @@ -1112,6 +1113,16 @@ public final class Debug } /** + * Set whether the app is waiting for a debugger to connect + * + * @hide + */ + private static void setWaitingForDebugger(boolean waiting) { + mWaiting = waiting; + VMDebug.setWaitingForDebugger(waiting); + } + + /** * Returns an array of strings that identify Framework features. This is * used by DDMS to determine what sorts of operations the Framework can * perform. diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java index 346ee7ca4f87..71d29af6cf02 100644 --- a/core/java/android/os/Process.java +++ b/core/java/android/os/Process.java @@ -41,6 +41,7 @@ import com.android.internal.os.SomeArgs; import com.android.internal.util.Preconditions; import com.android.sdksandbox.flags.Flags; +import dalvik.system.VMDebug; import dalvik.system.VMRuntime; import libcore.io.IoUtils; @@ -1410,6 +1411,7 @@ public class Process { public static void setArgV0(@NonNull String text) { sArgV0 = text; setArgV0Native(text); + VMDebug.setCurrentProcessName(text); } private static native void setArgV0Native(String text); diff --git a/core/java/android/security/forensic/IForensicService.aidl b/core/java/android/security/forensic/IForensicService.aidl new file mode 100644 index 000000000000..a944b18cb26d --- /dev/null +++ b/core/java/android/security/forensic/IForensicService.aidl @@ -0,0 +1,32 @@ +/* + * Copyright 2024 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.security.forensic; + +import android.security.forensic.IForensicServiceCommandCallback; +import android.security.forensic.IForensicServiceStateCallback; + +/** + * Binder interface to communicate with ForensicService. + * @hide + */ +interface IForensicService { + void monitorState(IForensicServiceStateCallback callback); + void makeVisible(IForensicServiceCommandCallback callback); + void makeInvisible(IForensicServiceCommandCallback callback); + void enable(IForensicServiceCommandCallback callback); + void disable(IForensicServiceCommandCallback callback); +} diff --git a/core/java/android/security/forensic/IForensicServiceCommandCallback.aidl b/core/java/android/security/forensic/IForensicServiceCommandCallback.aidl new file mode 100644 index 000000000000..7fa0c7f72690 --- /dev/null +++ b/core/java/android/security/forensic/IForensicServiceCommandCallback.aidl @@ -0,0 +1,33 @@ +/* + * Copyright 2024 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.security.forensic; + +/** + * @hide + */ + oneway interface IForensicServiceCommandCallback { + @Backing(type="int") + enum ErrorCode{ + UNKNOWN = 0, + PERMISSION_DENIED = 1, + INVALID_STATE_TRANSITION = 2, + BACKUP_TRANSPORT_UNAVAILABLE = 3, + DATA_SOURCE_UNAVAILABLE = 3, + } + void onSuccess(); + void onFailure(ErrorCode error); + } diff --git a/core/java/android/security/forensic/IForensicServiceStateCallback.aidl b/core/java/android/security/forensic/IForensicServiceStateCallback.aidl new file mode 100644 index 000000000000..0cda35083ffd --- /dev/null +++ b/core/java/android/security/forensic/IForensicServiceStateCallback.aidl @@ -0,0 +1,31 @@ +/* + * Copyright 2024 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.security.forensic; + +/** + * @hide + */ + oneway interface IForensicServiceStateCallback { + @Backing(type="int") + enum State{ + UNKNOWN = 0, + INVISIBLE = 1, + VISIBLE = 2, + ENABLED = 3, + } + void onStateChange(State state); + } diff --git a/core/java/android/util/NtpTrustedTime.java b/core/java/android/util/NtpTrustedTime.java index 9f54d9fca24b..3adbd686cd2c 100644 --- a/core/java/android/util/NtpTrustedTime.java +++ b/core/java/android/util/NtpTrustedTime.java @@ -24,7 +24,7 @@ import android.content.Context; import android.content.res.Resources; import android.net.ConnectivityManager; import android.net.Network; -import android.net.NetworkCapabilities; +import android.net.NetworkInfo; import android.net.SntpClient; import android.os.Build; import android.os.SystemClock; @@ -687,16 +687,8 @@ public abstract class NtpTrustedTime implements TrustedTime { if (connectivityManager == null) { return false; } - final NetworkCapabilities networkCapabilities = - connectivityManager.getNetworkCapabilities(network); - if (networkCapabilities == null) { - if (LOGD) Log.d(TAG, "getNetwork: failed to get network capabilities"); - return false; - } - final boolean isConnectedToInternet = networkCapabilities.hasCapability( - NetworkCapabilities.NET_CAPABILITY_INTERNET) - && networkCapabilities.hasCapability( - NetworkCapabilities.NET_CAPABILITY_VALIDATED); + final NetworkInfo ni = connectivityManager.getNetworkInfo(network); + // This connectivity check is to avoid performing a DNS lookup for the time server on a // unconnected network. There are races to obtain time in Android when connectivity // changes, which means that forceRefresh() can be called by various components before @@ -706,8 +698,8 @@ public abstract class NtpTrustedTime implements TrustedTime { // A side effect of check is that tests that run a fake NTP server on the device itself // will only be able to use it if the active network is connected, even though loopback // addresses are actually reachable. - if (!isConnectedToInternet) { - if (LOGD) Log.d(TAG, "getNetwork: no internet connectivity"); + if (ni == null || !ni.isConnected()) { + if (LOGD) Log.d(TAG, "getNetwork: no connectivity"); return false; } return true; diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 182ed1ebad59..1f17e8ec7b85 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -5952,7 +5952,34 @@ public final class ViewRootImpl implements ViewParent, // If no intersection, set bounds to empty. bounds.setEmpty(); } - return !bounds.isEmpty(); + + if (bounds.isEmpty()) { + return false; + } + + if (android.view.accessibility.Flags.focusRectMinSize()) { + adjustAccessibilityFocusedRectBoundsIfNeeded(bounds); + } + + return true; + } + + /** + * Adjusts accessibility focused rect bounds so that they are not invisible. + * + * <p>Focus bounds smaller than double the stroke width are very hard to see (or invisible). + * Expand the focus bounds if necessary to at least double the stroke width. + * @param bounds The bounds to adjust + */ + @VisibleForTesting + public void adjustAccessibilityFocusedRectBoundsIfNeeded(Rect bounds) { + final int minRectLength = mAccessibilityManager.getAccessibilityFocusStrokeWidth() * 2; + if (bounds.width() < minRectLength || bounds.height() < minRectLength) { + final float missingWidth = Math.max(0, minRectLength - bounds.width()); + final float missingHeight = Math.max(0, minRectLength - bounds.height()); + bounds.inset(-1 * (int) Math.ceil(missingWidth / 2), + -1 * (int) Math.ceil(missingHeight / 2)); + } } private Drawable getAccessibilityFocusedDrawable() { diff --git a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig index 8ffae845de1f..820a1fba11ad 100644 --- a/core/java/android/view/accessibility/flags/accessibility_flags.aconfig +++ b/core/java/android/view/accessibility/flags/accessibility_flags.aconfig @@ -96,6 +96,16 @@ flag { flag { namespace: "accessibility" + name: "focus_rect_min_size" + description: "Ensures the a11y focus rect is big enough to be drawn as visible" + bug: "368667566" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + namespace: "accessibility" name: "force_invert_color" description: "Enable force force-dark for smart inversion and dark theme everywhere" bug: "282821643" diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig index 18129530978f..45f6480b0b7f 100644 --- a/core/java/android/window/flags/lse_desktop_experience.aconfig +++ b/core/java/android/window/flags/lse_desktop_experience.aconfig @@ -308,6 +308,13 @@ flag { } flag { + name: "enable_restore_to_previous_size_from_desktop_immersive" + namespace: "lse_desktop_experience" + description: "Restores the window bounds to their previous size when exiting desktop immersive" + bug: "372318163" +} + +flag { name: "enable_display_focus_in_shell_transitions" namespace: "lse_desktop_experience" description: "Creates a shell transition when display focus switches." diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 5693d666adc2..5522aa09a32e 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -8479,6 +8479,18 @@ android:protectionLevel="internal" android:featureFlag="android.content.pm.verification_service" /> + <!-- + This permission allows the system to receive PACKAGE_CHANGED broadcasts when the component + state of a non-exported component has been changed. + <p>Not for use by third-party applications. </p> + <p>Protection level: internal + @hide + --> + <permission + android:name="android.permission.RECEIVE_PACKAGE_CHANGED_BROADCAST_ON_COMPONENT_STATE_CHANGED" + android:protectionLevel="internal" + android:featureFlag="android.content.pm.reduce_broadcasts_for_component_state_changes"/> + <!-- Attribution for Geofencing service. --> <attribution android:tag="GeofencingService" android:label="@string/geofencing_service"/> <!-- Attribution for Country Detector. --> diff --git a/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java b/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java index 228647ae9094..b5ee1302fc1d 100644 --- a/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java +++ b/core/tests/coretests/src/android/app/PropertyInvalidatedCacheTests.java @@ -19,7 +19,6 @@ package android.app; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotSame; import static org.junit.Assert.assertSame; -import static org.junit.Assert.fail; import android.platform.test.annotations.IgnoreUnderRavenwood; import android.platform.test.ravenwood.RavenwoodRule; @@ -27,7 +26,6 @@ import android.platform.test.ravenwood.RavenwoodRule; import androidx.test.filters.SmallTest; import org.junit.After; -import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -92,10 +90,11 @@ public class PropertyInvalidatedCacheTests { } } - // Ensure all test nonces are cleared after the test ends. + // Clear the test mode after every test, in case this process is used for other + // tests. This also resets the test property map. @After public void tearDown() throws Exception { - PropertyInvalidatedCache.resetAfterTest(); + PropertyInvalidatedCache.setTestMode(false); } // This test is disabled pending an sepolicy change that allows any app to set the @@ -112,6 +111,9 @@ public class PropertyInvalidatedCacheTests { new PropertyInvalidatedCache<>(4, MODULE, API, "cache1", new ServerQuery(tester)); + PropertyInvalidatedCache.setTestMode(true); + testCache.testPropertyName(); + tester.verify(0); assertEquals(tester.value(3), testCache.query(3)); tester.verify(1); @@ -221,16 +223,22 @@ public class PropertyInvalidatedCacheTests { TestCache(String module, String api) { this(module, api, new TestQuery()); + setTestMode(true); + testPropertyName(); } TestCache(String module, String api, TestQuery query) { super(4, module, api, api, query); mQuery = query; + setTestMode(true); + testPropertyName(); } public int getRecomputeCount() { return mQuery.getRecomputeCount(); } + + } @Test @@ -367,18 +375,4 @@ public class PropertyInvalidatedCacheTests { PropertyInvalidatedCache.MODULE_BLUETOOTH, "getState"); assertEquals(n1, "cache_key.bluetooth.get_state"); } - - // It is illegal to continue to use a cache with a test key after calling setTestMode(false). - // This test verifies the code detects errors in calling setTestMode(). - @Test - public void testTestMode() { - TestCache cache = new TestCache(); - cache.invalidateCache(); - PropertyInvalidatedCache.resetAfterTest(); - try { - cache.invalidateCache(); - fail("expected an IllegalStateException"); - } catch (IllegalStateException expected) { - } - } } diff --git a/core/tests/coretests/src/android/content/pm/verify/VerificationSessionTest.java b/core/tests/coretests/src/android/content/pm/verify/VerificationSessionTest.java index 987f68d4f9e1..80255c5f6600 100644 --- a/core/tests/coretests/src/android/content/pm/verify/VerificationSessionTest.java +++ b/core/tests/coretests/src/android/content/pm/verify/VerificationSessionTest.java @@ -16,6 +16,9 @@ package android.content.pm.verify; +import static android.content.pm.PackageInstaller.VERIFICATION_POLICY_BLOCK_FAIL_CLOSED; +import static android.content.pm.PackageInstaller.VERIFICATION_POLICY_BLOCK_FAIL_WARN; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.anyInt; @@ -23,6 +26,8 @@ import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; import static org.mockito.Mockito.when; import android.content.pm.SharedLibraryInfo; @@ -73,6 +78,7 @@ public class VerificationSessionTest { private static final long TEST_EXTEND_TIME = 2000L; private static final String TEST_KEY = "test key"; private static final String TEST_VALUE = "test value"; + private static final int TEST_POLICY = VERIFICATION_POLICY_BLOCK_FAIL_CLOSED; private final ArrayList<SharedLibraryInfo> mTestDeclaredLibraries = new ArrayList<>(); private final PersistableBundle mTestExtensionParams = new PersistableBundle(); @@ -90,7 +96,7 @@ public class VerificationSessionTest { mTestExtensionParams.putString(TEST_KEY, TEST_VALUE); mTestSession = new VerificationSession(TEST_ID, TEST_INSTALL_SESSION_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI, TEST_SIGNING_INFO, mTestDeclaredLibraries, - mTestExtensionParams, mTestSessionInterface, mTestCallback); + mTestExtensionParams, TEST_POLICY, mTestSessionInterface, mTestCallback); } @Test @@ -118,6 +124,7 @@ public class VerificationSessionTest { // structure is different, but all the key/value pairs should be preserved as before. assertThat(sessionFromParcel.getExtensionParams().getString(TEST_KEY)) .isEqualTo(mTestExtensionParams.getString(TEST_KEY)); + assertThat(sessionFromParcel.getVerificationPolicy()).isEqualTo(TEST_POLICY); } @Test @@ -152,4 +159,42 @@ public class VerificationSessionTest { verify(mTestCallback, times(1)).reportVerificationIncomplete( eq(TEST_ID), eq(reason)); } + + @Test + public void testPolicyNoOverride() { + assertThat(mTestSession.getVerificationPolicy()).isEqualTo(TEST_POLICY); + // This "set" is a no-op + assertThat(mTestSession.setVerificationPolicy(TEST_POLICY)).isTrue(); + assertThat(mTestSession.getVerificationPolicy()).isEqualTo(TEST_POLICY); + verifyZeroInteractions(mTestSessionInterface); + } + + @Test + public void testPolicyOverrideFail() throws Exception { + final int newPolicy = VERIFICATION_POLICY_BLOCK_FAIL_WARN; + when(mTestSessionInterface.setVerificationPolicy(anyInt(), anyInt())).thenReturn(false); + assertThat(mTestSession.setVerificationPolicy(newPolicy)).isFalse(); + verify(mTestSessionInterface, times(1)) + .setVerificationPolicy(eq(TEST_ID), eq(newPolicy)); + // Next "get" should not trigger binder call because the previous "set" has failed + assertThat(mTestSession.getVerificationPolicy()).isEqualTo(TEST_POLICY); + verifyNoMoreInteractions(mTestSessionInterface); + } + + @Test + public void testPolicyOverrideSuccess() throws Exception { + final int newPolicy = VERIFICATION_POLICY_BLOCK_FAIL_WARN; + when(mTestSessionInterface.setVerificationPolicy(anyInt(), anyInt())).thenReturn(true); + assertThat(mTestSession.setVerificationPolicy(newPolicy)).isTrue(); + verify(mTestSessionInterface, times(1)) + .setVerificationPolicy(eq(TEST_ID), eq(newPolicy)); + assertThat(mTestSession.getVerificationPolicy()).isEqualTo(newPolicy); + assertThat(mTestSession.getVerificationPolicy()).isEqualTo(newPolicy); + + // Setting back to the original policy should still trigger binder calls + assertThat(mTestSession.setVerificationPolicy(TEST_POLICY)).isTrue(); + verify(mTestSessionInterface, times(1)) + .setVerificationPolicy(eq(TEST_ID), eq(TEST_POLICY)); + assertThat(mTestSession.getVerificationPolicy()).isEqualTo(TEST_POLICY); + } } diff --git a/core/tests/coretests/src/android/content/pm/verify/VerifierServiceTest.java b/core/tests/coretests/src/android/content/pm/verify/VerifierServiceTest.java index 7f73a1eb4b48..7807c8a94530 100644 --- a/core/tests/coretests/src/android/content/pm/verify/VerifierServiceTest.java +++ b/core/tests/coretests/src/android/content/pm/verify/VerifierServiceTest.java @@ -16,6 +16,8 @@ package android.content.pm.verify; +import static android.content.pm.PackageInstaller.VERIFICATION_POLICY_BLOCK_FAIL_CLOSED; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.eq; @@ -52,6 +54,7 @@ public class VerifierServiceTest { private static final String TEST_PACKAGE_NAME = "com.foo"; private static final Uri TEST_PACKAGE_URI = Uri.parse("test://test"); private static final SigningInfo TEST_SIGNING_INFO = new SigningInfo(); + private static final int TEST_POLICY = VERIFICATION_POLICY_BLOCK_FAIL_CLOSED; private VerifierService mService; private VerificationSession mSession; @@ -60,8 +63,7 @@ public class VerifierServiceTest { mService = Mockito.mock(VerifierService.class, Answers.CALLS_REAL_METHODS); mSession = new VerificationSession(TEST_ID, TEST_INSTALL_SESSION_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI, TEST_SIGNING_INFO, - new ArrayList<>(), - new PersistableBundle(), null, null); + new ArrayList<>(), new PersistableBundle(), TEST_POLICY, null, null); } @Test diff --git a/core/tests/coretests/src/android/os/IpcDataCacheTest.java b/core/tests/coretests/src/android/os/IpcDataCacheTest.java index 5852bee53778..64f77b309829 100644 --- a/core/tests/coretests/src/android/os/IpcDataCacheTest.java +++ b/core/tests/coretests/src/android/os/IpcDataCacheTest.java @@ -17,7 +17,6 @@ package android.os; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; import android.multiuser.Flags; import android.platform.test.annotations.IgnoreUnderRavenwood; @@ -27,7 +26,6 @@ import android.platform.test.ravenwood.RavenwoodRule; import androidx.test.filters.SmallTest; import org.junit.After; -import org.junit.Before; import org.junit.Rule; import org.junit.Test; @@ -94,17 +92,17 @@ public class IpcDataCacheTest { public Boolean apply(Integer x) { return mServer.query(x); } - @Override public boolean shouldBypassCache(Integer x) { return x % 13 == 0; } } - // Ensure all test nonces are cleared after the test ends. + // Clear the test mode after every test, in case this process is used for other + // tests. This also resets the test property map. @After public void tearDown() throws Exception { - IpcDataCache.resetAfterTest(); + IpcDataCache.setTestMode(false); } // This test is disabled pending an sepolicy change that allows any app to set the @@ -121,6 +119,9 @@ public class IpcDataCacheTest { new IpcDataCache<>(4, MODULE, API, "testCache1", new ServerQuery(tester)); + IpcDataCache.setTestMode(true); + testCache.testPropertyName(); + tester.verify(0); assertEquals(tester.value(3), testCache.query(3)); tester.verify(1); @@ -164,6 +165,9 @@ public class IpcDataCacheTest { IpcDataCache<Integer, Boolean> testCache = new IpcDataCache<>(config, (x) -> tester.query(x, x % 10 == 9)); + IpcDataCache.setTestMode(true); + testCache.testPropertyName(); + tester.verify(0); assertEquals(tester.value(3), testCache.query(3)); tester.verify(1); @@ -201,6 +205,9 @@ public class IpcDataCacheTest { IpcDataCache<Integer, Boolean> testCache = new IpcDataCache<>(config, (x) -> tester.query(x), (x) -> x % 9 == 0); + IpcDataCache.setTestMode(true); + testCache.testPropertyName(); + tester.verify(0); assertEquals(tester.value(3), testCache.query(3)); tester.verify(1); @@ -306,6 +313,8 @@ public class IpcDataCacheTest { TestCache(String module, String api, TestQuery query) { super(4, module, api, "testCache7", query); mQuery = query; + setTestMode(true); + testPropertyName(); } TestCache(IpcDataCache.Config c) { @@ -315,6 +324,8 @@ public class IpcDataCacheTest { TestCache(IpcDataCache.Config c, TestQuery query) { super(c, query); mQuery = query; + setTestMode(true); + testPropertyName(); } int getRecomputeCount() { @@ -445,18 +456,4 @@ public class IpcDataCacheTest { TestCache ec = new TestCache(e); assertEquals(ec.isDisabled(), true); } - - // It is illegal to continue to use a cache with a test key after calling setTestMode(false). - // This test verifies the code detects errors in calling setTestMode(). - @Test - public void testTestMode() { - TestCache cache = new TestCache(); - cache.invalidateCache(); - IpcDataCache.resetAfterTest(); - try { - cache.invalidateCache(); - fail("expected an IllegalStateException"); - } catch (IllegalStateException expected) { - } - } } diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java index 632721126714..ed9fc1c9e547 100644 --- a/core/tests/coretests/src/android/view/ViewRootImplTest.java +++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java @@ -71,6 +71,7 @@ import android.graphics.Rect; import android.hardware.display.DisplayManagerGlobal; import android.os.Binder; import android.os.SystemProperties; +import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.Presubmit; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; @@ -82,6 +83,7 @@ import android.util.DisplayMetrics; import android.util.Log; import android.view.WindowInsets.Side; import android.view.WindowInsets.Type; +import android.view.accessibility.AccessibilityManager; import androidx.test.annotation.UiThreadTest; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -1628,6 +1630,42 @@ public class ViewRootImplTest { }); } + @Test + @EnableFlags(android.view.accessibility.Flags.FLAG_FOCUS_RECT_MIN_SIZE) + public void testAdjustAccessibilityFocusedBounds_largeEnoughBoundsAreUnchanged() { + final int strokeWidth = sContext.getSystemService(AccessibilityManager.class) + .getAccessibilityFocusStrokeWidth(); + final int left, top, width, height; + left = top = 100; + width = height = strokeWidth * 2; + final Rect bounds = new Rect(left, top, left + width, top + height); + final Rect originalBounds = new Rect(bounds); + + mViewRootImpl.adjustAccessibilityFocusedRectBoundsIfNeeded(bounds); + + assertThat(bounds).isEqualTo(originalBounds); + } + + @Test + @EnableFlags(android.view.accessibility.Flags.FLAG_FOCUS_RECT_MIN_SIZE) + public void testAdjustAccessibilityFocusedBounds_smallBoundsAreExpanded() { + final int strokeWidth = sContext.getSystemService(AccessibilityManager.class) + .getAccessibilityFocusStrokeWidth(); + final int left, top, width, height; + left = top = 100; + width = height = strokeWidth; + final Rect bounds = new Rect(left, top, left + width, top + height); + final Rect originalBounds = new Rect(bounds); + + mViewRootImpl.adjustAccessibilityFocusedRectBoundsIfNeeded(bounds); + + // Bounds should be centered on the same point, but expanded to at least strokeWidth * 2 + assertThat(bounds.centerX()).isEqualTo(originalBounds.centerX()); + assertThat(bounds.centerY()).isEqualTo(originalBounds.centerY()); + assertThat(bounds.width()).isAtLeast(strokeWidth * 2); + assertThat(bounds.height()).isAtLeast(strokeWidth * 2); + } + private boolean setForceDarkSysProp(boolean isForceDarkEnabled) { try { SystemProperties.set( diff --git a/data/etc/privapp-permissions-platform.xml b/data/etc/privapp-permissions-platform.xml index 2e72f0ec90b8..4aa7e96f352d 100644 --- a/data/etc/privapp-permissions-platform.xml +++ b/data/etc/privapp-permissions-platform.xml @@ -572,6 +572,7 @@ applications that come with the platform <permission name="android.permission.READ_BLOCKED_NUMBERS" /> <!-- Permission required for CTS test - PackageManagerTest --> <permission name="android.permission.DOMAIN_VERIFICATION_AGENT"/> + <permission name="android.permission.VERIFICATION_AGENT"/> <!-- Permission required for CTS test CtsInputTestCases --> <permission name="android.permission.OVERRIDE_SYSTEM_KEY_BEHAVIOR_IN_FOCUSED_WINDOW" /> <!-- Permission required for CTS test - PackageManagerShellCommandInstallTest --> diff --git a/errorprone/OWNERS b/errorprone/OWNERS index bddbdb364683..aa8c126a32e1 100644 --- a/errorprone/OWNERS +++ b/errorprone/OWNERS @@ -1,2 +1 @@ -jsharkey@android.com -jsharkey@google.com +colefaust@google.com diff --git a/graphics/java/android/graphics/text/PositionedGlyphs.java b/graphics/java/android/graphics/text/PositionedGlyphs.java index 671eb6e514c5..ed17fdefcb53 100644 --- a/graphics/java/android/graphics/text/PositionedGlyphs.java +++ b/graphics/java/android/graphics/text/PositionedGlyphs.java @@ -26,6 +26,7 @@ import android.graphics.Typeface; import android.graphics.fonts.Font; import com.android.internal.util.Preconditions; +import com.android.text.flags.Flags; import dalvik.annotation.optimization.CriticalNative; @@ -132,6 +133,9 @@ public final class PositionedGlyphs { @NonNull public Font getFont(@IntRange(from = 0) int index) { Preconditions.checkArgumentInRange(index, 0, glyphCount() - 1, "index"); + if (Flags.typefaceRedesign()) { + return mFonts.get(nGetFontId(mLayoutPtr, index)); + } return mFonts.get(index); } @@ -245,20 +249,29 @@ public final class PositionedGlyphs { */ public PositionedGlyphs(long layoutPtr, float xOffset, float yOffset) { mLayoutPtr = layoutPtr; - int glyphCount = nGetGlyphCount(layoutPtr); - mFonts = new ArrayList<>(glyphCount); mXOffset = xOffset; mYOffset = yOffset; - long prevPtr = 0; - Font prevFont = null; - for (int i = 0; i < glyphCount; ++i) { - long ptr = nGetFont(layoutPtr, i); - if (prevPtr != ptr) { - prevPtr = ptr; - prevFont = new Font(ptr); + if (Flags.typefaceRedesign()) { + int fontCount = nGetFontCount(layoutPtr); + mFonts = new ArrayList<>(fontCount); + for (int i = 0; i < fontCount; ++i) { + mFonts.add(new Font(nGetFontRef(layoutPtr, i))); + } + } else { + int glyphCount = nGetGlyphCount(layoutPtr); + mFonts = new ArrayList<>(glyphCount); + + long prevPtr = 0; + Font prevFont = null; + for (int i = 0; i < glyphCount; ++i) { + long ptr = nGetFont(layoutPtr, i); + if (prevPtr != ptr) { + prevPtr = ptr; + prevFont = new Font(ptr); + } + mFonts.add(prevFont); } - mFonts.add(prevFont); } NoImagePreloadHolder.REGISTRY.registerNativeAllocation(this, layoutPtr); @@ -290,6 +303,12 @@ public final class PositionedGlyphs { private static native float nGetWeightOverride(long minikinLayout, int i); @CriticalNative private static native float nGetItalicOverride(long minikinLayout, int i); + @CriticalNative + private static native int nGetFontCount(long minikinLayout); + @CriticalNative + private static native long nGetFontRef(long minikinLayout, int fontId); + @CriticalNative + private static native int nGetFontId(long minikinLayout, int glyphIndex); @Override public boolean equals(Object o) { diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java index 882a8d035e93..ad194f707cf3 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java @@ -1255,7 +1255,8 @@ class DividerPresenter implements View.OnTouchListener { // Update divider line surface visibility and color. // If a container is fully expanded, the divider line is invisible unless dragging. - final boolean isDividerLineVisible = !mProperties.mIsDraggableExpandType || mIsDragging; + final boolean isDividerLineVisible = mProperties.mDividerWidthPx > 0 + && (!mProperties.mIsDraggableExpandType || mIsDragging); t.setVisibility(mDividerLineSurface, isDividerLineVisible); t.setColor(mDividerLineSurface, colorToFloatArray( Color.valueOf(mProperties.mDividerAttributes.getDividerColor()))); diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewTest.kt index f181ce004478..fa9d2baa78d9 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewTest.kt @@ -18,6 +18,7 @@ package com.android.wm.shell.bubbles.bar import android.app.ActivityManager import android.content.Context +import android.content.pm.ShortcutInfo import android.graphics.Insets import android.graphics.Rect import android.view.LayoutInflater @@ -45,11 +46,14 @@ import com.android.wm.shell.shared.handles.RegionSamplingHelper import com.android.wm.shell.taskview.TaskView import com.android.wm.shell.taskview.TaskViewTaskController import com.google.common.truth.Truth.assertThat +import com.google.common.util.concurrent.MoreExecutors.directExecutor import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.mock +import org.mockito.kotlin.spy +import org.mockito.kotlin.verify import org.mockito.kotlin.whenever import java.util.Collections import java.util.concurrent.Executor @@ -72,14 +76,18 @@ class BubbleBarExpandedViewTest { private lateinit var expandedViewManager: BubbleExpandedViewManager private lateinit var positioner: BubblePositioner private lateinit var bubbleTaskView: BubbleTaskView + private lateinit var bubble: Bubble private lateinit var bubbleExpandedView: BubbleBarExpandedView private var testableRegionSamplingHelper: TestableRegionSamplingHelper? = null private var regionSamplingProvider: TestRegionSamplingProvider? = null + private val bubbleLogger = spy(BubbleLogger(UiEventLoggerFake())) + @Before fun setUp() { ProtoLog.REQUIRE_PROTOLOGTOOL = false + ProtoLog.init() mainExecutor = TestExecutor() bgExecutor = TestExecutor() positioner = BubblePositioner(context, windowManager) @@ -108,7 +116,7 @@ class BubbleBarExpandedViewTest { bubbleExpandedView.initialize( expandedViewManager, positioner, - BubbleLogger(UiEventLoggerFake()), + bubbleLogger, false /* isOverflow */, bubbleTaskView, mainExecutor, @@ -121,6 +129,20 @@ class BubbleBarExpandedViewTest { // Helper should be created once attached to window testableRegionSamplingHelper = regionSamplingProvider!!.helper }) + + bubble = Bubble( + "key", + ShortcutInfo.Builder(context, "id").build(), + 100 /* desiredHeight */, + 0 /* desiredHeightResId */, + "title", + 0 /* taskId */, + null /* locus */, + true /* isDismissable */, + directExecutor(), + directExecutor() + ) {} + bubbleExpandedView.update(bubble) } @After @@ -194,6 +216,16 @@ class BubbleBarExpandedViewTest { assertThat(testableRegionSamplingHelper!!.isStopped).isTrue() } + @Test + fun testEventLogging_dismissBubbleViaAppMenu() { + getInstrumentation().runOnMainSync { bubbleExpandedView.handleView.performClick() } + val dismissMenuItem = + bubbleExpandedView.findViewWithTag<View>(BubbleBarMenuView.DISMISS_ACTION_TAG) + assertThat(dismissMenuItem).isNotNull() + getInstrumentation().runOnMainSync { dismissMenuItem.performClick() } + verify(bubbleLogger).log(bubble, BubbleLogger.Event.BUBBLE_BAR_BUBBLE_DISMISSED_APP_MENU) + } + private inner class FakeBubbleTaskViewFactory : BubbleTaskViewFactory { override fun create(): BubbleTaskView { val taskViewTaskController = mock<TaskViewTaskController>() diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml index 35ef2393bb9b..37596182f05b 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml @@ -38,7 +38,7 @@ <Button android:layout_width="94dp" android:layout_height="60dp" - android:id="@+id/maximize_menu_maximize_button" + android:id="@+id/maximize_menu_size_toggle_button" style="?android:attr/buttonBarButtonStyle" android:stateListAnimator="@null" android:importantForAccessibility="yes" @@ -48,7 +48,7 @@ android:alpha="0"/> <TextView - android:id="@+id/maximize_menu_maximize_window_text" + android:id="@+id/maximize_menu_size_toggle_button_text" android:layout_width="94dp" android:layout_height="18dp" android:textSize="11sp" diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index 1f1565160965..df1e2248872b 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -492,8 +492,12 @@ <dimen name="desktop_mode_maximize_menu_buttons_outline_stroke">1dp</dimen> <!-- The radius of the inner fill of the maximize menu buttons. --> <dimen name="desktop_mode_maximize_menu_buttons_fill_radius">4dp</dimen> - <!-- The padding between the outline and fill of the maximize menu buttons. --> - <dimen name="desktop_mode_maximize_menu_buttons_fill_padding">4dp</dimen> + <!-- The padding between the outline and fill of the maximize menu snap and maximize buttons. --> + <dimen name="desktop_mode_maximize_menu_snap_and_maximize_buttons_fill_padding">4dp</dimen> + <!-- The vertical padding between the outline and fill of the maximize menu restore button. --> + <dimen name="desktop_mode_maximize_menu_restore_button_fill_vertical_padding">13dp</dimen> + <!-- The horizontal padding between the outline and fill of the maximize menu restore button. --> + <dimen name="desktop_mode_maximize_menu_restore_button_fill_horizontal_padding">21dp</dimen> <!-- The corner radius of the maximize menu. --> <dimen name="desktop_mode_maximize_menu_corner_radius">8dp</dimen> diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml index 621e2aacd673..afac9f6433a3 100644 --- a/libs/WindowManager/Shell/res/values/strings.xml +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -319,6 +319,8 @@ <string name="desktop_mode_non_resizable_snap_text">App can\'t be moved here</string> <!-- Accessibility text for the Maximize Menu's maximize button [CHAR LIMIT=NONE] --> <string name="desktop_mode_maximize_menu_maximize_button_text">Maximize</string> + <!-- Accessibility text for the Maximize Menu's restore button [CHAR LIMIT=NONE] --> + <string name="desktop_mode_maximize_menu_restore_button_text">Restore</string> <!-- Accessibility text for the Maximize Menu's snap left button [CHAR LIMIT=NONE] --> <string name="desktop_mode_maximize_menu_snap_left_button_text">Snap left</string> <!-- Accessibility text for the Maximize Menu's snap right button [CHAR LIMIT=NONE] --> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java index 84405bbe5823..0ce651c3f1fe 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java @@ -252,6 +252,7 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView @Override public void onDismissBubble(Bubble bubble) { mManager.dismissBubble(bubble, Bubbles.DISMISS_USER_GESTURE); + mBubbleLogger.log(bubble, BubbleLogger.Event.BUBBLE_BAR_BUBBLE_DISMISSED_APP_MENU); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.java index 0300869cbbe1..52b807abddd6 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.java @@ -16,6 +16,7 @@ package com.android.wm.shell.bubbles.bar; import android.annotation.ColorInt; +import android.annotation.Nullable; import android.content.Context; import android.content.res.ColorStateList; import android.content.res.TypedArray; @@ -41,6 +42,9 @@ import java.util.ArrayList; * Bubble bar expanded view menu */ public class BubbleBarMenuView extends LinearLayout { + + public static final Object DISMISS_ACTION_TAG = new Object(); + private ViewGroup mBubbleSectionView; private ViewGroup mActionsSectionView; private ImageView mBubbleIconView; @@ -119,6 +123,9 @@ public class BubbleBarMenuView extends LinearLayout { R.layout.bubble_bar_menu_item, mActionsSectionView, false); itemView.update(action.mIcon, action.mTitle, action.mTint); itemView.setOnClickListener(action.mOnClick); + if (action.mTag != null) { + itemView.setTag(action.mTag); + } mActionsSectionView.addView(itemView); } } @@ -159,6 +166,8 @@ public class BubbleBarMenuView extends LinearLayout { private Icon mIcon; private @ColorInt int mTint; private String mTitle; + @Nullable + private Object mTag; private OnClickListener mOnClick; MenuAction(Icon icon, String title, OnClickListener onClick) { @@ -171,5 +180,14 @@ public class BubbleBarMenuView extends LinearLayout { this.mTint = tint; this.mOnClick = onClick; } + + MenuAction(Icon icon, String title, @ColorInt int tint, @Nullable Object tag, + OnClickListener onClick) { + this.mIcon = icon; + this.mTitle = title; + this.mTint = tint; + this.mTag = tag; + this.mOnClick = onClick; + } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java index 514810745e10..5ed01b66ec67 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java @@ -212,6 +212,7 @@ class BubbleBarMenuViewController { Icon.createWithResource(resources, R.drawable.ic_remove_no_shadow), resources.getString(R.string.bubble_dismiss_text), tintColor, + BubbleBarMenuView.DISMISS_ACTION_TAG, view -> { hideMenu(true /* animated */); if (mListener != null) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index b723337cc894..4440778a5a45 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -1185,7 +1185,13 @@ class DesktopTasksController( val options = createNewWindowOptions(callingTask) if (options.launchWindowingMode == WINDOWING_MODE_FREEFORM) { wct.startTask(requestedTaskId, options.toBundle()) - transitions.startTransition(TRANSIT_OPEN, wct, null) + val taskToMinimize = bringDesktopAppsToFrontBeforeShowingNewTask( + callingTask.displayId, wct, requestedTaskId) + val runOnTransit = immersiveTransitionHandler + .exitImmersiveIfApplicable(wct, callingTask.displayId) + val transition = transitions.startTransition(TRANSIT_OPEN, wct, null) + addPendingMinimizeTransition(transition, taskToMinimize) + runOnTransit?.invoke(transition) } else { val splitPosition = splitScreenController.determineNewInstancePosition(callingTask) splitScreenController.startTask(requestedTaskId, splitPosition, @@ -1219,7 +1225,8 @@ class DesktopTasksController( .determineNewInstancePosition(callingTaskInfo) splitScreenController.startIntent( launchIntent, context.userId, fillIn, splitPosition, - options.toBundle(), null /* hideTaskToken */ + options.toBundle(), null /* hideTaskToken */, + true /* forceLaunchNewTask */ ) } WINDOWING_MODE_FREEFORM -> { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java index cbb08b804dfe..6da39951efbe 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java @@ -463,7 +463,7 @@ public class PipTransition extends PipTransitionController { // so update fixed rotation state to default. mFixedRotationState = FIXED_ROTATION_UNDEFINED; - if (transition != mExitTransition) { + if (transition != mExitTransition && transition != mMoveToBackTransition) { return; } // This means an expand happened before enter-pip finished and we are now "merging" a @@ -477,8 +477,10 @@ public class PipTransition extends PipTransitionController { cancelled = true; } - // Unset exitTransition AFTER cancel so that finishResize knows we are merging. + // Unset both exitTransition and moveToBackTransition AFTER cancel so that + // finishResize knows we are merging. mExitTransition = null; + mMoveToBackTransition = null; if (!cancelled) return; final ActivityManager.RunningTaskInfo taskInfo = mPipOrganizer.getTaskInfo(); if (taskInfo != null) { @@ -515,7 +517,8 @@ public class PipTransition extends PipTransitionController { // means we're expecting the exit transition will be "merged" into another transition // (likely a remote like launcher), so don't fire the finish-callback here -- wait until // the exit transition is merged. - if ((mExitTransition == null || isAnimatingLocally()) && mFinishCallback != null) { + if ((mExitTransition == null || mMoveToBackTransition == null || isAnimatingLocally()) + && mFinishCallback != null) { final SurfaceControl leash = mPipOrganizer.getSurfaceControl(); final boolean hasValidLeash = leash != null && leash.isValid(); WindowContainerTransaction wct = null; @@ -665,17 +668,6 @@ public class PipTransition extends PipTransitionController { return null; } - @Nullable - private TransitionInfo.Change findFixedRotationChange(@NonNull TransitionInfo info) { - for (int i = info.getChanges().size() - 1; i >= 0; --i) { - final TransitionInfo.Change change = info.getChanges().get(i); - if (change.getEndFixedRotation() != ROTATION_UNDEFINED) { - return change; - } - } - return null; - } - private void startExitAnimation(@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java index 9b815817d4d3..94b344fb575a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java @@ -16,6 +16,7 @@ package com.android.wm.shell.pip; +import static android.app.WindowConfiguration.ROTATION_UNDEFINED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.view.WindowManager.TRANSIT_PIP; @@ -346,6 +347,21 @@ public abstract class PipTransitionController implements Transitions.TransitionH return false; } + /** + * Gets a change amongst the transition targets that is in a different final orientation than + * the display, signalling a potential fixed rotation transition. + */ + @Nullable + public TransitionInfo.Change findFixedRotationChange(@NonNull TransitionInfo info) { + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + if (change.getEndFixedRotation() != ROTATION_UNDEFINED) { + return change; + } + } + return null; + } + /** End the currently-playing PiP animation. */ public void end() { } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterAnimator.java index f40a87c39aef..fcd5c3baab5d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterAnimator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterAnimator.java @@ -16,10 +16,14 @@ package com.android.wm.shell.pip2.animation; +import static android.view.Surface.ROTATION_270; +import static android.view.Surface.ROTATION_90; + import android.animation.Animator; import android.animation.RectEvaluator; import android.animation.ValueAnimator; import android.content.Context; +import android.graphics.Matrix; import android.graphics.PointF; import android.graphics.Rect; import android.view.Surface; @@ -60,6 +64,11 @@ public class PipEnterAnimator extends ValueAnimator private final PointF mInitScale = new PointF(); private final PointF mInitPos = new PointF(); private final Rect mInitCrop = new Rect(); + private final PointF mInitActivityScale = new PointF(); + private final PointF mInitActivityPos = new PointF(); + + Matrix mTransformTensor = new Matrix(); + final float[] mMatrixTmp = new float[9]; public PipEnterAnimator(Context context, @NonNull SurfaceControl leash, @@ -109,6 +118,10 @@ public class PipEnterAnimator extends ValueAnimator @Override public void onAnimationEnd(@NonNull Animator animation) { + if (mFinishTransaction != null) { + onEnterAnimationUpdate(mInitScale, mInitPos, mInitCrop, + 1f /* fraction */, mFinishTransaction); + } if (mAnimationEndCallback != null) { mAnimationEndCallback.run(); } @@ -126,16 +139,24 @@ public class PipEnterAnimator extends ValueAnimator float fraction, SurfaceControl.Transaction tx) { float scaleX = 1 + (initScale.x - 1) * (1 - fraction); float scaleY = 1 + (initScale.y - 1) * (1 - fraction); - tx.setScale(mLeash, scaleX, scaleY); - float posX = initPos.x + (mEndBounds.left - initPos.x) * fraction; float posY = initPos.y + (mEndBounds.top - initPos.y) * fraction; - tx.setPosition(mLeash, posX, posY); + + int normalizedRotation = mRotation; + if (normalizedRotation == ROTATION_270) { + normalizedRotation = -ROTATION_90; + } + float degrees = -normalizedRotation * 90f * fraction; Rect endCrop = new Rect(mEndBounds); endCrop.offsetTo(0, 0); mRectEvaluator.evaluate(fraction, initCrop, endCrop); tx.setCrop(mLeash, mAnimatedRect); + + mTransformTensor.setScale(scaleX, scaleY); + mTransformTensor.postTranslate(posX, posY); + mTransformTensor.postRotate(degrees); + tx.setMatrix(mLeash, mTransformTensor, mMatrixTmp); } // no-ops @@ -153,7 +174,22 @@ public class PipEnterAnimator extends ValueAnimator * calculated differently from generic transitions. * @param pipChange PiP change received as a transition target. */ - public void setEnterStartState(@NonNull TransitionInfo.Change pipChange) { + public void setEnterStartState(@NonNull TransitionInfo.Change pipChange, + @NonNull TransitionInfo.Change pipActivityChange) { + PipUtils.calcEndTransform(pipActivityChange, pipChange, mInitActivityScale, + mInitActivityPos); + if (mStartTransaction != null && pipActivityChange.getLeash() != null) { + mStartTransaction.setCrop(pipActivityChange.getLeash(), null); + mStartTransaction.setScale(pipActivityChange.getLeash(), mInitActivityScale.x, + mInitActivityScale.y); + mStartTransaction.setPosition(pipActivityChange.getLeash(), mInitActivityPos.x, + mInitActivityPos.y); + mFinishTransaction.setCrop(pipActivityChange.getLeash(), null); + mFinishTransaction.setScale(pipActivityChange.getLeash(), mInitActivityScale.x, + mInitActivityScale.y); + mFinishTransaction.setPosition(pipActivityChange.getLeash(), mInitActivityPos.x, + mInitActivityPos.y); + } PipUtils.calcStartTransform(pipChange, mInitScale, mInitPos, mInitCrop); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipExpandAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipExpandAnimator.java index 8fa5aa933929..a93ef12cb7fa 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipExpandAnimator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipExpandAnimator.java @@ -157,6 +157,7 @@ public class PipExpandAnimator extends ValueAnimator .shadow(tx, mLeash, false /* applyCornerRadius */); tx.apply(); } + private Rect getInsets(float fraction) { final Rect startInsets = mSourceRectHintInsets; final Rect endInsets = mZeroInsets; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java index 73be8db0ea8a..0427294579dc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java @@ -292,23 +292,34 @@ public class PipController implements ConfigurationChangeListener, setDisplayLayout(mDisplayController.getDisplayLayout(displayId)); if (!mPipTransitionState.isInPip()) { + // Skip the PiP-relevant updates if we aren't in a valid PiP state. + if (mPipTransitionState.isInFixedRotation()) { + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, + "Fixed rotation flag shouldn't be set while in an invalid PiP state"); + } return; } mPipTouchHandler.updateMinMaxSize(mPipBoundsState.getAspectRatio()); - // Update the caches to reflect the new display layout in the movement bounds; - // temporarily update bounds to be at the top left for the movement bounds calculation. - Rect toBounds = new Rect(0, 0, - (int) Math.ceil(mPipBoundsState.getMaxSize().x * boundsScale), - (int) Math.ceil(mPipBoundsState.getMaxSize().y * boundsScale)); - mPipBoundsState.setBounds(toBounds); - mPipTouchHandler.updateMovementBounds(); - - // The policy is to keep PiP snap fraction invariant. - mPipBoundsAlgorithm.applySnapFraction(toBounds, snapFraction); - mPipBoundsState.setBounds(toBounds); - t.setBounds(mPipTransitionState.mPipTaskToken, toBounds); + if (mPipTransitionState.isInFixedRotation()) { + // Do not change the bounds when in fixed rotation, but do update the movement bounds + // based on the current bounds state and potentially new display layout. + mPipTouchHandler.updateMovementBounds(); + mPipTransitionState.setInFixedRotation(false); + } else { + Rect toBounds = new Rect(0, 0, + (int) Math.ceil(mPipBoundsState.getMaxSize().x * boundsScale), + (int) Math.ceil(mPipBoundsState.getMaxSize().y * boundsScale)); + // Update the caches to reflect the new display layout in the movement bounds; + // temporarily update bounds to be at the top left for the movement bounds calculation. + mPipBoundsState.setBounds(toBounds); + mPipTouchHandler.updateMovementBounds(); + // The policy is to keep PiP snap fraction invariant. + mPipBoundsAlgorithm.applySnapFraction(toBounds, snapFraction); + mPipBoundsState.setBounds(toBounds); + } + t.setBounds(mPipTransitionState.mPipTaskToken, mPipBoundsState.getBounds()); } private void setDisplayLayout(DisplayLayout layout) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java index a4a7973ef4bb..4d0432e1066e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java @@ -406,12 +406,9 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha // We need to remove the callback even if the shelf is visible, in case it the delayed // callback hasn't been executed yet to avoid the wrong final state. mMainExecutor.removeCallbacks(mMoveOnShelVisibilityChanged); - if (shelfVisible) { - mMoveOnShelVisibilityChanged.run(); - } else { - // Postpone moving in response to hide of Launcher in case there's another change - mMainExecutor.executeDelayed(mMoveOnShelVisibilityChanged, PIP_KEEP_CLEAR_AREAS_DELAY); - } + + // Postpone moving in response to hide of Launcher in case there's another change + mMainExecutor.executeDelayed(mMoveOnShelVisibilityChanged, PIP_KEEP_CLEAR_AREAS_DELAY); } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java index ac1567aba6e9..779e4ea51347 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java @@ -16,7 +16,9 @@ package com.android.wm.shell.pip2.phone; +import static android.app.WindowConfiguration.ROTATION_UNDEFINED; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; +import static android.view.Surface.ROTATION_0; import static android.view.Surface.ROTATION_270; import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OPEN; @@ -36,6 +38,7 @@ import android.app.ActivityManager; import android.app.PictureInPictureParams; import android.content.Context; import android.graphics.Matrix; +import android.graphics.Point; import android.graphics.Rect; import android.os.Bundle; import android.os.IBinder; @@ -388,8 +391,15 @@ public class PipTransition extends PipTransitionController implements return false; } - Rect startBounds = pipChange.getStartAbsBounds(); + // We expect the PiP activity as a separate change in a config-at-end transition. + TransitionInfo.Change pipActivityChange = getDeferConfigActivityChange(info, + pipChange.getTaskInfo().getToken()); + if (pipActivityChange == null) { + return false; + } + Rect endBounds = pipChange.getEndAbsBounds(); + Rect activityEndBounds = pipActivityChange.getEndAbsBounds(); SurfaceControl pipLeash = mPipTransitionState.mPinnedTaskLeash; Preconditions.checkNotNull(pipLeash, "Leash is null for bounds transition."); @@ -411,14 +421,63 @@ public class PipTransition extends PipTransitionController implements } } + final TransitionInfo.Change fixedRotationChange = findFixedRotationChange(info); + int startRotation = pipChange.getStartRotation(); + int endRotation = fixedRotationChange != null + ? fixedRotationChange.getEndFixedRotation() : ROTATION_UNDEFINED; + final int delta = endRotation == ROTATION_UNDEFINED ? ROTATION_0 + : startRotation - endRotation; + + if (delta != ROTATION_0) { + mPipTransitionState.setInFixedRotation(true); + handleBoundsTypeFixedRotation(pipChange, pipActivityChange, fixedRotationChange); + } + PipEnterAnimator animator = new PipEnterAnimator(mContext, pipLeash, - startTransaction, finishTransaction, endBounds, sourceRectHint, Surface.ROTATION_0); - animator.setAnimationStartCallback(() -> animator.setEnterStartState(pipChange)); + startTransaction, finishTransaction, endBounds, sourceRectHint, delta); + animator.setAnimationStartCallback(() -> animator.setEnterStartState(pipChange, + pipActivityChange)); animator.setAnimationEndCallback(this::finishInner); animator.start(); return true; } + private void handleBoundsTypeFixedRotation(TransitionInfo.Change pipTaskChange, + TransitionInfo.Change pipActivityChange, + TransitionInfo.Change fixedRotationChange) { + final Rect endBounds = pipTaskChange.getEndAbsBounds(); + final Rect endActivityBounds = pipActivityChange.getEndAbsBounds(); + int startRotation = pipTaskChange.getStartRotation(); + int endRotation = fixedRotationChange.getEndFixedRotation(); + + // Cache the task to activity offset to potentially restore later. + Point activityEndOffset = new Point(endActivityBounds.left - endBounds.left, + endActivityBounds.top - endBounds.top); + + // If we are running a fixed rotation bounds enter PiP animation, + // then update the display layout rotation, and recalculate the end rotation bounds. + // Update the endBounds in place, so that the PiP change is up-to-date. + mPipDisplayLayoutState.rotateTo(endRotation); + float snapFraction = mPipBoundsAlgorithm.getSnapFraction( + mPipBoundsAlgorithm.getEntryDestinationBounds()); + mPipBoundsAlgorithm.applySnapFraction(endBounds, snapFraction); + mPipBoundsState.setBounds(endBounds); + + // Display bounds were already updated to represent the final orientation, + // so we just need to readjust the origin, and perform rotation about (0, 0). + boolean isClockwise = (endRotation - startRotation) == -ROTATION_270; + Rect displayBounds = mPipDisplayLayoutState.getDisplayBounds(); + int originTranslateX = isClockwise ? 0 : -displayBounds.width(); + int originTranslateY = isClockwise ? -displayBounds.height() : 0; + endBounds.offset(originTranslateX, originTranslateY); + + // Update the activity end bounds in place as well, as this is used for transform + // calculation later. + endActivityBounds.offsetTo(endBounds.left + activityEndOffset.x, + endBounds.top + activityEndOffset.y); + } + + private boolean startAlphaTypeEnterAnimation(@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @@ -533,6 +592,19 @@ public class PipTransition extends PipTransitionController implements } @Nullable + private TransitionInfo.Change getDeferConfigActivityChange(TransitionInfo info, + @NonNull WindowContainerToken parent) { + for (TransitionInfo.Change change : info.getChanges()) { + if (change.getTaskInfo() == null + && change.hasFlags(TransitionInfo.FLAG_CONFIG_AT_END) + && change.getParent() != null && change.getParent().equals(parent)) { + return change; + } + } + return null; + } + + @Nullable private TransitionInfo.Change getChangeByToken(TransitionInfo info, WindowContainerToken token) { for (TransitionInfo.Change change : info.getChanges()) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java index a132796f4a84..ccdd66b5d1a8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java @@ -155,6 +155,8 @@ public class PipTransitionState { @Nullable private Runnable mOnIdlePipTransitionStateRunnable; + private boolean mInFixedRotation = false; + /** * An interface to track state updates as we progress through PiP transitions. */ @@ -256,7 +258,7 @@ public class PipTransitionState { private void maybeRunOnIdlePipTransitionStateCallback() { if (mOnIdlePipTransitionStateRunnable != null && isPipStateIdle()) { - mOnIdlePipTransitionStateRunnable.run(); + mMainHandler.post(mOnIdlePipTransitionStateRunnable); mOnIdlePipTransitionStateRunnable = null; } } @@ -303,6 +305,23 @@ public class PipTransitionState { } /** + * @return true if either in swipe or button-nav fixed rotation. + */ + public boolean isInFixedRotation() { + return mInFixedRotation; + } + + /** + * Sets the fixed rotation flag. + */ + public void setInFixedRotation(boolean inFixedRotation) { + mInFixedRotation = inFixedRotation; + if (!inFixedRotation) { + maybeRunOnIdlePipTransitionStateCallback(); + } + } + + /** * @return true if in swipe PiP to home. Note that this is true until overlay fades if used too. */ public boolean isInSwipePipToHomeTransition() { @@ -351,7 +370,7 @@ public class PipTransitionState { public boolean isPipStateIdle() { // This needs to be a valid in-PiP state that isn't a transient state. - return mState == ENTERED_PIP || mState == CHANGED_PIP_BOUNDS; + return (mState == ENTERED_PIP || mState == CHANGED_PIP_BOUNDS) && !isInFixedRotation(); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java index 9e39f440915c..a23b576beebc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java @@ -772,15 +772,25 @@ public class SplitScreenController implements SplitDragPolicy.Starter, instanceId); } + @Override + public void startIntent(PendingIntent intent, int userId1, @Nullable Intent fillInIntent, + @SplitPosition int position, @Nullable Bundle options, + @Nullable WindowContainerToken hideTaskToken) { + startIntent(intent, userId1, fillInIntent, position, options, hideTaskToken, + false /* forceLaunchNewTask */); + } + /** * Starts the given intent into split. + * * @param hideTaskToken If non-null, a task matching this token will be moved to back in the * same window container transaction as the starting of the intent. + * @param forceLaunchNewTask If true, this method will skip the check for a background task + * matching the intent and launch a new task. */ - @Override public void startIntent(PendingIntent intent, int userId1, @Nullable Intent fillInIntent, @SplitPosition int position, @Nullable Bundle options, - @Nullable WindowContainerToken hideTaskToken) { + @Nullable WindowContainerToken hideTaskToken, boolean forceLaunchNewTask) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN, "startIntent(): intent=%s user=%d fillInIntent=%s position=%d", intent, userId1, fillInIntent, position); @@ -798,8 +808,9 @@ public class SplitScreenController implements SplitDragPolicy.Starter, // To prevent accumulating large number of instances in the background, reuse task // in the background. If we don't explicitly reuse, new may be created even if the app // isn't multi-instance because WM won't automatically remove/reuse the previous instance - final ActivityManager.RecentTaskInfo taskInfo = mRecentTasksOptional - .map(recentTasks -> recentTasks.findTaskInBackground(component, userId1, + final ActivityManager.RecentTaskInfo taskInfo = forceLaunchNewTask ? null : + mRecentTasksOptional + .map(recentTasks -> recentTasks.findTaskInBackground(component, userId1, hideTaskToken)) .orElse(null); if (taskInfo != null) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java index ae4bd1615ae1..6313231b449e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageTaskListener.java @@ -244,8 +244,9 @@ public class StageTaskListener implements ShellTaskOrganizer.TaskListener { return; } mChildrenTaskInfo.put(taskInfo.taskId, taskInfo); + mVisible = taskInfo.isVisible && taskInfo.isVisibleRequested; mCallbacks.onChildTaskStatusChanged(this, taskInfo.taskId, true /* present */, - taskInfo.isVisible && taskInfo.isVisibleRequested); + mVisible); } else { throw new IllegalArgumentException(this + "\n Unknown task: " + taskInfo + "\n mRootTaskInfo: " + mRootTaskInfo); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt index 0cb219ae4b81..3ae5a1afc7e2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt @@ -62,6 +62,7 @@ import com.android.wm.shell.R import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.SyncTransactionQueue +import com.android.wm.shell.desktopmode.calculateMaximizeBounds import com.android.wm.shell.shared.animation.Interpolators.EMPHASIZED_DECELERATE import com.android.wm.shell.shared.animation.Interpolators.FAST_OUT_LINEAR_IN import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewHostViewContainer @@ -73,7 +74,8 @@ import java.util.function.Supplier /** * Menu that appears when user long clicks the maximize button. Gives the user the option to - * maximize the task or snap the task to the right or left half of the screen. + * maximize the task or restore previous task bounds from the maximized state and to snap the task + * to the right or left half of the screen. */ class MaximizeMenu( private val syncQueue: SyncTransactionQueue, @@ -176,6 +178,7 @@ class MaximizeMenu( "MaximizeMenu") maximizeMenuView = MaximizeMenuView( context = decorWindowContext, + sizeToggleDirection = getSizeToggleDirection(), menuHeight = menuHeight, menuPadding = menuPadding, ).also { menuView -> @@ -202,6 +205,18 @@ class MaximizeMenu( } } + private fun getSizeToggleDirection(): MaximizeMenuView.SizeToggleDirection { + val maximizeBounds = calculateMaximizeBounds( + displayController.getDisplayLayout(taskInfo.displayId)!!, + taskInfo + ) + val maximized = taskInfo.configuration.windowConfiguration.bounds.equals(maximizeBounds) + return if (maximized) + MaximizeMenuView.SizeToggleDirection.RESTORE + else + MaximizeMenuView.SizeToggleDirection.MAXIMIZE + } + private fun loadDimensionPixelSize(resourceId: Int): Int { return if (resourceId == Resources.ID_NULL) { 0 @@ -236,18 +251,19 @@ class MaximizeMenu( * resizing a Task. */ class MaximizeMenuView( - context: Context, + private val context: Context, + private val sizeToggleDirection: SizeToggleDirection, private val menuHeight: Int, - private val menuPadding: Int, + private val menuPadding: Int ) { val rootView = LayoutInflater.from(context) .inflate(R.layout.desktop_mode_window_decor_maximize_menu, null /* root */) as ViewGroup private val container = requireViewById(R.id.container) private val overlay = requireViewById(R.id.maximize_menu_overlay) - private val maximizeText = - requireViewById(R.id.maximize_menu_maximize_window_text) as TextView - private val maximizeButton = - requireViewById(R.id.maximize_menu_maximize_button) as Button + private val sizeToggleButtonText = + requireViewById(R.id.maximize_menu_size_toggle_button_text) as TextView + private val sizeToggleButton = + requireViewById(R.id.maximize_menu_size_toggle_button) as Button private val snapWindowText = requireViewById(R.id.maximize_menu_snap_window_text) as TextView private val snapRightButton = @@ -263,8 +279,6 @@ class MaximizeMenu( .getDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_buttons_outline_radius) private val outlineStroke = context.resources .getDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_buttons_outline_stroke) - private val fillPadding = context.resources - .getDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_buttons_fill_padding) private val fillRadius = context.resources .getDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_buttons_fill_radius) @@ -324,7 +338,7 @@ class MaximizeMenu( return@setOnHoverListener false } - maximizeButton.setOnClickListener { onMaximizeClickListener?.invoke() } + sizeToggleButton.setOnClickListener { onMaximizeClickListener?.invoke() } snapRightButton.setOnClickListener { onRightSnapClickListener?.invoke() } snapLeftButton.setOnClickListener { onLeftSnapClickListener?.invoke() } rootView.setOnTouchListener { _, event -> @@ -335,9 +349,17 @@ class MaximizeMenu( true } + val btnTextId = if (sizeToggleDirection == SizeToggleDirection.RESTORE) + R.string.desktop_mode_maximize_menu_restore_button_text + else + R.string.desktop_mode_maximize_menu_maximize_button_text + val btnText = context.resources.getText(btnTextId) + sizeToggleButton.contentDescription = btnText + sizeToggleButtonText.text = btnText + // To prevent aliasing. - maximizeButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null) - maximizeText.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + sizeToggleButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + sizeToggleButtonText.setLayerType(View.LAYER_TYPE_SOFTWARE, null) } /** Bind the menu views to the new [RunningTaskInfo] data. */ @@ -348,8 +370,8 @@ class MaximizeMenu( rootView.background.setTint(style.backgroundColor) // Maximize option. - maximizeButton.background = style.maximizeOption.drawable - maximizeText.setTextColor(style.textColor) + sizeToggleButton.background = style.maximizeOption.drawable + sizeToggleButtonText.setTextColor(style.textColor) // Snap options. snapWindowText.setTextColor(style.textColor) @@ -358,8 +380,8 @@ class MaximizeMenu( /** Animate the opening of the menu */ fun animateOpenMenu(onEnd: () -> Unit) { - maximizeButton.setLayerType(View.LAYER_TYPE_HARDWARE, null) - maximizeText.setLayerType(View.LAYER_TYPE_HARDWARE, null) + sizeToggleButton.setLayerType(View.LAYER_TYPE_HARDWARE, null) + sizeToggleButtonText.setLayerType(View.LAYER_TYPE_HARDWARE, null) menuAnimatorSet = AnimatorSet() menuAnimatorSet?.playTogether( ObjectAnimator.ofFloat(rootView, SCALE_Y, STARTING_MENU_HEIGHT_SCALE, 1f) @@ -388,9 +410,9 @@ class MaximizeMenu( // Scale up the children of the maximize menu so that the menu // scale is cancelled out and only the background is scaled. val value = animatedValue as Float - maximizeButton.scaleY = value + sizeToggleButton.scaleY = value snapButtonsLayout.scaleY = value - maximizeText.scaleY = value + sizeToggleButtonText.scaleY = value snapWindowText.scaleY = value } }, @@ -409,9 +431,9 @@ class MaximizeMenu( startDelay = CONTROLS_ALPHA_OPEN_MENU_ANIMATION_DELAY_MS addUpdateListener { val value = animatedValue as Float - maximizeButton.alpha = value + sizeToggleButton.alpha = value snapButtonsLayout.alpha = value - maximizeText.alpha = value + sizeToggleButtonText.alpha = value snapWindowText.alpha = value } }, @@ -423,8 +445,8 @@ class MaximizeMenu( ) menuAnimatorSet?.addListener( onEnd = { - maximizeButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null) - maximizeText.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + sizeToggleButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + sizeToggleButtonText.setLayerType(View.LAYER_TYPE_SOFTWARE, null) onEnd.invoke() } ) @@ -433,8 +455,8 @@ class MaximizeMenu( /** Animate the closing of the menu */ fun animateCloseMenu(onEnd: (() -> Unit)) { - maximizeButton.setLayerType(View.LAYER_TYPE_HARDWARE, null) - maximizeText.setLayerType(View.LAYER_TYPE_HARDWARE, null) + sizeToggleButton.setLayerType(View.LAYER_TYPE_HARDWARE, null) + sizeToggleButtonText.setLayerType(View.LAYER_TYPE_HARDWARE, null) cancelAnimation() menuAnimatorSet = AnimatorSet() menuAnimatorSet?.playTogether( @@ -464,9 +486,9 @@ class MaximizeMenu( // Scale up the children of the maximize menu so that the menu // scale is cancelled out and only the background is scaled. val value = animatedValue as Float - maximizeButton.scaleY = value + sizeToggleButton.scaleY = value snapButtonsLayout.scaleY = value - maximizeText.scaleY = value + sizeToggleButtonText.scaleY = value snapWindowText.scaleY = value } }, @@ -485,9 +507,9 @@ class MaximizeMenu( duration = ALPHA_ANIMATION_DURATION_MS addUpdateListener { val value = animatedValue as Float - maximizeButton.alpha = value + sizeToggleButton.alpha = value snapButtonsLayout.alpha = value - maximizeText.alpha = value + sizeToggleButtonText.alpha = value snapWindowText.alpha = value } }, @@ -498,8 +520,8 @@ class MaximizeMenu( ) menuAnimatorSet?.addListener( onEnd = { - maximizeButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null) - maximizeText.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + sizeToggleButton.setLayerType(View.LAYER_TYPE_SOFTWARE, null) + sizeToggleButtonText.setLayerType(View.LAYER_TYPE_SOFTWARE, null) onEnd?.invoke() } ) @@ -509,8 +531,8 @@ class MaximizeMenu( /** Request that the accessibility service focus on the menu. */ fun requestAccessibilityFocus() { // Focus the first button in the menu by default. - maximizeButton.post { - maximizeButton.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED) + sizeToggleButton.post { + sizeToggleButton.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED) } } @@ -685,15 +707,31 @@ class MaximizeMenu( paint.color = strokeAndFillColor paint.style = Paint.Style.FILL }) + + val (horizontalFillPadding, verticalFillPadding) = + if (sizeToggleDirection == SizeToggleDirection.MAXIMIZE) { + context.resources.getDimensionPixelSize(R.dimen + .desktop_mode_maximize_menu_snap_and_maximize_buttons_fill_padding) to + context.resources.getDimensionPixelSize(R.dimen + .desktop_mode_maximize_menu_snap_and_maximize_buttons_fill_padding) + } else { + context.resources.getDimensionPixelSize(R.dimen + .desktop_mode_maximize_menu_restore_button_fill_horizontal_padding) to + context.resources.getDimensionPixelSize(R.dimen + .desktop_mode_maximize_menu_restore_button_fill_vertical_padding) + } + return LayerDrawable(layers.toTypedArray()).apply { when (numberOfLayers) { 3 -> { setLayerInset(1, outlineStroke) - setLayerInset(2, fillPadding) + setLayerInset(2, horizontalFillPadding, verticalFillPadding, + horizontalFillPadding, verticalFillPadding) } 4 -> { setLayerInset(intArrayOf(1, 2), outlineStroke) - setLayerInset(3, fillPadding) + setLayerInset(3, horizontalFillPadding, verticalFillPadding, + horizontalFillPadding, verticalFillPadding) } else -> error("Unexpected number of layers: $numberOfLayers") } @@ -737,6 +775,11 @@ class MaximizeMenu( enum class SnapToHalfSelection { NONE, LEFT, RIGHT } + + /** The possible selection states of the size toggle button in the maximize menu. */ + enum class SizeToggleDirection { + MAXIMIZE, RESTORE + } } companion object { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index b3c10d64c3a3..f9376570dc83 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -2902,7 +2902,8 @@ class DesktopTasksControllerTest : ShellTestCase() { runOpenNewWindow(task) verify(splitScreenController) .startIntent(any(), anyInt(), any(), any(), - optionsCaptor.capture(), anyOrNull()) + optionsCaptor.capture(), anyOrNull(), eq(true) + ) assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode) .isEqualTo(WINDOWING_MODE_MULTI_WINDOW) } @@ -2917,7 +2918,7 @@ class DesktopTasksControllerTest : ShellTestCase() { verify(splitScreenController) .startIntent( any(), anyInt(), any(), any(), - optionsCaptor.capture(), anyOrNull() + optionsCaptor.capture(), anyOrNull(), eq(true) ) assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode) .isEqualTo(WINDOWING_MODE_MULTI_WINDOW) @@ -2984,6 +2985,58 @@ class DesktopTasksControllerTest : ShellTestCase() { .launchWindowingMode).isEqualTo(WINDOWING_MODE_FREEFORM) } + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES) + fun openInstance_fromFreeform_minimizesIfNeeded() { + setUpLandscapeDisplay() + val homeTask = setUpHomeTask() + val freeformTasks = (1..MAX_TASK_LIMIT + 1).map { _ -> setUpFreeformTask() } + val oldestTask = freeformTasks.first() + val newestTask = freeformTasks.last() + + runOpenInstance(newestTask, freeformTasks[1].taskId) + + val wct = getLatestWct(type = TRANSIT_OPEN) + // Home is moved to front of everything. + assertThat( + wct.hierarchyOps.any { hop -> + hop.container == homeTask.token.asBinder() && hop.toTop + } + ).isTrue() + // And the oldest task isn't moved in front of home, effectively minimizing it. + assertThat( + wct.hierarchyOps.none { hop -> + hop.container == oldestTask.token.asBinder() && hop.toTop + } + ).isTrue() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES) + fun openInstance_fromFreeform_exitsImmersiveIfNeeded() { + setUpLandscapeDisplay() + val homeTask = setUpHomeTask() + val freeformTask = setUpFreeformTask() + val immersiveTask = setUpFreeformTask() + taskRepository.setTaskInFullImmersiveState( + displayId = immersiveTask.displayId, + taskId = immersiveTask.taskId, + immersive = true + ) + val runOnStartTransit = RunOnStartTransitionCallback() + val transition = Binder() + whenever(transitions.startTransition(eq(TRANSIT_OPEN), any(), anyOrNull())) + .thenReturn(transition) + whenever(mockDesktopFullImmersiveTransitionHandler + .exitImmersiveIfApplicable(any(), eq(immersiveTask.displayId))).thenReturn(runOnStartTransit) + + runOpenInstance(immersiveTask, freeformTask.taskId) + + verify(mockDesktopFullImmersiveTransitionHandler) + .exitImmersiveIfApplicable(any(), eq(immersiveTask.displayId)) + runOnStartTransit.assertOnlyInvocation(transition) + } + private fun runOpenInstance( callingTask: RunningTaskInfo, requestedTaskId: Int diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java index 9260a07fd945..ef3af8e7bdac 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitScreenControllerTests.java @@ -297,6 +297,28 @@ public class SplitScreenControllerTests extends ShellTestCase { } @Test + public void startIntent_forceLaunchNewTaskTrue_skipsBackgroundTasks() { + Intent startIntent = createStartIntent("startActivity"); + PendingIntent pendingIntent = + PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE); + mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null, + SPLIT_POSITION_TOP_OR_LEFT, null /* options */, null /* hideTaskToken */, + true /* forceLaunchNewTask */); + verify(mRecentTasks, never()).findTaskInBackground(any(), anyInt(), any()); + } + + @Test + public void startIntent_forceLaunchNewTaskFalse_checksBackgroundTasks() { + Intent startIntent = createStartIntent("startActivity"); + PendingIntent pendingIntent = + PendingIntent.getActivity(mContext, 0, startIntent, FLAG_IMMUTABLE); + mSplitScreenController.startIntent(pendingIntent, mContext.getUserId(), null, + SPLIT_POSITION_TOP_OR_LEFT, null /* options */, null /* hideTaskToken */, + false /* forceLaunchNewTask */); + verify(mRecentTasks).findTaskInBackground(any(), anyInt(), any()); + } + + @Test public void testSwitchSplitPosition_checksIsSplitScreenVisible() { final String reason = "test"; when(mSplitScreenController.isSplitScreenVisible()).thenReturn(true, false); diff --git a/libs/appfunctions/api/current.txt b/libs/appfunctions/api/current.txt index e9845c1d9f13..27817e9eb984 100644 --- a/libs/appfunctions/api/current.txt +++ b/libs/appfunctions/api/current.txt @@ -4,7 +4,6 @@ package com.google.android.appfunctions.sidecar { public final class AppFunctionManager { ctor public AppFunctionManager(android.content.Context); method public void executeAppFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); - method @Deprecated public void executeAppFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); method public void isAppFunctionEnabled(@NonNull String, @NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,java.lang.Exception>); method public void setAppFunctionEnabled(@NonNull String, int, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,java.lang.Exception>); field public static final int APP_FUNCTION_STATE_DEFAULT = 0; // 0x0 @@ -15,9 +14,7 @@ package com.google.android.appfunctions.sidecar { public abstract class AppFunctionService extends android.app.Service { ctor public AppFunctionService(); method @NonNull public final android.os.IBinder onBind(@Nullable android.content.Intent); - method @MainThread public void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull String, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); - method @Deprecated @MainThread public void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); - method @Deprecated @MainThread public void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); + method @MainThread public abstract void onExecuteFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull String, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); field @NonNull public static final String BIND_APP_FUNCTION_SERVICE = "android.permission.BIND_APP_FUNCTION_SERVICE"; field @NonNull public static final String SERVICE_INTERFACE = "android.app.appfunctions.AppFunctionService"; } diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java index d660926575d1..43377d8eb91c 100644 --- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java +++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java @@ -126,33 +126,6 @@ public final class AppFunctionManager { } /** - * Executes the app function. - * - * <p>Proxies request and response to the underlying {@link - * android.app.appfunctions.AppFunctionManager#executeAppFunction}, converting the request and - * response in the appropriate type required by the function. - * - * @deprecated Use {@link #executeAppFunction(ExecuteAppFunctionRequest, Executor, - * CancellationSignal, Consumer)} instead. This method will be removed once usage references - * are updated. - */ - @Deprecated - public void executeAppFunction( - @NonNull ExecuteAppFunctionRequest sidecarRequest, - @NonNull @CallbackExecutor Executor executor, - @NonNull Consumer<ExecuteAppFunctionResponse> callback) { - Objects.requireNonNull(sidecarRequest); - Objects.requireNonNull(executor); - Objects.requireNonNull(callback); - - executeAppFunction( - sidecarRequest, - executor, - new CancellationSignal(), - callback); - } - - /** * Returns a boolean through a callback, indicating whether the app function is enabled. * * <p>* This method can only check app functions owned by the caller, or those where the caller diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java index 2a168e871713..0dc87e45b7e3 100644 --- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java +++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionService.java @@ -119,76 +119,9 @@ public abstract class AppFunctionService extends Service { * @param callback A callback to report back the result. */ @MainThread - public void onExecuteFunction( + public abstract void onExecuteFunction( @NonNull ExecuteAppFunctionRequest request, @NonNull String callingPackage, @NonNull CancellationSignal cancellationSignal, - @NonNull Consumer<ExecuteAppFunctionResponse> callback) { - onExecuteFunction(request, cancellationSignal, callback); - } - - /** - * Called by the system to execute a specific app function. - * - * <p>This method is triggered when the system requests your AppFunctionService to handle a - * particular function you have registered and made available. - * - * <p>To ensure proper routing of function requests, assign a unique identifier to each - * function. This identifier doesn't need to be globally unique, but it must be unique within - * your app. For example, a function to order food could be identified as "orderFood". In most - * cases this identifier should come from the ID automatically generated by the AppFunctions - * SDK. You can determine the specific function to invoke by calling {@link - * ExecuteAppFunctionRequest#getFunctionIdentifier()}. - * - * <p>This method is always triggered in the main thread. You should run heavy tasks on a worker - * thread and dispatch the result with the given callback. You should always report back the - * result using the callback, no matter if the execution was successful or not. - * - * @param request The function execution request. - * @param cancellationSignal A {@link CancellationSignal} to cancel the request. - * @param callback A callback to report back the result. - * @deprecated Use {@link #onExecuteFunction(ExecuteAppFunctionRequest, String, - * CancellationSignal, Consumer)} instead. This method will be removed once usage references - * are updated. - */ - @MainThread - @Deprecated - public void onExecuteFunction( - @NonNull ExecuteAppFunctionRequest request, - @NonNull CancellationSignal cancellationSignal, - @NonNull Consumer<ExecuteAppFunctionResponse> callback) { - onExecuteFunction(request, callback); - } - - /** - * Called by the system to execute a specific app function. - * - * <p>This method is triggered when the system requests your AppFunctionService to handle a - * particular function you have registered and made available. - * - * <p>To ensure proper routing of function requests, assign a unique identifier to each - * function. This identifier doesn't need to be globally unique, but it must be unique within - * your app. For example, a function to order food could be identified as "orderFood". In most - * cases this identifier should come from the ID automatically generated by the AppFunctions - * SDK. You can determine the specific function to invoke by calling {@link - * ExecuteAppFunctionRequest#getFunctionIdentifier()}. - * - * <p>This method is always triggered in the main thread. You should run heavy tasks on a worker - * thread and dispatch the result with the given callback. You should always report back the - * result using the callback, no matter if the execution was successful or not. - * - * @param request The function execution request. - * @param callback A callback to report back the result. - * @deprecated Use {@link #onExecuteFunction(ExecuteAppFunctionRequest, CancellationSignal, - * Consumer)} instead. This method will be removed once usage references are updated. - */ - @MainThread - @Deprecated - public void onExecuteFunction( - @NonNull ExecuteAppFunctionRequest request, - @NonNull Consumer<ExecuteAppFunctionResponse> callback) { - Log.w( - "AppFunctionService", - "Calling deprecated default implementation of onExecuteFunction"); - } + @NonNull Consumer<ExecuteAppFunctionResponse> callback); } diff --git a/libs/hwui/jni/text/TextShaper.cpp b/libs/hwui/jni/text/TextShaper.cpp index 456338631ae4..70e6beda6cb9 100644 --- a/libs/hwui/jni/text/TextShaper.cpp +++ b/libs/hwui/jni/text/TextShaper.cpp @@ -31,12 +31,34 @@ namespace android { +struct FakedFontKey { + uint32_t operator()(const minikin::FakedFont& fakedFont) const { + return minikin::Hasher() + .update(reinterpret_cast<uintptr_t>(fakedFont.font.get())) + .update(fakedFont.fakery.bits()) + .update(fakedFont.fakery.variationSettings()) + .hash(); + } +}; + struct LayoutWrapper { LayoutWrapper(minikin::Layout&& layout, float ascent, float descent) : layout(std::move(layout)), ascent(ascent), descent(descent) {} + + LayoutWrapper(minikin::Layout&& layout, float ascent, float descent, std::vector<jlong>&& fonts, + std::vector<uint32_t>&& fontIds) + : layout(std::move(layout)) + , ascent(ascent) + , descent(descent) + , fonts(std::move(fonts)) + , fontIds(std::move(fontIds)) {} + minikin::Layout layout; float ascent; float descent; + + std::vector<jlong> fonts; + std::vector<uint32_t> fontIds; // per glyph }; static void releaseLayout(jlong ptr) { @@ -64,6 +86,43 @@ static jlong shapeTextRun(const uint16_t* text, int textSize, int start, int cou overallDescent = std::max(overallDescent, extent.descent); } + if (text_feature::typeface_redesign()) { + uint32_t runCount = layout.getFontRunCount(); + + std::unordered_map<minikin::FakedFont, uint32_t, FakedFontKey> fakedToFontIds; + std::vector<jlong> fonts; + std::vector<uint32_t> fontIds; + + fontIds.resize(layout.nGlyphs()); + for (uint32_t ri = 0; ri < runCount; ++ri) { + const minikin::FakedFont& fakedFont = layout.getFontRunFont(ri); + + auto it = fakedToFontIds.find(fakedFont); + uint32_t fontId; + if (it != fakedToFontIds.end()) { + fontId = it->second; // We've seen it. + } else { + fontId = fonts.size(); // This is new to us. Create new one. + std::shared_ptr<minikin::Font> font = std::make_shared<minikin::Font>( + fakedFont.font, fakedFont.fakery.variationSettings()); + fonts.push_back(reinterpret_cast<jlong>(new FontWrapper(std::move(font)))); + fakedToFontIds.insert(std::make_pair(fakedFont, fontId)); + } + + const uint32_t runStart = layout.getFontRunStart(ri); + const uint32_t runEnd = layout.getFontRunEnd(ri); + for (uint32_t i = runStart; i < runEnd; ++i) { + fontIds[i] = fontId; + } + } + + std::unique_ptr<LayoutWrapper> ptr = + std::make_unique<LayoutWrapper>(std::move(layout), overallAscent, overallDescent, + std::move(fonts), std::move(fontIds)); + + return reinterpret_cast<jlong>(ptr.release()); + } + std::unique_ptr<LayoutWrapper> ptr = std::make_unique<LayoutWrapper>( std::move(layout), overallAscent, overallDescent ); @@ -156,6 +215,8 @@ static jboolean TextShaper_Result_getFakeItalic(CRITICAL_JNI_PARAMS_COMMA jlong return layout->layout.getFakery(i).isFakeItalic(); } +constexpr float NO_OVERRIDE = -1; + float findValueFromVariationSettings(const minikin::FontFakery& fakery, minikin::AxisTag tag) { for (const minikin::FontVariation& fv : fakery.variationSettings()) { if (fv.axisTag == tag) { @@ -171,12 +232,7 @@ static jfloat TextShaper_Result_getWeightOverride(CRITICAL_JNI_PARAMS_COMMA jlon if (text_feature::typeface_redesign()) { float value = findValueFromVariationSettings(layout->layout.getFakery(i), minikin::TAG_wght); - if (!std::isnan(value)) { - return value; - } else { - const std::shared_ptr<minikin::Font>& font = layout->layout.getFontRef(i); - return font->style().weight(); - } + return std::isnan(value) ? NO_OVERRIDE : value; } else { return layout->layout.getFakery(i).wghtAdjustment(); } @@ -188,12 +244,7 @@ static jfloat TextShaper_Result_getItalicOverride(CRITICAL_JNI_PARAMS_COMMA jlon if (text_feature::typeface_redesign()) { float value = findValueFromVariationSettings(layout->layout.getFakery(i), minikin::TAG_ital); - if (!std::isnan(value)) { - return value; - } else { - const std::shared_ptr<minikin::Font>& font = layout->layout.getFontRef(i); - return font->style().isItalic(); - } + return std::isnan(value) ? NO_OVERRIDE : value; } else { return layout->layout.getFakery(i).italAdjustment(); } @@ -207,6 +258,24 @@ static jlong TextShaper_Result_getFont(CRITICAL_JNI_PARAMS_COMMA jlong ptr, jint } // CriticalNative +static jint TextShaper_Result_getFontCount(CRITICAL_JNI_PARAMS_COMMA jlong ptr) { + const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr); + return layout->fonts.size(); +} + +// CriticalNative +static jlong TextShaper_Result_getFontRef(CRITICAL_JNI_PARAMS_COMMA jlong ptr, jint fontId) { + const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr); + return layout->fonts[fontId]; +} + +// CriticalNative +static jint TextShaper_Result_getFontId(CRITICAL_JNI_PARAMS_COMMA jlong ptr, jint glyphIdx) { + const LayoutWrapper* layout = reinterpret_cast<LayoutWrapper*>(ptr); + return layout->fontIds[glyphIdx]; +} + +// CriticalNative static jlong TextShaper_Result_nReleaseFunc(CRITICAL_JNI_PARAMS) { return reinterpret_cast<jlong>(releaseLayout); } @@ -250,6 +319,10 @@ static const JNINativeMethod gResultMethods[] = { {"nGetWeightOverride", "(JI)F", (void*)TextShaper_Result_getWeightOverride}, {"nGetItalicOverride", "(JI)F", (void*)TextShaper_Result_getItalicOverride}, {"nReleaseFunc", "()J", (void*)TextShaper_Result_nReleaseFunc}, + + {"nGetFontCount", "(J)I", (void*)TextShaper_Result_getFontCount}, + {"nGetFontRef", "(JI)J", (void*)TextShaper_Result_getFontRef}, + {"nGetFontId", "(JI)I", (void*)TextShaper_Result_getFontId}, }; int register_android_graphics_text_TextShaper(JNIEnv* env) { diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v31/non_collapsing_toolbar_content_layout.xml b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v31/non_collapsing_toolbar_content_layout.xml index 33519cba2940..dd7eac776583 100644 --- a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v31/non_collapsing_toolbar_content_layout.xml +++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/layout-v31/non_collapsing_toolbar_content_layout.xml @@ -26,12 +26,12 @@ android:outlineAmbientShadowColor="@android:color/transparent" android:outlineSpotShadowColor="@android:color/transparent" android:background="@android:color/transparent" - android:theme="@style/Theme.CollapsingToolbar.Settings"> + android:theme="@style/ThemeOverlay.MaterialComponents.PlatformBridge.CollapsingToolbar"> <Toolbar android:id="@+id/action_bar" android:layout_width="match_parent" - android:layout_height="?attr/actionBarSize" + android:layout_height="?android:attr/actionBarSize" android:theme="?android:attr/actionBarTheme" android:transitionName="shared_element_view" app:layout_collapseMode="pin"/> diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-night-v31/themes.xml b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-night-v31/themes.xml index c20beaf9bf93..02f171cf0d9e 100644 --- a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-night-v31/themes.xml +++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-night-v31/themes.xml @@ -21,4 +21,15 @@ <item name="colorPrimary">@color/settingslib_primary_dark_device_default_settings</item> <item name="colorAccent">@color/settingslib_accent_device_default_dark</item> </style> -</resources>
\ No newline at end of file + + <!-- + ~ TODO(b/349675008): Remove this theme overlay once the platform bridge theme properly sets + ~ the MaterialComponents colors based on the platform theme. + --> + <style name="ThemeOverlay.MaterialComponents.PlatformBridge.CollapsingToolbar"> + <item name="elevationOverlayEnabled">true</item> + <item name="elevationOverlayColor">?attr/colorPrimary</item> + <item name="colorPrimary">@color/settingslib_primary_dark_device_default_settings</item> + <item name="colorAccent">@color/settingslib_accent_device_default_dark</item> + </style> +</resources> diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v31/themes.xml b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v31/themes.xml index 9ecc297c6d36..403931764d7e 100644 --- a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v31/themes.xml +++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v31/themes.xml @@ -21,4 +21,15 @@ <item name="colorPrimary">@color/settingslib_primary_device_default_settings_light</item> <item name="colorAccent">@color/settingslib_accent_device_default_light</item> </style> -</resources>
\ No newline at end of file + + <!-- + ~ TODO(b/349675008): Remove this theme overlay once the platform bridge theme properly sets + ~ the MaterialComponents colors based on the platform theme. + --> + <style name="ThemeOverlay.MaterialComponents.PlatformBridge.CollapsingToolbar"> + <item name="elevationOverlayEnabled">true</item> + <item name="elevationOverlayColor">?attr/colorPrimary</item> + <item name="colorPrimary">@color/settingslib_primary_device_default_settings_light</item> + <item name="colorAccent">@color/settingslib_accent_device_default_light</item> + </style> +</resources> diff --git a/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v31/themes_bridge.xml b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v31/themes_bridge.xml new file mode 100644 index 000000000000..bcb9baf94706 --- /dev/null +++ b/packages/SettingsLib/CollapsingToolbarBaseActivity/res/values-v31/themes_bridge.xml @@ -0,0 +1,176 @@ +<?xml version="1.0" encoding="utf-8"?> + +<!-- + Copyright (C) 2024 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. +--> + +<!-- See appcompat/appcompat/THEMES for the theme structure. --> +<resources> + <!-- + ~ Bridge theme overlay to simulate AppCompat themes based on a platform theme. + ~ Only non-widget attributes are included here since we should still use the platform widgets. + ~ Only public theme attributes (as in platform public-final.xml) can be referenced here since + ~ this is used in modules. + --> + <style name="Base.V31.ThemeOverlay.AppCompat.PlatformBridge" parent=""> + <!-- START Base.V7.Theme.AppCompat --> + + <item name="colorBackgroundFloating">?android:colorBackgroundFloating</item> + + <item name="isLightTheme">?android:isLightTheme</item> + + <item name="selectableItemBackground">?android:selectableItemBackground</item> + <item name="selectableItemBackgroundBorderless">?android:selectableItemBackgroundBorderless</item> + <item name="homeAsUpIndicator">?android:homeAsUpIndicator</item> + + <item name="dividerVertical">?android:dividerVertical</item> + <item name="dividerHorizontal">?android:dividerHorizontal</item> + + <!-- List attributes --> + <item name="textAppearanceListItem">?android:textAppearanceListItem</item> + <item name="textAppearanceListItemSmall">?android:textAppearanceListItemSmall</item> + <item name="textAppearanceListItemSecondary">?android:textAppearanceListItemSecondary</item> + <item name="listPreferredItemHeight">?android:listPreferredItemHeight</item> + <item name="listPreferredItemHeightSmall">?android:listPreferredItemHeightSmall</item> + <item name="listPreferredItemHeightLarge">?android:listPreferredItemHeightLarge</item> + <item name="listPreferredItemPaddingLeft">?android:listPreferredItemPaddingLeft</item> + <item name="listPreferredItemPaddingRight">?android:listPreferredItemPaddingRight</item> + <item name="listPreferredItemPaddingStart">?android:listPreferredItemPaddingStart</item> + <item name="listPreferredItemPaddingEnd">?android:listPreferredItemPaddingEnd</item> + + <!-- Color palette --> + <item name="colorPrimaryDark">?android:colorPrimaryDark</item> + <item name="colorPrimary">?android:colorPrimary</item> + <item name="colorAccent">?android:colorAccent</item> + + <item name="colorControlNormal">?android:colorControlNormal</item> + <item name="colorControlActivated">?android:colorControlActivated</item> + <item name="colorControlHighlight">?android:colorControlHighlight</item> + <item name="colorButtonNormal">?android:colorButtonNormal</item> + + <item name="colorError">?android:colorError</item> + + <!-- END Base.V7.Theme.AppCompat --> + </style> + <style name="Base.ThemeOverlay.AppCompat.PlatformBridge" parent="Base.V31.ThemeOverlay.AppCompat.PlatformBridge" /> + <style name="ThemeOverlay.AppCompat.PlatformBridge" parent="Base.ThemeOverlay.AppCompat.PlatformBridge" /> + + <!-- + ~ Bridge theme overlay to simulate MaterialComponents themes based on a platform theme. + --> + <style name="Base.V31.ThemeOverlay.MaterialComponents.PlatformBridge" parent="ThemeOverlay.AppCompat.PlatformBridge"> + <!-- START Base.V14.Theme.MaterialComponents.Bridge --> + <!-- + ~ This is copied as-is from the original bridge theme since it is guaranteed to not affect + ~ existing widgets. + --> + + <item name="isMaterialTheme">true</item> + + <item name="colorPrimaryVariant">@color/design_dark_default_color_primary_variant</item> + <item name="colorSecondary">@color/design_dark_default_color_secondary</item> + <item name="colorSecondaryVariant">@color/design_dark_default_color_secondary_variant</item> + <item name="colorSurface">@color/design_dark_default_color_surface</item> + <item name="colorPrimarySurface">?attr/colorSurface</item> + <item name="colorOnPrimary">@color/design_dark_default_color_on_primary</item> + <item name="colorOnSecondary">@color/design_dark_default_color_on_secondary</item> + <item name="colorOnBackground">@color/design_dark_default_color_on_background</item> + <item name="colorOnError">@color/design_dark_default_color_on_error</item> + <item name="colorOnSurface">@color/design_dark_default_color_on_surface</item> + <item name="colorOnPrimarySurface">?attr/colorOnSurface</item> + + <item name="scrimBackground">@color/mtrl_scrim_color</item> + <item name="popupMenuBackground">@drawable/mtrl_popupmenu_background_overlay</item> + + <item name="minTouchTargetSize">@dimen/mtrl_min_touch_target_size</item> + + <!-- MaterialComponents Widget styles --> + <item name="badgeStyle">@style/Widget.MaterialComponents.Badge</item> + <item name="bottomAppBarStyle">@style/Widget.MaterialComponents.BottomAppBar</item> + <item name="chipStyle">@style/Widget.MaterialComponents.Chip.Action</item> + <item name="chipGroupStyle">@style/Widget.MaterialComponents.ChipGroup</item> + <item name="chipStandaloneStyle">@style/Widget.MaterialComponents.Chip.Entry</item> + <item name="circularProgressIndicatorStyle">@style/Widget.MaterialComponents.CircularProgressIndicator</item> + <item name="extendedFloatingActionButtonStyle">@style/Widget.MaterialComponents.ExtendedFloatingActionButton.Icon</item> + <item name="linearProgressIndicatorStyle">@style/Widget.MaterialComponents.LinearProgressIndicator</item> + <item name="materialButtonStyle">@style/Widget.MaterialComponents.Button</item> + <item name="materialButtonOutlinedStyle">@style/Widget.MaterialComponents.Button.OutlinedButton</item> + <item name="materialButtonToggleGroupStyle">@style/Widget.MaterialComponents.MaterialButtonToggleGroup</item> + <item name="materialCardViewStyle">@style/Widget.MaterialComponents.CardView</item> + <item name="navigationRailStyle">@style/Widget.MaterialComponents.NavigationRailView</item> + <item name="sliderStyle">@style/Widget.MaterialComponents.Slider</item> + + <!-- Type styles --> + <item name="textAppearanceHeadline1">@style/TextAppearance.MaterialComponents.Headline1</item> + <item name="textAppearanceHeadline2">@style/TextAppearance.MaterialComponents.Headline2</item> + <item name="textAppearanceHeadline3">@style/TextAppearance.MaterialComponents.Headline3</item> + <item name="textAppearanceHeadline4">@style/TextAppearance.MaterialComponents.Headline4</item> + <item name="textAppearanceHeadline5">@style/TextAppearance.MaterialComponents.Headline5</item> + <item name="textAppearanceHeadline6">@style/TextAppearance.MaterialComponents.Headline6</item> + <item name="textAppearanceSubtitle1">@style/TextAppearance.MaterialComponents.Subtitle1</item> + <item name="textAppearanceSubtitle2">@style/TextAppearance.MaterialComponents.Subtitle2</item> + <item name="textAppearanceBody1">@style/TextAppearance.MaterialComponents.Body1</item> + <item name="textAppearanceBody2">@style/TextAppearance.MaterialComponents.Body2</item> + <item name="textAppearanceCaption">@style/TextAppearance.MaterialComponents.Caption</item> + <item name="textAppearanceButton">@style/TextAppearance.MaterialComponents.Button</item> + <item name="textAppearanceOverline">@style/TextAppearance.MaterialComponents.Overline</item> + + <!-- Shape styles --> + <item name="shapeAppearanceSmallComponent"> + @style/ShapeAppearance.MaterialComponents.SmallComponent + </item> + <item name="shapeAppearanceMediumComponent"> + @style/ShapeAppearance.MaterialComponents.MediumComponent + </item> + <item name="shapeAppearanceLargeComponent"> + @style/ShapeAppearance.MaterialComponents.LargeComponent + </item> + + <!-- Motion --> + <item name="motionEasingStandard">@string/material_motion_easing_standard</item> + <item name="motionEasingEmphasized">@string/material_motion_easing_emphasized</item> + <item name="motionEasingDecelerated">@string/material_motion_easing_decelerated</item> + <item name="motionEasingAccelerated">@string/material_motion_easing_accelerated</item> + <item name="motionEasingLinear">@string/material_motion_easing_linear</item> + + <item name="motionDurationShort1">@integer/material_motion_duration_short_1</item> + <item name="motionDurationShort2">@integer/material_motion_duration_short_2</item> + <item name="motionDurationMedium1">@integer/material_motion_duration_medium_1</item> + <item name="motionDurationMedium2">@integer/material_motion_duration_medium_2</item> + <item name="motionDurationLong1">@integer/material_motion_duration_long_1</item> + <item name="motionDurationLong2">@integer/material_motion_duration_long_2</item> + + <item name="motionPath">@integer/material_motion_path</item> + + <!-- Elevation Overlays --> + <item name="elevationOverlayEnabled">true</item> + <item name="elevationOverlayColor">?attr/colorOnSurface</item> + + <!-- END Base.V14.Theme.MaterialComponents.Bridge --> + + <!-- START Base.V14.Theme.MaterialComponents --> + <!-- + ~ Only a subset of widget attributes being actually used are included here since there are + ~ too many of them and they need to be investigated on a case-by-case basis. + --> + + <!-- Framework, AppCompat, or Design Widget styles --> + <item name="appBarLayoutStyle">@style/Widget.MaterialComponents.AppBarLayout.Surface</item> + + <!-- END Base.V14.Theme.MaterialComponents --> + </style> + <style name="Base.ThemeOverlay.MaterialComponents.PlatformBridge" parent="Base.V31.ThemeOverlay.AppCompat.PlatformBridge" /> + <style name="ThemeOverlay.MaterialComponents.PlatformBridge" parent="Base.ThemeOverlay.AppCompat.PlatformBridge" /> +</resources> diff --git a/packages/SettingsLib/Preference/testutils/com/android/settingslib/preference/CatalystScreenTestCase.kt b/packages/SettingsLib/Preference/testutils/com/android/settingslib/preference/CatalystScreenTestCase.kt index 1412c84c137b..89881f4d74bb 100644 --- a/packages/SettingsLib/Preference/testutils/com/android/settingslib/preference/CatalystScreenTestCase.kt +++ b/packages/SettingsLib/Preference/testutils/com/android/settingslib/preference/CatalystScreenTestCase.kt @@ -37,7 +37,7 @@ import org.junit.runner.RunWith abstract class CatalystScreenTestCase { @get:Rule val setFlagsRule = SetFlagsRule() - protected val context: Context = ApplicationProvider.getApplicationContext() + protected val appContext: Context = ApplicationProvider.getApplicationContext() /** Catalyst screen. */ protected abstract val preferenceScreenCreator: PreferenceScreenCreator @@ -52,12 +52,12 @@ abstract class CatalystScreenTestCase { @Test open fun migration() { enableCatalystScreen() - assertThat(preferenceScreenCreator.isFlagEnabled(context)).isTrue() + assertThat(preferenceScreenCreator.isFlagEnabled(appContext)).isTrue() val catalystScreen = dumpPreferenceScreen() Log.i(TAG, catalystScreen) disableCatalystScreen() - assertThat(preferenceScreenCreator.isFlagEnabled(context)).isFalse() + assertThat(preferenceScreenCreator.isFlagEnabled(appContext)).isFalse() val legacyScreen = dumpPreferenceScreen() assertThat(catalystScreen).isEqualTo(legacyScreen) diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt index dfd296fe006f..8636524ed23c 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt @@ -23,6 +23,7 @@ import com.android.settingslib.spa.framework.common.SpaEnvironment import com.android.settingslib.spa.framework.common.createSettingsPage import com.android.settingslib.spa.gallery.button.ActionButtonPageProvider import com.android.settingslib.spa.gallery.banner.BannerPageProvider +import com.android.settingslib.spa.gallery.card.CardPageProvider import com.android.settingslib.spa.gallery.chart.ChartPageProvider import com.android.settingslib.spa.gallery.dialog.DialogMainPageProvider import com.android.settingslib.spa.gallery.dialog.NavDialogProvider @@ -109,6 +110,7 @@ class GallerySpaEnvironment(context: Context) : SpaEnvironment(context) { TopIntroPreferencePageProvider, CheckBoxPreferencePageProvider, TwoTargetButtonPreferencePageProvider, + CardPageProvider, ), rootPages = listOf( HomePageProvider.createSettingsPage(), diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/card/CardPageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/card/CardPageProvider.kt new file mode 100644 index 000000000000..5659e2f33156 --- /dev/null +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/card/CardPageProvider.kt @@ -0,0 +1,104 @@ +/* + * 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.settingslib.spa.gallery.card + +import android.os.Bundle +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Stars +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.tooling.preview.Preview +import com.android.settingslib.spa.framework.common.SettingsPageProvider +import com.android.settingslib.spa.framework.compose.navigator +import com.android.settingslib.spa.framework.theme.SettingsTheme +import com.android.settingslib.spa.widget.card.SuggestionCard +import com.android.settingslib.spa.widget.card.SuggestionCardModel +import com.android.settingslib.spa.widget.preference.Preference +import com.android.settingslib.spa.widget.preference.PreferenceModel +import com.android.settingslib.spa.widget.scaffold.RegularScaffold + +object CardPageProvider : SettingsPageProvider { + override val name = "Card" + + override fun getTitle(arguments: Bundle?) = TITLE + + @Composable + override fun Page(arguments: Bundle?) { + RegularScaffold(title = TITLE) { + SuggestionCard() + SuggestionCardWithLongTitle() + SuggestionCardDismissible() + } + } + + @Composable + private fun SuggestionCard() { + SuggestionCard( + SuggestionCardModel( + title = "Suggestion card", + description = "Suggestion card description", + imageVector = Icons.Filled.Stars, + ) + ) + } + + @Composable + private fun SuggestionCardWithLongTitle() { + SuggestionCard( + SuggestionCardModel( + title = "Top level suggestion card with a really, really long title", + imageVector = Icons.Filled.Stars, + onClick = {}, + ) + ) + } + + @Composable + private fun SuggestionCardDismissible() { + var isVisible by rememberSaveable { mutableStateOf(true) } + SuggestionCard( + SuggestionCardModel( + title = "Suggestion card", + description = "Suggestion card description", + imageVector = Icons.Filled.Stars, + onDismiss = { isVisible = false }, + isVisible = isVisible, + ) + ) + } + + @Composable + fun Entry() { + Preference( + object : PreferenceModel { + override val title = TITLE + override val onClick = navigator(name) + } + ) + } + + private const val TITLE = "Sample Card" +} + +@Preview +@Composable +private fun CardPagePreview() { + SettingsTheme { CardPageProvider.Page(null) } +} diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt index 4d77ea173a85..ebfc0c536868 100644 --- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt +++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt @@ -27,6 +27,7 @@ import com.android.settingslib.spa.gallery.R import com.android.settingslib.spa.gallery.SettingsPageProviderEnum import com.android.settingslib.spa.gallery.banner.BannerPageProvider import com.android.settingslib.spa.gallery.button.ActionButtonPageProvider +import com.android.settingslib.spa.gallery.card.CardPageProvider import com.android.settingslib.spa.gallery.chart.ChartPageProvider import com.android.settingslib.spa.gallery.dialog.DialogMainPageProvider import com.android.settingslib.spa.gallery.editor.EditorMainPageProvider @@ -80,6 +81,7 @@ object HomePageProvider : SettingsPageProvider { DialogMainPageProvider.Entry() EditorMainPageProvider.Entry() BannerPageProvider.Entry() + CardPageProvider.Entry() CopyablePageProvider.Entry() } } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt index 395748384b85..08bedf99519d 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsDimension.kt @@ -28,6 +28,7 @@ object SettingsDimension { val paddingExtraSmall6 = 12.dp val paddingLarge = 16.dp val paddingExtraLarge = 24.dp + val paddingExtraLarge1 = 28.dp val spinnerHorizontalPadding = paddingExtraLarge val spinnerVerticalPadding = paddingLarge @@ -37,6 +38,7 @@ object SettingsDimension { val actionIconPadding = 4.dp val itemIconSize = 24.dp + val itemIconContainerSizeSmall = 40.dp val itemIconContainerSize = 72.dp val itemPaddingStart = if (isSpaExpressiveEnabled) paddingLarge else paddingExtraLarge val itemPaddingEnd = paddingLarge @@ -47,6 +49,12 @@ object SettingsDimension { end = itemPaddingEnd, bottom = itemPaddingVertical, ) + val footerItemPadding = PaddingValues( + start = paddingExtraLarge1, + top = itemPaddingVertical, + end = itemPaddingEnd, + bottom = itemPaddingVertical, + ) val textFieldPadding = PaddingValues( start = itemPaddingStart, end = itemPaddingEnd, diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsShape.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsShape.kt index 86ba6864574c..61607bc8ae8a 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsShape.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsShape.kt @@ -29,4 +29,6 @@ object SettingsShape { val CornerLarge = RoundedCornerShape(24.dp) val CornerExtraLarge = RoundedCornerShape(28.dp) + + val CornerExtraLarge1 = RoundedCornerShape(40.dp) } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SuggestionCard.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SuggestionCard.kt new file mode 100644 index 000000000000..2126634ebd4d --- /dev/null +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SuggestionCard.kt @@ -0,0 +1,166 @@ +/* + * Copyright (C) 2024 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.settingslib.spa.widget.card + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Stars +import androidx.compose.material.icons.outlined.Stars +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.tooling.preview.Preview +import com.android.settingslib.spa.framework.theme.SettingsDimension +import com.android.settingslib.spa.framework.theme.SettingsShape +import com.android.settingslib.spa.framework.theme.SettingsTheme +import com.android.settingslib.spa.framework.theme.toSemiBoldWeight + +data class SuggestionCardModel( + val title: String, + val description: String? = null, + val imageVector: ImageVector, + + /** + * A dismiss button will be displayed if this is not null. + * + * And this callback will be called when user clicks the button. + */ + val onDismiss: (() -> Unit)? = null, + val isVisible: Boolean = true, + val onClick: (() -> Unit)? = null, +) + +@Composable +fun SuggestionCard(model: SuggestionCardModel) { + AnimatedVisibility(visible = model.isVisible) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = + Modifier.padding( + horizontal = SettingsDimension.paddingLarge, + vertical = SettingsDimension.paddingSmall, + ) + .fillMaxWidth() + .heightIn(min = SettingsDimension.preferenceMinHeight) + .clip(SettingsShape.CornerExtraLarge1) + .background(MaterialTheme.colorScheme.secondaryContainer) + .then(model.onClick?.let { Modifier.clickable(onClick = it) } ?: Modifier) + .padding(SettingsDimension.paddingExtraSmall6), + ) { + SuggestionCardIcon(model.imageVector) + Spacer(Modifier.padding(SettingsDimension.paddingSmall)) + Column(modifier = Modifier.weight(1f).semantics(mergeDescendants = true) {}) { + SuggestionCardTitle(model.title) + if (model.description != null) SuggestionCardDescription(model.description) + } + if (model.onDismiss != null) { + Spacer(Modifier.padding(SettingsDimension.paddingSmall)) + SuggestionCardDismissButton(model.onDismiss) + } + } + } +} + +@Composable +private fun SuggestionCardIcon(imageVector: ImageVector) { + Box( + modifier = + Modifier.padding(SettingsDimension.paddingSmall) + .size(SettingsDimension.itemIconContainerSizeSmall) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.secondary), + contentAlignment = Alignment.Center, + ) { + Icon( + imageVector = imageVector, + contentDescription = null, + modifier = Modifier.size(SettingsDimension.itemIconSize), + tint = MaterialTheme.colorScheme.onSecondary, + ) + } +} + +@Composable +private fun SuggestionCardTitle(title: String) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium.toSemiBoldWeight(), + modifier = Modifier.padding(vertical = SettingsDimension.paddingTiny), + color = MaterialTheme.colorScheme.onSecondaryContainer, + ) +} + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +private fun SuggestionCardDescription(description: String) { + Text( + text = description, + style = MaterialTheme.typography.bodySmallEmphasized, + modifier = Modifier.padding(vertical = SettingsDimension.paddingTiny), + color = MaterialTheme.colorScheme.onSecondaryContainer, + ) +} + +@Composable +private fun SuggestionCardDismissButton(onDismiss: () -> Unit) { + IconButton(shape = CircleShape, onClick = onDismiss) { + Icon( + imageVector = Icons.Filled.Close, + contentDescription = + stringResource(androidx.compose.material3.R.string.m3c_snackbar_dismiss), + modifier = Modifier.size(SettingsDimension.itemIconSize), + tint = MaterialTheme.colorScheme.onSecondaryContainer, + ) + } +} + +@Preview +@Composable +private fun SuggestionCardPreview() { + SettingsTheme { + SuggestionCard( + SuggestionCardModel( + title = "Suggestion card", + description = "Suggestion card description", + imageVector = Icons.Outlined.Stars, + onDismiss = {}, + onClick = {}, + ) + ) + } +} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt index acbdec0b30aa..66680fa547b1 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt @@ -49,7 +49,9 @@ fun CategoryTitle(title: String) { text = title, modifier = Modifier.padding( - start = SettingsDimension.itemPaddingStart, + start = + if (isSpaExpressiveEnabled) SettingsDimension.paddingSmall + else SettingsDimension.itemPaddingStart, top = 20.dp, end = if (isSpaExpressiveEnabled) SettingsDimension.paddingSmall @@ -67,16 +69,16 @@ fun CategoryTitle(title: String) { */ @Composable fun Category(title: String? = null, content: @Composable ColumnScope.() -> Unit) { + var displayTitle by remember { mutableStateOf(false) } Column( modifier = - if (isSpaExpressiveEnabled) + if (isSpaExpressiveEnabled && displayTitle) Modifier.padding( horizontal = SettingsDimension.paddingLarge, vertical = SettingsDimension.paddingSmall, ) else Modifier ) { - var displayTitle by remember { mutableStateOf(false) } if (title != null && displayTitle) CategoryTitle(title = title) Column( modifier = diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/card/SettingsBannerTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/banner/SettingsBannerTest.kt index a8479b01a861..a8479b01a861 100644 --- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/card/SettingsBannerTest.kt +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/banner/SettingsBannerTest.kt diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/card/SettingsCollapsibleBannerTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/banner/SettingsCollapsibleBannerTest.kt index 1080fdea9455..1080fdea9455 100644 --- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/card/SettingsCollapsibleBannerTest.kt +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/banner/SettingsCollapsibleBannerTest.kt diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/card/SuggestionCardTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/card/SuggestionCardTest.kt new file mode 100644 index 000000000000..96bfb3d71642 --- /dev/null +++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/card/SuggestionCardTest.kt @@ -0,0 +1,84 @@ +/* + * 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.settingslib.spa.widget.card + +import android.content.Context +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Star +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.isNotDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class SuggestionCardTest { + @get:Rule val composeTestRule = createComposeRule() + + private val context: Context = ApplicationProvider.getApplicationContext() + + @Test + fun suggestionCard_contentDisplayed() { + setContent() + + composeTestRule.onNodeWithText(TITLE).assertIsDisplayed() + composeTestRule.onNodeWithText(DESCRIPTION).assertIsDisplayed() + } + + @Test + fun suggestionCard_dismiss() { + setContent() + composeTestRule + .onNodeWithContentDescription( + context.getString(androidx.compose.material3.R.string.m3c_snackbar_dismiss) + ) + .performClick() + + composeTestRule.onNodeWithText(TITLE).isNotDisplayed() + composeTestRule.onNodeWithText(DESCRIPTION).isNotDisplayed() + } + + private fun setContent() { + composeTestRule.setContent { + var isVisible by rememberSaveable { mutableStateOf(true) } + SuggestionCard( + SuggestionCardModel( + title = TITLE, + description = DESCRIPTION, + imageVector = Icons.Outlined.Star, + isVisible = isVisible, + onDismiss = { isVisible = false }, + ) + ) + } + } + + private companion object { + const val TITLE = "Title" + const val DESCRIPTION = "Description" + } +} diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt index f306918ec72f..d89d3977cac3 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt @@ -39,6 +39,8 @@ import androidx.compose.ui.unit.Dp import com.android.settingslib.development.DevelopmentSettingsEnabler import com.android.settingslib.spa.framework.compose.rememberDrawablePainter import com.android.settingslib.spa.framework.theme.SettingsDimension +import com.android.settingslib.spa.framework.theme.isSpaExpressiveEnabled +import com.android.settingslib.spa.widget.preference.IntroAppPreference import com.android.settingslib.spa.widget.ui.CopyableBody import com.android.settingslib.spa.widget.ui.SettingsBody import com.android.settingslib.spa.widget.ui.SettingsTitle @@ -48,23 +50,53 @@ import com.android.settingslib.spaprivileged.model.app.rememberAppRepository class AppInfoProvider(private val packageInfo: PackageInfo) { @Composable fun AppInfo(displayVersion: Boolean = false, isClonedAppPage: Boolean = false) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding( - horizontal = SettingsDimension.itemPaddingStart, - vertical = SettingsDimension.itemPaddingVertical, - ) - .semantics(mergeDescendants = true) {}, - horizontalAlignment = Alignment.CenterHorizontally, - ) { + if (isSpaExpressiveEnabled) { + val appRepository = rememberAppRepository() val app = checkNotNull(packageInfo.applicationInfo) - Box(modifier = Modifier.padding(SettingsDimension.itemPaddingAround)) { - AppIcon(app = app, size = SettingsDimension.appIconInfoSize) + val title = appRepository.produceLabel(app, isClonedAppPage).value + + val descriptions = mutableListOf<String>() + if (app.isInstantApp) { + descriptions.add( + stringResource( + com.android.settingslib.widget.preference.app.R.string.install_type_instant + ) + ) + } + if (displayVersion) { + val versionName = packageInfo.versionNameBidiWrapped + if (versionName != null) descriptions.add(versionName) + } + + IntroAppPreference( + title = title, + descriptions = descriptions, + appIcon = { + Image( + painter = rememberDrawablePainter(appRepository.produceIcon(app).value), + contentDescription = appRepository.produceIconContentDescription(app).value, + ) + }, + ) + } else { + Column( + modifier = + Modifier.fillMaxWidth() + .padding( + horizontal = SettingsDimension.itemPaddingStart, + vertical = SettingsDimension.itemPaddingVertical, + ) + .semantics(mergeDescendants = true) {}, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + val app = checkNotNull(packageInfo.applicationInfo) + Box(modifier = Modifier.padding(SettingsDimension.itemPaddingAround)) { + AppIcon(app = app, size = SettingsDimension.appIconInfoSize) + } + AppLabel(app, isClonedAppPage) + InstallType(app) + if (displayVersion) AppVersion() } - AppLabel(app, isClonedAppPage) - InstallType(app) - if (displayVersion) AppVersion() } } @@ -89,19 +121,24 @@ class AppInfoProvider(private val packageInfo: PackageInfo) { @Composable fun FooterAppVersion(showPackageName: Boolean = rememberIsDevelopmentSettingsEnabled()) { val context = LocalContext.current - val footer = remember(packageInfo, showPackageName) { - val list = mutableListOf<String>() - packageInfo.versionNameBidiWrapped?.let { - list += context.getString(R.string.version_text, it) + val footer = + remember(packageInfo, showPackageName) { + val list = mutableListOf<String>() + packageInfo.versionNameBidiWrapped?.let { + list += context.getString(R.string.version_text, it) + } + if (showPackageName) { + list += packageInfo.packageName + } + list.joinToString(separator = System.lineSeparator()) } - if (showPackageName) { - list += packageInfo.packageName - } - list.joinToString(separator = System.lineSeparator()) - } if (footer.isBlank()) return HorizontalDivider() - Column(modifier = Modifier.padding(SettingsDimension.itemPadding)) { + Column( + modifier = + if (isSpaExpressiveEnabled) Modifier.padding(SettingsDimension.footerItemPadding) + else Modifier.padding(SettingsDimension.itemPadding) + ) { CopyableBody(footer) } } @@ -109,9 +146,7 @@ class AppInfoProvider(private val packageInfo: PackageInfo) { @Composable private fun rememberIsDevelopmentSettingsEnabled(): Boolean { val context = LocalContext.current - return remember { - DevelopmentSettingsEnabler.isDevelopmentSettingsEnabled(context) - } + return remember { DevelopmentSettingsEnabler.isDevelopmentSettingsEnabled(context) } } private companion object { diff --git a/packages/SettingsLib/res/values/strings.xml b/packages/SettingsLib/res/values/strings.xml index abc58ee99904..fd2a1cb14edd 100644 --- a/packages/SettingsLib/res/values/strings.xml +++ b/packages/SettingsLib/res/values/strings.xml @@ -819,6 +819,11 @@ <!-- Summary of checkbox setting that enables the terminal app. [CHAR LIMIT=64] --> <string name="enable_terminal_summary">Enable terminal app that offers local shell access</string> + <!-- Title of checkbox setting that enables the Linux terminal app. [CHAR LIMIT=32] --> + <string name="enable_linux_terminal_title">Linux development environment</string> + <!-- Summary of checkbox setting that enables the Linux terminal app. [CHAR LIMIT=64] --> + <string name="enable_linux_terminal_summary">Run Linux terminal on Android</string> + <!-- HDCP checking title, used for debug purposes only. [CHAR LIMIT=25] --> <string name="hdcp_checking_title">HDCP checking</string> <!-- HDCP checking dialog title, used for debug purposes only. [CHAR LIMIT=25] --> diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InputMediaDevice.java b/packages/SettingsLib/src/com/android/settingslib/media/InputMediaDevice.java index 6335e712f904..83ee9751329f 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/InputMediaDevice.java +++ b/packages/SettingsLib/src/com/android/settingslib/media/InputMediaDevice.java @@ -125,7 +125,9 @@ public class InputMediaDevice extends MediaDevice { ? mProductName : mContext.getString(R.string.media_transfer_usb_device_mic_name); case TYPE_BLUETOOTH_SCO -> - mContext.getString(R.string.media_transfer_bt_device_mic_name); + mProductName != null + ? mProductName + : mContext.getString(R.string.media_transfer_bt_device_mic_name); default -> mContext.getString(R.string.media_transfer_this_device_name_desktop); }; } diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputMediaDeviceTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputMediaDeviceTest.java index 6c1cb7015225..7775b912e51d 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputMediaDeviceTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputMediaDeviceTest.java @@ -44,6 +44,7 @@ public class InputMediaDeviceTest { private static final String PRODUCT_NAME_BUILTIN_MIC = "Built-in Mic"; private static final String PRODUCT_NAME_WIRED_HEADSET = "My Wired Headset"; private static final String PRODUCT_NAME_USB_HEADSET = "My USB Headset"; + private static final String PRODUCT_NAME_BT_HEADSET = "My Bluetooth Headset"; @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @@ -142,6 +143,21 @@ public class InputMediaDeviceTest { MAX_VOLUME, CURRENT_VOLUME, IS_VOLUME_FIXED, + PRODUCT_NAME_BT_HEADSET); + assertThat(btMediaDevice).isNotNull(); + assertThat(btMediaDevice.getName()).isEqualTo(PRODUCT_NAME_BT_HEADSET); + } + + @Test + public void getName_returnCorrectName_btHeadset_nullProductName() { + InputMediaDevice btMediaDevice = + InputMediaDevice.create( + mContext, + String.valueOf(BT_HEADSET_ID), + AudioDeviceInfo.TYPE_BLUETOOTH_SCO, + MAX_VOLUME, + CURRENT_VOLUME, + IS_VOLUME_FIXED, null); assertThat(btMediaDevice).isNotNull(); assertThat(btMediaDevice.getName()) diff --git a/packages/Shell/AndroidManifest.xml b/packages/Shell/AndroidManifest.xml index 408ed1e861c3..b385aaa5c7d9 100644 --- a/packages/Shell/AndroidManifest.xml +++ b/packages/Shell/AndroidManifest.xml @@ -924,6 +924,7 @@ <!-- Permission required for CTS test - CtsPackageManagerTestCases--> <uses-permission android:name="android.permission.DOMAIN_VERIFICATION_AGENT" /> + <uses-permission android:name="android.permission.VERIFICATION_AGENT" /> <!-- Permission required for Cts test - CtsInputTestCases --> <uses-permission diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index 540115de8830..651244a9e52a 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -1497,3 +1497,13 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "secondary_user_widget_host" + namespace: "systemui" + description: "Host communal widgets in the current secondary user on HSUM." + bug: "373874416" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt index 9dc93484a638..4cf264253bf8 100644 --- a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt +++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt @@ -486,8 +486,8 @@ class TransitionAnimator( endState: State, windowBackgroundLayer: GradientDrawable, fadeWindowBackgroundLayer: Boolean = true, - useSpring: Boolean = false, drawHole: Boolean = false, + useSpring: Boolean = false, ): Animation { val transitionContainer = controller.transitionContainer val transitionContainerOverlay = transitionContainer.overlay diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/modifier/BurnInModifiers.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/modifier/BurnInModifiers.kt index 9444664885c8..71230f9cde12 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/modifier/BurnInModifiers.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/modifier/BurnInModifiers.kt @@ -17,8 +17,9 @@ package com.android.systemui.keyguard.ui.composable.modifier import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer @@ -41,15 +42,17 @@ fun Modifier.burnInAware( params: BurnInParameters, isClock: Boolean = false, ): Modifier { - val translationYState = remember { mutableStateOf(0F) } - viewModel.updateBurnInParams(params.copy(translationY = { translationYState.value })) + val cachedYTranslation = remember { mutableFloatStateOf(0f) } + LaunchedEffect(Unit) { + viewModel.updateBurnInParams(params.copy(translationY = { cachedYTranslation.floatValue })) + } val burnIn = viewModel.movement val translationX by burnIn.map { it.translationX.toFloat() }.collectAsStateWithLifecycle(initialValue = 0f) val translationY by burnIn.map { it.translationY.toFloat() }.collectAsStateWithLifecycle(initialValue = 0f) - translationYState.value = translationY + cachedYTranslation.floatValue = translationY val scaleViewModel by burnIn .map { BurnInScaleViewModel(scale = it.scale, scaleClockOnly = it.scaleClockOnly) } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ElementMatcher.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ElementMatcher.kt index ca68c256fd73..772872719ebe 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ElementMatcher.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/ElementMatcher.kt @@ -22,19 +22,52 @@ interface ElementMatcher { fun matches(key: ElementKey, content: ContentKey): Boolean } +/** Returns an [ElementMatcher] that matches any element in [content]. */ +fun inContent(content: ContentKey): ElementMatcher { + val matcherContent = content + return object : ElementMatcher { + override fun matches(key: ElementKey, content: ContentKey): Boolean { + return content == matcherContent + } + } +} + +/** Returns an [ElementMatcher] that matches all elements not matching [this] matcher. */ +operator fun ElementMatcher.not(): ElementMatcher { + val delegate = this + return object : ElementMatcher { + override fun matches(key: ElementKey, content: ContentKey): Boolean { + return !delegate.matches(key, content) + } + } +} + +/** + * Returns an [ElementMatcher] that matches all elements matching both [this] matcher and [other]. + */ +infix fun ElementMatcher.and(other: ElementMatcher): ElementMatcher { + val delegate = this + return object : ElementMatcher { + override fun matches(key: ElementKey, content: ContentKey): Boolean { + return delegate.matches(key, content) && other.matches(key, content) + } + } +} + /** - * Returns an [ElementMatcher] that matches elements in [content] also matching [this] - * [ElementMatcher]. + * Returns an [ElementMatcher] that matches all elements either [this] matcher, or [other], or both. */ -fun ElementMatcher.inContent(content: ContentKey): ElementMatcher { +infix fun ElementMatcher.or(other: ElementMatcher): ElementMatcher { val delegate = this - val matcherScene = content return object : ElementMatcher { override fun matches(key: ElementKey, content: ContentKey): Boolean { - return content == matcherScene && delegate.matches(key, content) + return delegate.matches(key, content) || other.matches(key, content) } } } -@Deprecated("Use inContent() instead", replaceWith = ReplaceWith("inContent(scene)")) -fun ElementMatcher.inScene(scene: SceneKey) = inContent(scene) +@Deprecated( + "Use `this and inContent()` instead", + replaceWith = ReplaceWith("this and inContent(scene)"), +) +fun ElementMatcher.inScene(scene: SceneKey) = this and inContent(scene) diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt index 061583a64702..a3f2a434cff7 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SwipeToScene.kt @@ -81,16 +81,16 @@ private class SwipeToSceneRootNode( draggableHandler: DraggableHandlerImpl, swipeDetector: SwipeDetector, ) : DelegatingNode() { - private var delegate = delegate(SwipeToSceneNode(draggableHandler, swipeDetector)) + private var delegateNode = delegate(SwipeToSceneNode(draggableHandler, swipeDetector)) fun update(draggableHandler: DraggableHandlerImpl, swipeDetector: SwipeDetector) { - if (draggableHandler == delegate.draggableHandler) { + if (draggableHandler == delegateNode.draggableHandler) { // Simple update, just update the swipe detector directly and keep the node. - delegate.swipeDetector = swipeDetector + delegateNode.swipeDetector = swipeDetector } else { // The draggableHandler changed, force recreate the underlying SwipeToSceneNode. - undelegate(delegate) - delegate = delegate(SwipeToSceneNode(draggableHandler, swipeDetector)) + undelegate(delegateNode) + delegateNode = delegate(SwipeToSceneNode(draggableHandler, swipeDetector)) } } } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementMatcherTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementMatcherTest.kt new file mode 100644 index 000000000000..af0962361fb2 --- /dev/null +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementMatcherTest.kt @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2024 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.compose.animation.scene + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.compose.animation.scene.TestElements.Bar +import com.android.compose.animation.scene.TestElements.Foo +import com.android.compose.animation.scene.TestScenes.SceneA +import com.android.compose.animation.scene.TestScenes.SceneB +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class ElementMatcherTest { + @Test + fun and() { + val matcher = Foo and inContent(SceneA) + assertThat(matcher.matches(Foo, SceneA)).isTrue() + assertThat(matcher.matches(Foo, SceneB)).isFalse() + assertThat(matcher.matches(Bar, SceneA)).isFalse() + assertThat(matcher.matches(Bar, SceneB)).isFalse() + } + + @Test + fun or() { + val matcher = Foo or inContent(SceneA) + assertThat(matcher.matches(Foo, SceneA)).isTrue() + assertThat(matcher.matches(Foo, SceneB)).isTrue() + assertThat(matcher.matches(Bar, SceneA)).isTrue() + assertThat(matcher.matches(Bar, SceneB)).isFalse() + } + + @Test + fun not() { + val matcher = !Foo + assertThat(matcher.matches(Foo, SceneA)).isFalse() + assertThat(matcher.matches(Foo, SceneB)).isFalse() + assertThat(matcher.matches(Bar, SceneA)).isTrue() + assertThat(matcher.matches(Bar, SceneB)).isTrue() + } +} diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt index 39d46990dc4b..ee807e6a7ede 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt @@ -2221,8 +2221,8 @@ class ElementTest { // In A => B, Foo is not shared and first fades out from A then fades in // B. sharedElement(TestElements.Foo, enabled = false) - fractionRange(end = 0.5f) { fade(TestElements.Foo.inContent(SceneA)) } - fractionRange(start = 0.5f) { fade(TestElements.Foo.inContent(SceneB)) } + fractionRange(end = 0.5f) { fade(TestElements.Foo.inScene(SceneA)) } + fractionRange(start = 0.5f) { fade(TestElements.Foo.inScene(SceneB)) } } from(SceneB, to = SceneA) { diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt index cae6617cb11b..7ea414d6b8cd 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/OverlayTest.kt @@ -35,6 +35,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.platform.testTag +import androidx.compose.ui.test.SemanticsMatcher import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.assertIsNotDisplayed import androidx.compose.ui.test.assertPositionInRootIsEqualTo @@ -205,7 +206,8 @@ class OverlayTest { val key = MovableElementKey("MovableBar", contents = setOf(SceneA, OverlayA, OverlayB)) val elementChildTag = "elementChildTag" - fun elementChild(content: ContentKey) = hasTestTag(elementChildTag) and inContent(content) + fun elementChild(content: ContentKey) = + hasTestTag(elementChildTag) and SemanticsMatcher.inContent(content) @Composable fun ContentScope.MovableBar() { @@ -773,7 +775,7 @@ class OverlayTest { // Overscroll on Overlay A. scope.launch { state.startTransition(transition(SceneA, OverlayA, progress = { 1.5f })) } rule - .onNode(hasTestTag(movableElementChildTag) and inContent(SceneA)) + .onNode(hasTestTag(movableElementChildTag) and SemanticsMatcher.inContent(SceneA)) .assertPositionInRootIsEqualTo(0.dp, 0.dp) .assertSizeIsEqualTo(100.dp) .assertIsDisplayed() diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/SharedElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/SharedElementTest.kt index 4877cd610875..2e3a934c2701 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/SharedElementTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/SharedElementTest.kt @@ -31,7 +31,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.compose.animation.scene.Edge import com.android.compose.animation.scene.TestElements import com.android.compose.animation.scene.TestScenes -import com.android.compose.animation.scene.inContent +import com.android.compose.animation.scene.inScene import com.android.compose.animation.scene.testTransition import com.android.compose.test.assertSizeIsEqualTo import org.junit.Rule @@ -125,10 +125,10 @@ class SharedElementTest { sharedElement(TestElements.Foo, enabled = false) // In SceneA, Foo leaves to the left edge. - translate(TestElements.Foo.inContent(TestScenes.SceneA), Edge.Left) + translate(TestElements.Foo.inScene(TestScenes.SceneA), Edge.Left) // In SceneB, Foo comes from the bottom edge. - translate(TestElements.Foo.inContent(TestScenes.SceneB), Edge.Bottom) + translate(TestElements.Foo.inScene(TestScenes.SceneB), Edge.Bottom) }, ) { before { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(10.dp, 50.dp) } diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestMatchers.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestMatchers.kt index 22450d32ea62..b3201d0daffe 100644 --- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestMatchers.kt +++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestMatchers.kt @@ -25,11 +25,11 @@ fun isElement(element: ElementKey, content: ContentKey? = null): SemanticsMatche return if (content == null) { hasTestTag(element.testTag) } else { - hasTestTag(element.testTag) and inContent(content) + hasTestTag(element.testTag) and SemanticsMatcher.inContent(content) } } /** A [SemanticsMatcher] that matches anything inside [content]. */ -fun inContent(content: ContentKey): SemanticsMatcher { +fun SemanticsMatcher.Companion.inContent(content: ContentKey): SemanticsMatcher { return hasAnyAncestor(hasTestTag(content.testTag)) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java index 662815ee7cbe..fcb433b5db4e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegateTest.java @@ -46,7 +46,9 @@ import android.platform.test.annotations.EnableFlags; import android.provider.Settings; import android.testing.TestableLooper; import android.view.View; +import android.view.ViewGroup; import android.widget.LinearLayout; +import android.widget.Space; import android.widget.Spinner; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -226,7 +228,7 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase { mDialog.show(); LinearLayout relatedToolsView = (LinearLayout) getRelatedToolsView(mDialog); - assertThat(relatedToolsView.getChildCount()).isEqualTo(1); + assertThat(countChildWithoutSpace(relatedToolsView)).isEqualTo(1); } @Test @@ -244,12 +246,13 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase { when(mActivityInfo.loadLabel(mPackageManager)).thenReturn(TEST_LABEL); when(mActivityInfo.loadIcon(mPackageManager)).thenReturn(mDrawable); when(mActivityInfo.getComponentName()).thenReturn(TEST_COMPONENT); + when(mDrawable.mutate()).thenReturn(mDrawable); setUpPairNewDeviceDialog(); mDialog.show(); LinearLayout relatedToolsView = (LinearLayout) getRelatedToolsView(mDialog); - assertThat(relatedToolsView.getChildCount()).isEqualTo(2); + assertThat(countChildWithoutSpace(relatedToolsView)).isEqualTo(2); } @Test @@ -364,6 +367,16 @@ public class HearingDevicesDialogDelegateTest extends SysuiTestCase { return dialog.requireViewById(R.id.preset_spinner); } + private int countChildWithoutSpace(ViewGroup viewGroup) { + int spaceCount = 0; + for (int i = 0; i < viewGroup.getChildCount(); i++) { + if (viewGroup.getChildAt(i) instanceof Space) { + spaceCount++; + } + } + return viewGroup.getChildCount() - spaceCount; + } + @After public void reset() { if (mDialogDelegate != null) { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesToolItemParserTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesToolItemParserTest.java index 17ce1ddee87a..77369f7ec881 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesToolItemParserTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesToolItemParserTest.java @@ -85,7 +85,7 @@ public class HearingDevicesToolItemParserTest extends SysuiTestCase { } @Test - public void parseStringArray_noString_emptyResult() { + public void parseStringArray_noToolName_emptyResult() { assertThat(HearingDevicesToolItemParser.parseStringArray(mContext, new String[]{}, new String[]{})).isEqualTo(emptyList()); } @@ -103,14 +103,14 @@ public class HearingDevicesToolItemParserTest extends SysuiTestCase { } @Test - public void parseStringArray_fourToolName_maxThreeToolItem() { + public void parseStringArray_threeToolNames_maxTwoToolItems() { String componentNameString = TEST_PKG + "/" + TEST_CLS; - String[] fourToolName = - new String[]{componentNameString, componentNameString, componentNameString, - componentNameString}; + String[] threeToolNames = + new String[]{componentNameString, componentNameString, componentNameString}; List<ToolItem> toolItemList = HearingDevicesToolItemParser.parseStringArray(mContext, - fourToolName, new String[]{}); + threeToolNames, new String[]{}); + assertThat(toolItemList.size()).isEqualTo(HearingDevicesToolItemParser.MAX_NUM); } @@ -120,6 +120,7 @@ public class HearingDevicesToolItemParserTest extends SysuiTestCase { List<ToolItem> toolItemList = HearingDevicesToolItemParser.parseStringArray(mContext, wrongFormatToolName, new String[]{}); + assertThat(toolItemList.size()).isEqualTo(0); } @@ -129,6 +130,7 @@ public class HearingDevicesToolItemParserTest extends SysuiTestCase { List<ToolItem> toolItemList = HearingDevicesToolItemParser.parseStringArray(mContext, notExistToolName, new String[]{}); + assertThat(toolItemList.size()).isEqualTo(0); } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/window/MultiDisplayStatusBarWindowControllerStoreTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/display/data/repository/PerDisplayStoreImplTest.kt index faaa4c415d28..1dd8ca9221a4 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/window/MultiDisplayStatusBarWindowControllerStoreTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/display/data/repository/PerDisplayStoreImplTest.kt @@ -14,23 +14,15 @@ * limitations under the License. */ -package com.android.systemui.statusbar.window +package com.android.systemui.display.data.repository -import android.platform.test.annotations.EnableFlags import android.view.Display -import android.view.WindowManager import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest -import com.android.app.viewcapture.ViewCaptureAwareWindowManager -import com.android.app.viewcapture.mockViewCaptureAwareWindowManager import com.android.systemui.SysuiTestCase -import com.android.systemui.display.data.repository.displayRepository -import com.android.systemui.display.data.repository.fakeDisplayWindowPropertiesRepository -import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.kosmos.unconfinedTestDispatcher -import com.android.systemui.statusbar.core.StatusBarConnectedDisplays import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.runBlocking @@ -43,28 +35,13 @@ import org.mockito.kotlin.mock @RunWith(AndroidJUnit4::class) @SmallTest -@EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) -class MultiDisplayStatusBarWindowControllerStoreTest : SysuiTestCase() { +class PerDisplayStoreImplTest : SysuiTestCase() { private val kosmos = testKosmos().also { it.testDispatcher = it.unconfinedTestDispatcher } private val testScope = kosmos.testScope private val fakeDisplayRepository = kosmos.displayRepository - private val store = - MultiDisplayStatusBarWindowControllerStore( - backgroundApplicationScope = kosmos.applicationCoroutineScope, - controllerFactory = kosmos.fakeStatusBarWindowControllerFactory, - displayWindowPropertiesRepository = kosmos.fakeDisplayWindowPropertiesRepository, - viewCaptureAwareWindowManagerFactory = - object : ViewCaptureAwareWindowManager.Factory { - override fun create( - windowManager: WindowManager - ): ViewCaptureAwareWindowManager { - return kosmos.mockViewCaptureAwareWindowManager - } - }, - displayRepository = fakeDisplayRepository, - ) + private val store = kosmos.fakePerDisplayStore @Before fun start() { @@ -80,34 +57,52 @@ class MultiDisplayStatusBarWindowControllerStoreTest : SysuiTestCase() { @Test fun forDisplay_defaultDisplay_multipleCalls_returnsSameInstance() = testScope.runTest { - val controller = store.defaultDisplay + val instance = store.defaultDisplay - assertThat(store.defaultDisplay).isSameInstanceAs(controller) + assertThat(store.defaultDisplay).isSameInstanceAs(instance) } @Test fun forDisplay_nonDefaultDisplay_multipleCalls_returnsSameInstance() = testScope.runTest { - val controller = store.forDisplay(NON_DEFAULT_DISPLAY_ID) + val instance = store.forDisplay(NON_DEFAULT_DISPLAY_ID) - assertThat(store.forDisplay(NON_DEFAULT_DISPLAY_ID)).isSameInstanceAs(controller) + assertThat(store.forDisplay(NON_DEFAULT_DISPLAY_ID)).isSameInstanceAs(instance) } @Test fun forDisplay_nonDefaultDisplay_afterDisplayRemoved_returnsNewInstance() = testScope.runTest { - val controller = store.forDisplay(NON_DEFAULT_DISPLAY_ID) + val instance = store.forDisplay(NON_DEFAULT_DISPLAY_ID) fakeDisplayRepository.removeDisplay(NON_DEFAULT_DISPLAY_ID) fakeDisplayRepository.addDisplay(createDisplay(NON_DEFAULT_DISPLAY_ID)) - assertThat(store.forDisplay(NON_DEFAULT_DISPLAY_ID)).isNotSameInstanceAs(controller) + assertThat(store.forDisplay(NON_DEFAULT_DISPLAY_ID)).isNotSameInstanceAs(instance) } @Test(expected = IllegalArgumentException::class) fun forDisplay_nonExistingDisplayId_throws() = testScope.runTest { store.forDisplay(NON_EXISTING_DISPLAY_ID) } + @Test + fun forDisplay_afterDisplayRemoved_onDisplayRemovalActionInvoked() = + testScope.runTest { + val instance = store.forDisplay(NON_DEFAULT_DISPLAY_ID) + + fakeDisplayRepository.removeDisplay(NON_DEFAULT_DISPLAY_ID) + + assertThat(store.removalActions).containsExactly(instance) + } + + @Test + fun forDisplay_withoutDisplayRemoval_onDisplayRemovalActionIsNotInvoked() = + testScope.runTest { + store.forDisplay(NON_DEFAULT_DISPLAY_ID) + + assertThat(store.removalActions).isEmpty() + } + private fun createDisplay(displayId: Int): Display = mock { on { getDisplayId() } doReturn displayId } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt index c804fc6990ae..ba5fb7f8df00 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProviderTest.kt @@ -98,7 +98,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { isRtl, dotWidth, BOTTOM_ALIGNED_MARGIN_NONE, - statusBarContentHeight + statusBarContentHeight, ) var chipBounds = getPrivacyChipBoundingRectForInsets(bounds, dotWidth, chipWidth, isRtl) @@ -135,7 +135,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { isRtl, dotWidth, BOTTOM_ALIGNED_MARGIN_NONE, - statusBarContentHeight + statusBarContentHeight, ) chipBounds = getPrivacyChipBoundingRectForInsets(bounds, dotWidth, chipWidth, isRtl) @@ -164,8 +164,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { val chipWidth = 30 val dotWidth = 10 val isRtl = false - val contentRect = - Rect(/* left = */ 0, /* top = */ 10, /* right = */ 1000, /* bottom = */ 100) + val contentRect = Rect(/* left= */ 0, /* top= */ 10, /* right= */ 1000, /* bottom= */ 100) val chipBounds = getPrivacyChipBoundingRectForInsets(contentRect, dotWidth, chipWidth, isRtl) @@ -207,7 +206,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { isRtl, dotWidth, BOTTOM_ALIGNED_MARGIN_NONE, - statusBarContentHeight + statusBarContentHeight, ) assertRects(expectedBounds, bounds, currentRotation, targetRotation) @@ -228,7 +227,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { isRtl, dotWidth, BOTTOM_ALIGNED_MARGIN_NONE, - statusBarContentHeight + statusBarContentHeight, ) assertRects(expectedBounds, bounds, currentRotation, targetRotation) @@ -251,7 +250,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { isRtl, dotWidth, BOTTOM_ALIGNED_MARGIN_NONE, - statusBarContentHeight + statusBarContentHeight, ) assertRects(expectedBounds, bounds, currentRotation, targetRotation) @@ -263,7 +262,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { minLeftPadding, 0, screenBounds.height() - dcBounds.height() - dotWidth, - sbHeightLandscape + sbHeightLandscape, ) bounds = @@ -278,7 +277,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { isRtl, dotWidth, BOTTOM_ALIGNED_MARGIN_NONE, - statusBarContentHeight + statusBarContentHeight, ) assertRects(expectedBounds, bounds, currentRotation, targetRotation) @@ -320,7 +319,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { isRtl, dotWidth, BOTTOM_ALIGNED_MARGIN_NONE, - statusBarContentHeight + statusBarContentHeight, ) assertRects(expectedBounds, bounds, currentRotation, targetRotation) @@ -331,7 +330,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { protectionBounds.bottom, 0, screenBounds.height() - minRightPadding, - sbHeightLandscape + sbHeightLandscape, ) bounds = @@ -346,7 +345,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { isRtl, dotWidth, BOTTOM_ALIGNED_MARGIN_NONE, - statusBarContentHeight + statusBarContentHeight, ) assertRects(expectedBounds, bounds, currentRotation, targetRotation) @@ -369,7 +368,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { isRtl, dotWidth, BOTTOM_ALIGNED_MARGIN_NONE, - statusBarContentHeight + statusBarContentHeight, ) assertRects(expectedBounds, bounds, currentRotation, targetRotation) @@ -381,7 +380,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { minLeftPadding, 0, screenBounds.height() - protectionBounds.bottom - dotWidth, - sbHeightLandscape + sbHeightLandscape, ) bounds = @@ -396,7 +395,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { isRtl, dotWidth, BOTTOM_ALIGNED_MARGIN_NONE, - statusBarContentHeight + statusBarContentHeight, ) assertRects(expectedBounds, bounds, currentRotation, targetRotation) @@ -415,7 +414,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { left = screenBounds.right - dcWidth, top = 0, right = screenBounds.right, - bottom = dcHeight + bottom = dcHeight, ) val dcBoundsLandscape = Rect(left = 0, top = 0, right = dcHeight, bottom = dcWidth) val dcBoundsSeascape = @@ -423,14 +422,14 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { left = screenBounds.right - dcHeight, top = screenBounds.bottom - dcWidth, right = screenBounds.right - dcHeight, - bottom = screenBounds.bottom - dcWidth + bottom = screenBounds.bottom - dcWidth, ) val dcBoundsUpsideDown = Rect( left = 0, top = screenBounds.bottom - dcHeight, right = dcWidth, - bottom = screenBounds.bottom - dcHeight + bottom = screenBounds.bottom - dcHeight, ) val minLeftPadding = 20 val minRightPadding = 20 @@ -448,7 +447,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { left = minLeftPadding, top = 0, right = dcBoundsPortrait.left - dotWidth, - bottom = sbHeightPortrait + bottom = sbHeightPortrait, ) var bounds = @@ -463,7 +462,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { isRtl, dotWidth, BOTTOM_ALIGNED_MARGIN_NONE, - statusBarContentHeight + statusBarContentHeight, ) assertRects(expectedBounds, bounds, currentRotation, targetRotation) @@ -475,7 +474,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { left = dcBoundsLandscape.height(), top = 0, right = screenBounds.height() - minRightPadding, - bottom = sbHeightLandscape + bottom = sbHeightLandscape, ) bounds = @@ -490,7 +489,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { isRtl, dotWidth, BOTTOM_ALIGNED_MARGIN_NONE, - statusBarContentHeight + statusBarContentHeight, ) assertRects(expectedBounds, bounds, currentRotation, targetRotation) @@ -502,7 +501,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { left = minLeftPadding, top = 0, right = screenBounds.width() - minRightPadding, - bottom = sbHeightPortrait + bottom = sbHeightPortrait, ) bounds = @@ -517,7 +516,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { isRtl, dotWidth, BOTTOM_ALIGNED_MARGIN_NONE, - statusBarContentHeight + statusBarContentHeight, ) assertRects(expectedBounds, bounds, currentRotation, targetRotation) @@ -529,7 +528,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { left = minLeftPadding, top = 0, right = screenBounds.height() - minRightPadding, - bottom = sbHeightLandscape + bottom = sbHeightLandscape, ) bounds = @@ -544,7 +543,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { isRtl, dotWidth, BOTTOM_ALIGNED_MARGIN_NONE, - statusBarContentHeight + statusBarContentHeight, ) assertRects(expectedBounds, bounds, currentRotation, targetRotation) @@ -584,7 +583,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { isRtl, dotWidth, BOTTOM_ALIGNED_MARGIN_NONE, - statusBarContentHeight + statusBarContentHeight, ) assertRects(expectedBounds, bounds, currentRotation, targetRotation) @@ -595,7 +594,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { protectionBounds.bottom, 0, screenBounds.height() - minRightPadding, - sbHeightLandscape + sbHeightLandscape, ) bounds = @@ -610,7 +609,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { isRtl, dotWidth, BOTTOM_ALIGNED_MARGIN_NONE, - statusBarContentHeight + statusBarContentHeight, ) assertRects(expectedBounds, bounds, currentRotation, targetRotation) @@ -633,7 +632,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { isRtl, dotWidth, BOTTOM_ALIGNED_MARGIN_NONE, - statusBarContentHeight + statusBarContentHeight, ) assertRects(expectedBounds, bounds, currentRotation, targetRotation) @@ -645,7 +644,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { minLeftPadding, 0, screenBounds.height() - protectionBounds.bottom - dotWidth, - sbHeightLandscape + sbHeightLandscape, ) bounds = @@ -660,7 +659,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { isRtl, dotWidth, BOTTOM_ALIGNED_MARGIN_NONE, - statusBarContentHeight + statusBarContentHeight, ) assertRects(expectedBounds, bounds, currentRotation, targetRotation) @@ -682,7 +681,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { isRtl = false, dotWidth = 10, bottomAlignedMargin = BOTTOM_ALIGNED_MARGIN_NONE, - statusBarContentHeight = 15 + statusBarContentHeight = 15, ) assertThat(bounds.top).isEqualTo(0) @@ -704,7 +703,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { isRtl = false, dotWidth = 10, bottomAlignedMargin = 5, - statusBarContentHeight = 15 + statusBarContentHeight = 15, ) // Content in the status bar is centered vertically. To achieve the bottom margin we want, @@ -756,7 +755,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { isRtl, dotWidth, BOTTOM_ALIGNED_MARGIN_NONE, - statusBarContentHeight + statusBarContentHeight, ) assertRects(expectedBounds, bounds, currentRotation, targetRotation) @@ -777,7 +776,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { isRtl, dotWidth, BOTTOM_ALIGNED_MARGIN_NONE, - statusBarContentHeight + statusBarContentHeight, ) assertRects(expectedBounds, bounds, currentRotation, targetRotation) @@ -798,7 +797,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { isRtl, dotWidth, BOTTOM_ALIGNED_MARGIN_NONE, - statusBarContentHeight + statusBarContentHeight, ) assertRects(expectedBounds, bounds, currentRotation, targetRotation) @@ -809,7 +808,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { minLeftPadding, 0, screenBounds.height() - dcBounds.height() - dotWidth, - sbHeightLandscape + sbHeightLandscape, ) bounds = @@ -824,7 +823,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { isRtl, dotWidth, BOTTOM_ALIGNED_MARGIN_NONE, - statusBarContentHeight + statusBarContentHeight, ) assertRects(expectedBounds, bounds, currentRotation, targetRotation) @@ -860,7 +859,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { isRtl, dotWidth, BOTTOM_ALIGNED_MARGIN_NONE, - statusBarContentHeight + statusBarContentHeight, ) assertRects(expectedBounds, bounds, currentRotation, targetRotation) @@ -880,7 +879,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { isRtl, dotWidth, BOTTOM_ALIGNED_MARGIN_NONE, - statusBarContentHeight + statusBarContentHeight, ) assertRects(expectedBounds, bounds, currentRotation, targetRotation) @@ -900,7 +899,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { isRtl, dotWidth, BOTTOM_ALIGNED_MARGIN_NONE, - statusBarContentHeight + statusBarContentHeight, ) assertRects(expectedBounds, bounds, currentRotation, targetRotation) @@ -920,7 +919,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { isRtl, dotWidth, BOTTOM_ALIGNED_MARGIN_NONE, - statusBarContentHeight + statusBarContentHeight, ) assertRects(expectedBounds, bounds, currentRotation, targetRotation) } @@ -958,7 +957,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { isRtl, dotWidth, BOTTOM_ALIGNED_MARGIN_NONE, - statusBarContentHeight + statusBarContentHeight, ) assertRects(expectedBounds, bounds, currentRotation, targetRotation) @@ -968,12 +967,12 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { fun testDisplayChanged_returnsUpdatedInsets() { // GIVEN: get insets on the first display and switch to the second display val provider = - StatusBarContentInsetsProvider( + StatusBarContentInsetsProviderImpl( contextMock, configurationController, mock<DumpManager>(), mock<CommandRegistry>(), - mock<SysUICutoutProvider>() + mock<SysUICutoutProvider>(), ) configuration.windowConfiguration.setMaxBounds(Rect(0, 0, 1080, 2160)) @@ -993,12 +992,12 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { // GIVEN: get insets on the first display, switch to the second display, // get insets and switch back val provider = - StatusBarContentInsetsProvider( + StatusBarContentInsetsProviderImpl( contextMock, configurationController, mock<DumpManager>(), mock<CommandRegistry>(), - mock<SysUICutoutProvider>() + mock<SysUICutoutProvider>(), ) configuration.windowConfiguration.setMaxBounds(Rect(0, 0, 1080, 2160)) @@ -1024,12 +1023,12 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { configuration.windowConfiguration.setMaxBounds(0, 0, 100, 100) configurationController.onConfigurationChanged(configuration) val provider = - StatusBarContentInsetsProvider( + StatusBarContentInsetsProviderImpl( contextMock, configurationController, mock<DumpManager>(), mock<CommandRegistry>(), - mock<SysUICutoutProvider>() + mock<SysUICutoutProvider>(), ) val listener = object : StatusBarContentInsetsChangedListener { @@ -1053,12 +1052,12 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { fun onDensityOrFontScaleChanged_listenerNotified() { configuration.densityDpi = 12 val provider = - StatusBarContentInsetsProvider( + StatusBarContentInsetsProviderImpl( contextMock, configurationController, mock<DumpManager>(), mock<CommandRegistry>(), - mock<SysUICutoutProvider>() + mock<SysUICutoutProvider>(), ) val listener = object : StatusBarContentInsetsChangedListener { @@ -1081,12 +1080,12 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { @Test fun onThemeChanged_listenerNotified() { val provider = - StatusBarContentInsetsProvider( + StatusBarContentInsetsProviderImpl( contextMock, configurationController, mock<DumpManager>(), mock<CommandRegistry>(), - mock<SysUICutoutProvider>() + mock<SysUICutoutProvider>(), ) val listener = object : StatusBarContentInsetsChangedListener { @@ -1108,13 +1107,13 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { expected: Rect, actual: Rect, @Rotation currentRotation: Int, - @Rotation targetRotation: Int + @Rotation targetRotation: Int, ) { assertTrue( "Rects must match. currentRotation=${RotationUtils.toString(currentRotation)}" + " targetRotation=${RotationUtils.toString(targetRotation)}" + " expected=$expected actual=$actual", - expected.equals(actual) + expected.equals(actual), ) } @@ -1126,7 +1125,7 @@ class StatusBarContentInsetsProviderTest : SysuiTestCase() { left: Rect = Rect(), top: Rect = Rect(), right: Rect = Rect(), - bottom: Rect = Rect() + bottom: Rect = Rect(), ) { whenever(dc.boundingRects) .thenReturn(listOf(left, top, right, bottom).filter { !it.isEmpty }) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizerTest.kt index 40c3f221e2df..29e9ba752b36 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizerTest.kt @@ -61,6 +61,26 @@ class BackGestureRecognizerTest : SysuiTestCase() { } @Test + fun triggersProgressRelativeToDistance() { + assertProgressWhileMovingFingers(deltaX = -SWIPE_DISTANCE / 2, expectedProgress = 0.5f) + assertProgressWhileMovingFingers(deltaX = SWIPE_DISTANCE / 2, expectedProgress = 0.5f) + assertProgressWhileMovingFingers(deltaX = -SWIPE_DISTANCE, expectedProgress = 1f) + assertProgressWhileMovingFingers(deltaX = SWIPE_DISTANCE, expectedProgress = 1f) + } + + private fun assertProgressWhileMovingFingers(deltaX: Float, expectedProgress: Float) { + assertStateAfterEvents( + events = ThreeFingerGesture.eventsForGestureInProgress { move(deltaX = deltaX) }, + expectedState = InProgress(progress = expectedProgress), + ) + } + + @Test + fun triggeredProgressIsNoBiggerThanOne() { + assertProgressWhileMovingFingers(deltaX = SWIPE_DISTANCE * 2, expectedProgress = 1f) + } + + @Test fun doesntTriggerGestureFinished_onGestureDistanceTooShort() { assertStateAfterEvents( events = ThreeFingerGesture.swipeLeft(distancePx = SWIPE_DISTANCE / 2), diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureTest.kt index 8406d3b99bac..ff0cec5e06e9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureTest.kt @@ -104,7 +104,8 @@ class EasterEggGestureTest : SysuiTestCase() { } private fun assertStateAfterTwoFingerGesture(gesturePath: List<Point>, wasTriggered: Boolean) { - val events = TwoFingerGesture.createEvents { gesturePath.forEach { (x, y) -> move(x, y) } } + val events = + TwoFingerGesture.eventsForFullGesture { gesturePath.forEach { (x, y) -> move(x, y) } } assertStateAfterEvents(events = events, wasTriggered = wasTriggered) } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizerTest.kt index 043b77577978..7d3ed92cecc6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizerTest.kt @@ -56,6 +56,27 @@ class HomeGestureRecognizerTest : SysuiTestCase() { } @Test + fun triggersProgressRelativeToDistance() { + assertProgressWhileMovingFingers(deltaY = -SWIPE_DISTANCE / 2, expectedProgress = 0.5f) + assertProgressWhileMovingFingers(deltaY = -SWIPE_DISTANCE, expectedProgress = 1f) + } + + private fun assertProgressWhileMovingFingers(deltaY: Float, expectedProgress: Float) { + assertStateAfterEvents( + events = ThreeFingerGesture.eventsForGestureInProgress { move(deltaY = deltaY) }, + expectedState = InProgress(progress = expectedProgress), + ) + } + + @Test + fun triggeredProgressIsBetweenZeroAndOne() { + // going in the wrong direction + assertProgressWhileMovingFingers(deltaY = SWIPE_DISTANCE / 2, expectedProgress = 0f) + // going further than required distance + assertProgressWhileMovingFingers(deltaY = -SWIPE_DISTANCE * 2, expectedProgress = 1f) + } + + @Test fun doesntTriggerGestureFinished_onGestureDistanceTooShort() { assertStateAfterEvents( events = ThreeFingerGesture.swipeUp(distancePx = SWIPE_DISTANCE / 2), diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizerTest.kt index 7095a91a4e5d..c5c0d59ea48b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizerTest.kt @@ -77,11 +77,32 @@ class RecentAppsGestureRecognizerTest : SysuiTestCase() { fun triggersGestureProgressForThreeFingerGestureStarted() { assertStateAfterEvents( events = ThreeFingerGesture.startEvents(x = 0f, y = 0f), - expectedState = InProgress(), + expectedState = InProgress(progress = 0f), ) } @Test + fun triggersProgressRelativeToDistance() { + assertProgressWhileMovingFingers(deltaY = -SWIPE_DISTANCE / 2, expectedProgress = 0.5f) + assertProgressWhileMovingFingers(deltaY = -SWIPE_DISTANCE, expectedProgress = 1f) + } + + private fun assertProgressWhileMovingFingers(deltaY: Float, expectedProgress: Float) { + assertStateAfterEvents( + events = ThreeFingerGesture.eventsForGestureInProgress { move(deltaY = deltaY) }, + expectedState = InProgress(progress = expectedProgress), + ) + } + + @Test + fun triggeredProgressIsBetweenZeroAndOne() { + // going in the wrong direction + assertProgressWhileMovingFingers(deltaY = SWIPE_DISTANCE / 2, expectedProgress = 0f) + // going further than required distance + assertProgressWhileMovingFingers(deltaY = -SWIPE_DISTANCE * 2, expectedProgress = 1f) + } + + @Test fun doesntTriggerGestureFinished_onGestureDistanceTooShort() { assertStateAfterEvents( events = ThreeFingerGesture.swipeUp(distancePx = SWIPE_DISTANCE / 2), diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureBuilder.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureBuilder.kt index 296d4dce8ce4..42fe1e5d6bec 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureBuilder.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureBuilder.kt @@ -25,11 +25,23 @@ import android.view.MotionEvent.ACTION_UP import com.android.systemui.touchpad.tutorial.ui.gesture.MultiFingerGesture.Companion.DEFAULT_X import com.android.systemui.touchpad.tutorial.ui.gesture.MultiFingerGesture.Companion.DEFAULT_Y -/** - * Interface for gesture builders which support creating list of [MotionEvent] for common swipe - * gestures. For simple usage see swipe* methods or use [createEvents] for more specific scenarios. - */ -interface MultiFingerGesture { +/** Given gesture move events can build list of [MotionEvent]s included in that gesture */ +interface GestureEventsBuilder { + /** + * Creates full gesture including provided move events. This means returned events include DOWN, + * MOVE and UP. Note that move event's x and y is always relative to the starting one. + */ + fun eventsForFullGesture(moveEvents: MoveEventsBuilder.() -> Unit): List<MotionEvent> + + /** + * Creates partial gesture including provided move events. This means returned events include + * DOWN and MOVE. Note that move event's x and y is always relative to the starting one. + */ + fun eventsForGestureInProgress(moveEvents: MoveEventsBuilder.() -> Unit): List<MotionEvent> +} + +/** Support creating list of [MotionEvent] for common swipe gestures. */ +interface MultiFingerGesture : GestureEventsBuilder { companion object { const val SWIPE_DISTANCE = 100f @@ -37,27 +49,41 @@ interface MultiFingerGesture { const val DEFAULT_Y = 500f } - fun swipeUp(distancePx: Float = SWIPE_DISTANCE) = createEvents { move(deltaY = -distancePx) } - - fun swipeDown(distancePx: Float = SWIPE_DISTANCE) = createEvents { move(deltaY = distancePx) } + fun swipeUp(distancePx: Float = SWIPE_DISTANCE) = eventsForFullGesture { + move(deltaY = -distancePx) + } - fun swipeRight(distancePx: Float = SWIPE_DISTANCE) = createEvents { move(deltaX = distancePx) } + fun swipeDown(distancePx: Float = SWIPE_DISTANCE) = eventsForFullGesture { + move(deltaY = distancePx) + } - fun swipeLeft(distancePx: Float = SWIPE_DISTANCE) = createEvents { move(deltaX = -distancePx) } + fun swipeRight(distancePx: Float = SWIPE_DISTANCE) = eventsForFullGesture { + move(deltaX = distancePx) + } - /** - * Creates gesture with provided move events. Note that move event's x and y is always relative - * to the starting one - */ - fun createEvents(moveEvents: GestureBuilder.() -> Unit): List<MotionEvent> + fun swipeLeft(distancePx: Float = SWIPE_DISTANCE) = eventsForFullGesture { + move(deltaX = -distancePx) + } } object ThreeFingerGesture : MultiFingerGesture { - override fun createEvents(moveEvents: GestureBuilder.() -> Unit): List<MotionEvent> { - return touchpadGesture( + + private val moveEventsBuilder = MoveEventsBuilder(::threeFingerEvent) + + override fun eventsForFullGesture(moveEvents: MoveEventsBuilder.() -> Unit): List<MotionEvent> { + return buildGesture( + startEvents = { x, y -> startEvents(x, y) }, + moveEvents = moveEventsBuilder.getEvents(moveEvents), + endEvents = { x, y -> endEvents(x, y) }, + ) + } + + override fun eventsForGestureInProgress( + moveEvents: MoveEventsBuilder.() -> Unit + ): List<MotionEvent> { + return buildGesture( startEvents = { x, y -> startEvents(x, y) }, - moveEvents = GestureBuilder(::threeFingerEvent).apply { moveEvents() }.events, - endEvents = { x, y -> endEvents(x, y) } + moveEvents = moveEventsBuilder.getEvents(moveEvents), ) } @@ -65,7 +91,7 @@ object ThreeFingerGesture : MultiFingerGesture { return listOf( threeFingerEvent(ACTION_DOWN, x, y), threeFingerEvent(ACTION_POINTER_DOWN, x, y), - threeFingerEvent(ACTION_POINTER_DOWN, x, y) + threeFingerEvent(ACTION_POINTER_DOWN, x, y), ) } @@ -73,32 +99,43 @@ object ThreeFingerGesture : MultiFingerGesture { return listOf( threeFingerEvent(ACTION_POINTER_UP, x, y), threeFingerEvent(ACTION_POINTER_UP, x, y), - threeFingerEvent(ACTION_UP, x, y) + threeFingerEvent(ACTION_UP, x, y), ) } private fun threeFingerEvent( action: Int, x: Float = DEFAULT_X, - y: Float = DEFAULT_Y + y: Float = DEFAULT_Y, ): MotionEvent { return touchpadEvent( action = action, x = x, y = y, classification = MotionEvent.CLASSIFICATION_MULTI_FINGER_SWIPE, - axisValues = mapOf(MotionEvent.AXIS_GESTURE_SWIPE_FINGER_COUNT to 3f) + axisValues = mapOf(MotionEvent.AXIS_GESTURE_SWIPE_FINGER_COUNT to 3f), ) } } object FourFingerGesture : MultiFingerGesture { - override fun createEvents(moveEvents: GestureBuilder.() -> Unit): List<MotionEvent> { - return touchpadGesture( + private val moveEventsBuilder = MoveEventsBuilder(::fourFingerEvent) + + override fun eventsForFullGesture(moveEvents: MoveEventsBuilder.() -> Unit): List<MotionEvent> { + return buildGesture( + startEvents = { x, y -> startEvents(x, y) }, + moveEvents = moveEventsBuilder.getEvents(moveEvents), + endEvents = { x, y -> endEvents(x, y) }, + ) + } + + override fun eventsForGestureInProgress( + moveEvents: MoveEventsBuilder.() -> Unit + ): List<MotionEvent> { + return buildGesture( startEvents = { x, y -> startEvents(x, y) }, - moveEvents = GestureBuilder(::fourFingerEvent).apply { moveEvents() }.events, - endEvents = { x, y -> endEvents(x, y) } + moveEvents = moveEventsBuilder.getEvents(moveEvents), ) } @@ -107,7 +144,7 @@ object FourFingerGesture : MultiFingerGesture { fourFingerEvent(ACTION_DOWN, x, y), fourFingerEvent(ACTION_POINTER_DOWN, x, y), fourFingerEvent(ACTION_POINTER_DOWN, x, y), - fourFingerEvent(ACTION_POINTER_DOWN, x, y) + fourFingerEvent(ACTION_POINTER_DOWN, x, y), ) } @@ -116,61 +153,74 @@ object FourFingerGesture : MultiFingerGesture { fourFingerEvent(ACTION_POINTER_UP, x, y), fourFingerEvent(ACTION_POINTER_UP, x, y), fourFingerEvent(ACTION_POINTER_UP, x, y), - fourFingerEvent(ACTION_UP, x, y) + fourFingerEvent(ACTION_UP, x, y), ) } private fun fourFingerEvent( action: Int, x: Float = DEFAULT_X, - y: Float = DEFAULT_Y + y: Float = DEFAULT_Y, ): MotionEvent { return touchpadEvent( action = action, x = x, y = y, classification = MotionEvent.CLASSIFICATION_MULTI_FINGER_SWIPE, - axisValues = mapOf(MotionEvent.AXIS_GESTURE_SWIPE_FINGER_COUNT to 4f) + axisValues = mapOf(MotionEvent.AXIS_GESTURE_SWIPE_FINGER_COUNT to 4f), ) } } object TwoFingerGesture : MultiFingerGesture { - override fun createEvents(moveEvents: GestureBuilder.() -> Unit): List<MotionEvent> { - return touchpadGesture( - startEvents = { x, y -> listOf(twoFingerEvent(ACTION_DOWN, x, y)) }, - moveEvents = GestureBuilder(::twoFingerEvent).apply { moveEvents() }.events, - endEvents = { x, y -> listOf(twoFingerEvent(ACTION_UP, x, y)) } + private val moveEventsBuilder = MoveEventsBuilder(::twoFingerEvent) + + override fun eventsForFullGesture(moveEvents: MoveEventsBuilder.() -> Unit): List<MotionEvent> { + return buildGesture( + startEvents = { x, y -> startEvents(x, y) }, + moveEvents = moveEventsBuilder.getEvents(moveEvents), + endEvents = { x, y -> listOf(twoFingerEvent(ACTION_UP, x, y)) }, + ) + } + + override fun eventsForGestureInProgress( + moveEvents: MoveEventsBuilder.() -> Unit + ): List<MotionEvent> { + return buildGesture( + startEvents = { x, y -> startEvents(x, y) }, + moveEvents = moveEventsBuilder.getEvents(moveEvents), ) } + private fun startEvents(x: Float, y: Float) = listOf(twoFingerEvent(ACTION_DOWN, x, y)) + private fun twoFingerEvent( action: Int, x: Float = DEFAULT_X, - y: Float = DEFAULT_Y + y: Float = DEFAULT_Y, ): MotionEvent { return touchpadEvent( action = action, x = x, y = y, classification = MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE, - axisValues = mapOf(MotionEvent.AXIS_GESTURE_SWIPE_FINGER_COUNT to 2f) + axisValues = mapOf(MotionEvent.AXIS_GESTURE_SWIPE_FINGER_COUNT to 2f), ) } } -private fun touchpadGesture( +private fun buildGesture( startEvents: (Float, Float) -> List<MotionEvent>, moveEvents: List<MotionEvent>, - endEvents: (Float, Float) -> List<MotionEvent> + endEvents: (Float, Float) -> List<MotionEvent> = { _, _ -> emptyList() }, ): List<MotionEvent> { val lastX = moveEvents.last().x val lastY = moveEvents.last().y return startEvents(DEFAULT_X, DEFAULT_Y) + moveEvents + endEvents(lastX, lastY) } -class GestureBuilder internal constructor(val eventBuilder: (Int, Float, Float) -> MotionEvent) { +class MoveEventsBuilder internal constructor(val eventBuilder: (Int, Float, Float) -> MotionEvent) { val events = mutableListOf<MotionEvent>() @@ -178,3 +228,11 @@ class GestureBuilder internal constructor(val eventBuilder: (Int, Float, Float) events.add(eventBuilder(ACTION_MOVE, DEFAULT_X + deltaX, DEFAULT_Y + deltaY)) } } + +private fun MoveEventsBuilder.getEvents( + moveEvents: MoveEventsBuilder.() -> Unit +): List<MotionEvent> { + events.clear() + this.moveEvents() + return events +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureBuilderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureBuilderTest.kt index 13ebb42531b8..64136775b4eb 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureBuilderTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureBuilderTest.kt @@ -54,7 +54,28 @@ class TouchpadGestureBuilderTest : SysuiTestCase() { ACTION_MOVE, ACTION_POINTER_UP, ACTION_POINTER_UP, - ACTION_UP + ACTION_UP, + ) + .inOrder() + } + + @Test + fun threeFingerGestureInProgressProducesCorrectEvents() { + val events = + ThreeFingerGesture.eventsForGestureInProgress { + move(deltaX = 10f) + move(deltaX = 20f) + } + + val actions = events.map { it.actionMasked } + assertWithMessage("Events have expected action type") + .that(actions) + .containsExactly( + ACTION_DOWN, + ACTION_POINTER_DOWN, + ACTION_POINTER_DOWN, + ACTION_MOVE, + ACTION_MOVE, ) .inOrder() } @@ -80,7 +101,7 @@ class TouchpadGestureBuilderTest : SysuiTestCase() { ACTION_POINTER_UP, ACTION_POINTER_UP, ACTION_POINTER_UP, - ACTION_UP + ACTION_UP, ) .inOrder() } @@ -109,7 +130,7 @@ class TouchpadGestureBuilderTest : SysuiTestCase() { @Test fun gestureBuilderProducesCorrectEventCoordinates() { val events = - ThreeFingerGesture.createEvents { + ThreeFingerGesture.eventsForFullGesture { move(deltaX = 50f) move(deltaX = 100f) } @@ -127,7 +148,7 @@ class TouchpadGestureBuilderTest : SysuiTestCase() { // up events DEFAULT_X + 100f to DEFAULT_Y, DEFAULT_X + 100f to DEFAULT_Y, - DEFAULT_X + 100f to DEFAULT_Y + DEFAULT_X + 100f to DEFAULT_Y, ) .inOrder() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt index a867eb38b44c..c302b40fc4d7 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt @@ -85,7 +85,7 @@ class TouchpadGestureHandlerTest : SysuiTestCase() { } private fun backGestureEvents(): List<MotionEvent> { - return ThreeFingerGesture.createEvents { + return ThreeFingerGesture.eventsForFullGesture { move(deltaX = SWIPE_DISTANCE / 4) move(deltaX = SWIPE_DISTANCE / 2) move(deltaX = SWIPE_DISTANCE) diff --git a/packages/SystemUI/res/drawable/qs_hearing_devices_related_tools_background.xml b/packages/SystemUI/res/drawable/qs_hearing_devices_related_tools_background.xml index 627b92b8a779..3c1668405909 100644 --- a/packages/SystemUI/res/drawable/qs_hearing_devices_related_tools_background.xml +++ b/packages/SystemUI/res/drawable/qs_hearing_devices_related_tools_background.xml @@ -21,11 +21,8 @@ android:color="?android:attr/colorControlHighlight"> <item> <shape android:shape="rectangle"> - <solid android:color="@android:color/transparent"/> + <solid android:color="?androidprv:attr/materialColorPrimaryContainer"/> <corners android:radius="@dimen/hearing_devices_preset_spinner_background_radius"/> - <stroke - android:width="1dp" - android:color="?androidprv:attr/textColorTertiary" /> </shape> </item> </ripple> diff --git a/packages/SystemUI/res/layout/hearing_devices_tile_dialog.xml b/packages/SystemUI/res/layout/hearing_devices_tile_dialog.xml index 4a7bef9f48b9..80692f9481b7 100644 --- a/packages/SystemUI/res/layout/hearing_devices_tile_dialog.xml +++ b/packages/SystemUI/res/layout/hearing_devices_tile_dialog.xml @@ -72,7 +72,6 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toBottomOf="@id/device_function_barrier" - app:layout_constraintBottom_toTopOf="@id/related_tools_scroll" android:drawableStart="@drawable/ic_add" android:drawablePadding="20dp" android:drawableTint="?android:attr/textColorPrimary" @@ -92,24 +91,16 @@ app:barrierDirection="bottom" app:constraint_referenced_ids="device_function_barrier, pair_new_device_button" /> - <HorizontalScrollView - android:id="@+id/related_tools_scroll" + <LinearLayout + android:id="@+id/related_tools_container" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginStart="@dimen/bluetooth_dialog_layout_margin" android:layout_marginEnd="@dimen/bluetooth_dialog_layout_margin" android:layout_marginTop="@dimen/hearing_devices_layout_margin" - android:scrollbars="none" - android:fillViewport="true" + android:orientation="horizontal" app:layout_constraintStart_toStartOf="parent" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toBottomOf="@id/preset_spinner"> - <LinearLayout - android:id="@+id/related_tools_container" - android:layout_width="match_parent" - android:layout_height="match_parent" - android:orientation="horizontal"> - </LinearLayout> - </HorizontalScrollView> + app:layout_constraintTop_toBottomOf="@id/device_barrier" /> </androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/hearing_tool_item.xml b/packages/SystemUI/res/layout/hearing_tool_item.xml index ff2fbe070e0f..f5baf2aaf6dc 100644 --- a/packages/SystemUI/res/layout/hearing_tool_item.xml +++ b/packages/SystemUI/res/layout/hearing_tool_item.xml @@ -17,8 +17,8 @@ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/tool_view" - android:layout_width="match_parent" - android:layout_height="match_parent" + android:layout_width="0dp" + android:layout_height="wrap_content" android:orientation="vertical" android:gravity="top|center_horizontal" android:focusable="true" @@ -26,8 +26,10 @@ android:layout_weight="1"> <FrameLayout android:id="@+id/icon_frame" - android:layout_width="@dimen/hearing_devices_tool_icon_frame_width" - android:layout_height="@dimen/hearing_devices_tool_icon_frame_height" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:paddingTop="20dp" + android:paddingBottom="20dp" android:background="@drawable/qs_hearing_devices_related_tools_background" android:focusable="false" > <ImageView diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 16a8bc5b034f..7d840cfe949a 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -134,17 +134,19 @@ <!-- Use collapsed layout for media player in landscape QQS --> <bool name="config_quickSettingsMediaLandscapeCollapsed">true</bool> - <!-- For hearing devices related tool list. Need to be in ComponentName format (package/class). - Should be activity to be launched. - Already contains tool that holds intent: "com.android.settings.action.live_caption". - Maximum number is 3. --> + <!-- Hearing devices related tool list. Each entry must be in ComponentName format + (package/class), indicating the specific application component to launch. + Already contains tool that handles intent: "com.android.settings.action.live_caption" by + default. You can add up to 2 additional related tools. --> <string-array name="config_quickSettingsHearingDevicesRelatedToolName" translatable="false"> </string-array> - <!-- The drawable resource names. If provided, it will replace the corresponding icons in - config_quickSettingsHearingDevicesRelatedToolName. Can be empty to use original icons. - Already contains tool that holds intent: "com.android.settings.action.live_caption". - Maximum number is 3. --> + <!-- Hearing devices related tool icon list. Provide drawable resource names in the same order + as the component names in config_quickSettingsHearingDevicesRelatedToolName. The icons + should be monochrome and will be tinted according to the system's material color. Ensure + the number of icon resources matches the number of components specified in + config_quickSettingsHearingDevicesRelatedToolName. If this array is empty or the sizes + don't match, the original application icons will be used. --> <string-array name="config_quickSettingsHearingDevicesRelatedToolIcon" translatable="false"> </string-array> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 6c8a7403953e..209a971814f4 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1798,8 +1798,6 @@ <dimen name="hearing_devices_preset_spinner_text_padding_vertical">15dp</dimen> <dimen name="hearing_devices_preset_spinner_arrow_size">24dp</dimen> <dimen name="hearing_devices_preset_spinner_background_radius">28dp</dimen> - <dimen name="hearing_devices_tool_icon_frame_width">94dp</dimen> - <dimen name="hearing_devices_tool_icon_frame_height">64dp</dimen> <dimen name="hearing_devices_tool_icon_size">28dp</dimen> <!-- Height percentage of the parent container occupied by the communal view --> diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java index 158623fa80af..1978bb89b5b2 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java @@ -42,6 +42,7 @@ import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.ImageView; import android.widget.LinearLayout; +import android.widget.Space; import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; @@ -52,6 +53,7 @@ import androidx.annotation.VisibleForTesting; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; +import com.android.settingslib.Utils; import com.android.settingslib.bluetooth.BluetoothCallback; import com.android.settingslib.bluetooth.CachedBluetoothDevice; import com.android.settingslib.bluetooth.HapClientProfile; @@ -428,10 +430,16 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, } catch (Resources.NotFoundException e) { Log.i(TAG, "No hearing devices related tool config resource"); } - final int listSize = toolItemList.size(); - for (int i = 0; i < listSize; i++) { + for (int i = 0; i < toolItemList.size(); i++) { View view = createHearingToolView(context, toolItemList.get(i)); mRelatedToolsContainer.addView(view); + if (i != toolItemList.size() - 1) { + final int spaceSize = context.getResources().getDimensionPixelSize( + R.dimen.hearing_devices_layout_margin); + Space space = new Space(context); + space.setLayoutParams(new LinearLayout.LayoutParams(spaceSize, 0)); + mRelatedToolsContainer.addView(space); + } } } @@ -492,6 +500,10 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, TextView text = view.requireViewById(R.id.tool_name); view.setContentDescription(item.getToolName()); icon.setImageDrawable(item.getToolIcon()); + if (item.isCustomIcon()) { + icon.getDrawable().mutate().setTint(Utils.getColorAttr(context, + com.android.internal.R.attr.materialColorOnPrimaryContainer).getDefaultColor()); + } text.setText(item.getToolName()); Intent intent = item.getToolIntent(); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK); @@ -517,7 +529,8 @@ public class HearingDevicesDialogDelegate implements SystemUIDialog.Delegate, return new ToolItem( context.getString(R.string.quick_settings_hearing_devices_live_caption_title), context.getDrawable(R.drawable.ic_volume_odi_captions), - LIVE_CAPTION_INTENT); + LIVE_CAPTION_INTENT, + /* isCustomIcon= */ true); } return null; diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesToolItemParser.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesToolItemParser.java index 2006726e6847..7e4c1e5e7b6e 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesToolItemParser.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesToolItemParser.java @@ -41,7 +41,7 @@ public class HearingDevicesToolItemParser { private static final String SPLIT_DELIMITER = "/"; private static final String RES_TYPE = "drawable"; @VisibleForTesting - static final int MAX_NUM = 3; + static final int MAX_NUM = 2; /** * Parses the string arrays to create a list of {@link ToolItem}. @@ -82,7 +82,8 @@ public class HearingDevicesToolItemParser { useCustomIcons ? iconList.get(i) : activityInfoList.get(i).loadIcon(packageManager), new Intent(Intent.ACTION_MAIN).setComponent( - activityInfoList.get(i).getComponentName()) + activityInfoList.get(i).getComponentName()), + useCustomIcons )); } diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/ToolItem.kt b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/ToolItem.kt index 66bb2b5e2328..ef03f0cdef79 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/ToolItem.kt +++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/ToolItem.kt @@ -23,4 +23,5 @@ data class ToolItem( var toolName: String = "", var toolIcon: Drawable, var toolIntent: Intent, + var isCustomIcon: Boolean, ) diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractor.kt index 2c026c0bb5ce..7337e5af51a1 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractor.kt @@ -30,6 +30,7 @@ import com.android.internal.util.EmergencyAffordanceManager import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor import com.android.systemui.bouncer.data.repository.EmergencyServicesRepository import com.android.systemui.bouncer.shared.model.BouncerActionButtonModel +import com.android.systemui.bouncer.ui.helper.BouncerHapticPlayer import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background @@ -74,6 +75,7 @@ constructor( private val metricsLogger: MetricsLogger, private val dozeLogger: DozeLogger, private val sceneInteractor: Lazy<SceneInteractor>, + private val bouncerHapticPlayer: BouncerHapticPlayer, ) { /** The bouncer action button. If `null`, the button should not be shown. */ val actionButton: Flow<BouncerActionButtonModel?> = @@ -111,6 +113,8 @@ constructor( BouncerActionButtonModel( label = applicationContext.getString(R.string.lockscreen_emergency_call), onClick = { + // TODO(b/373930432): haptics should be played at the UI layer -> refactor + bouncerHapticPlayer.playEmergencyButtonClickFeedback() prepareToPerformAction() dozeLogger.logEmergencyCall() startEmergencyDialerActivity() diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticPlayer.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticPlayer.kt index d6b92115c64b..837390730c7a 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticPlayer.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/helper/BouncerHapticPlayer.kt @@ -81,4 +81,11 @@ class BouncerHapticPlayer @Inject constructor(private val msdlPlayer: dagger.Laz /** Deliver MSDL feedback when a numpad key is pressed on the pin bouncer */ fun playNumpadKeyFeedback() = msdlPlayer.get().playToken(MSDLToken.KEYPRESS_STANDARD) + + /** Deliver MSDL feedback when clicking on the emergency button */ + fun playEmergencyButtonClickFeedback() { + if (isEnabled) { + msdlPlayer.get().playToken(MSDLToken.KEYPRESS_RETURN) + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayStore.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayStore.kt new file mode 100644 index 000000000000..2ce3e43389fa --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/PerDisplayStore.kt @@ -0,0 +1,108 @@ +/* + * Copyright (C) 2024 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.systemui.display.data.repository + +import android.view.Display +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.qualifiers.Background +import java.io.PrintWriter +import java.util.concurrent.ConcurrentHashMap +import kotlinx.coroutines.CoroutineName +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +/** Provides per display instances of [T]. */ +interface PerDisplayStore<T> { + + /** + * The instance for the default/main display of the device. For example, on a phone or a tablet, + * the default display is the internal/built-in display of the device. + * + * Note that the id of the default display is [Display.DEFAULT_DISPLAY]. + */ + val defaultDisplay: T + + /** + * Returns an instance for a specific display id. + * + * @throws IllegalArgumentException if [displayId] doesn't match the id of any existing + * displays. + */ + fun forDisplay(displayId: Int): T +} + +abstract class PerDisplayStoreImpl<T>( + @Background private val backgroundApplicationScope: CoroutineScope, + private val displayRepository: DisplayRepository, +) : PerDisplayStore<T>, CoreStartable { + + private val perDisplayInstances = ConcurrentHashMap<Int, T>() + + /** + * The instance for the default/main display of the device. For example, on a phone or a tablet, + * the default display is the internal/built-in display of the device. + * + * Note that the id of the default display is [Display.DEFAULT_DISPLAY]. + */ + override val defaultDisplay: T + get() = forDisplay(Display.DEFAULT_DISPLAY) + + /** + * Returns an instance for a specific display id. + * + * @throws IllegalArgumentException if [displayId] doesn't match the id of any existing + * displays. + */ + override fun forDisplay(displayId: Int): T { + if (displayRepository.getDisplay(displayId) == null) { + throw IllegalArgumentException("Display with id $displayId doesn't exist.") + } + return perDisplayInstances.computeIfAbsent(displayId) { + createInstanceForDisplay(displayId) + } + } + + abstract fun createInstanceForDisplay(displayId: Int): T + + override fun start() { + val instanceType = instanceClass.simpleName + backgroundApplicationScope.launch(CoroutineName("PerDisplayStore#<$instanceType>start")) { + displayRepository.displayRemovalEvent.collect { removedDisplayId -> + val removedInstance = perDisplayInstances.remove(removedDisplayId) + removedInstance?.let { onDisplayRemovalAction(it) } + } + } + } + + abstract val instanceClass: Class<T> + + /** + * Will be called when the display associated with [instance] was removed. It allows to perform + * any clean up if needed. + */ + open suspend fun onDisplayRemovalAction(instance: T) {} + + override fun dump(pw: PrintWriter, args: Array<out String>) { + pw.println(perDisplayInstances) + } +} + +class SingleDisplayStore<T>(defaultInstance: T) : PerDisplayStore<T> { + override val defaultDisplay: T = defaultInstance + + override fun forDisplay(displayId: Int): T = defaultDisplay +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt index 801a0ce4b744..537b56bccae8 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/modes/ui/ModesTileMapper.kt @@ -39,7 +39,7 @@ constructor(@Main private val resources: Resources, val theme: Resources.Theme) val loadedIcon: Icon.Loaded = when (val dataIcon = data.icon) { is Icon.Resource -> { - if (iconRes != dataIcon.res) { + if (data.iconResId != dataIcon.res) { Log.wtf( "ModesTileMapper", "Icon.Resource.res & iconResId are not identical", diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarInitializerStore.kt b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarInitializerStore.kt index 6c3802676f26..041f0b0fdf93 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarInitializerStore.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/core/StatusBarInitializerStore.kt @@ -16,88 +16,52 @@ package com.android.systemui.statusbar.core -import android.view.Display -import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.display.data.repository.DisplayRepository +import com.android.systemui.display.data.repository.PerDisplayStore +import com.android.systemui.display.data.repository.PerDisplayStoreImpl +import com.android.systemui.display.data.repository.SingleDisplayStore import com.android.systemui.statusbar.window.StatusBarWindowControllerStore -import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject -import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch /** Provides per display instances of [StatusBarInitializer]. */ -interface StatusBarInitializerStore { - /** - * The instance for the default/main display of the device. For example, on a phone or a tablet, - * the default display is the internal/built-in display of the device. - * - * Note that the id of the default display is [Display.DEFAULT_DISPLAY]. - */ - val defaultDisplay: StatusBarInitializer - - /** - * Returns an instance for a specific display id. - * - * @throws IllegalArgumentException if [displayId] doesn't match the id of any existing - * displays. - */ - fun forDisplay(displayId: Int): StatusBarInitializer -} +interface StatusBarInitializerStore : PerDisplayStore<StatusBarInitializer> @SysUISingleton class MultiDisplayStatusBarInitializerStore @Inject constructor( - @Background private val backgroundApplicationScope: CoroutineScope, + @Background backgroundApplicationScope: CoroutineScope, + displayRepository: DisplayRepository, private val factory: StatusBarInitializer.Factory, - private val displayRepository: DisplayRepository, private val statusBarWindowControllerStore: StatusBarWindowControllerStore, -) : StatusBarInitializerStore, CoreStartable { +) : + StatusBarInitializerStore, + PerDisplayStoreImpl<StatusBarInitializer>(backgroundApplicationScope, displayRepository) { init { StatusBarConnectedDisplays.assertInNewMode() } - private val perDisplayInitializers = ConcurrentHashMap<Int, StatusBarInitializer>() - - override val defaultDisplay: StatusBarInitializer - get() = forDisplay(Display.DEFAULT_DISPLAY) - - override fun forDisplay(displayId: Int): StatusBarInitializer { - if (displayRepository.getDisplay(displayId) == null) { - throw IllegalArgumentException("Display with id $displayId doesn't exist.") - } - return perDisplayInitializers.computeIfAbsent(displayId) { - factory.create( - statusBarWindowController = statusBarWindowControllerStore.forDisplay(displayId) - ) - } + override fun createInstanceForDisplay(displayId: Int): StatusBarInitializer { + return factory.create( + statusBarWindowController = statusBarWindowControllerStore.forDisplay(displayId) + ) } - override fun start() { - backgroundApplicationScope.launch( - CoroutineName("MultiDisplayStatusBarInitializerStore#start") - ) { - displayRepository.displayRemovalEvent.collect { removedDisplayId -> - perDisplayInitializers.remove(removedDisplayId) - } - } - } + override val instanceClass = StatusBarInitializer::class.java } @SysUISingleton class SingleDisplayStatusBarInitializerStore @Inject -constructor(private val defaultInstance: StatusBarInitializer) : StatusBarInitializerStore { +constructor(defaultInitializer: StatusBarInitializer) : + StatusBarInitializerStore, + PerDisplayStore<StatusBarInitializer> by SingleDisplayStore(defaultInitializer) { init { StatusBarConnectedDisplays.assertInLegacyMode() } - - override val defaultDisplay: StatusBarInitializer = defaultInstance - - override fun forDisplay(displayId: Int): StatusBarInitializer = defaultInstance } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt index 5aad11fe1034..f6f4503b210a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt @@ -17,17 +17,20 @@ package com.android.systemui.statusbar.dagger import android.content.Context -import com.android.app.viewcapture.ViewCaptureAwareWindowManager import com.android.systemui.CoreStartable +import com.android.systemui.SysUICutoutProvider import com.android.systemui.dagger.SysUISingleton import com.android.systemui.log.LogBuffer import com.android.systemui.log.LogBufferFactory import com.android.systemui.statusbar.core.StatusBarConnectedDisplays import com.android.systemui.statusbar.data.StatusBarDataLayerModule import com.android.systemui.statusbar.phone.LightBarController +import com.android.systemui.statusbar.phone.StatusBarContentInsetsProvider +import com.android.systemui.statusbar.phone.StatusBarContentInsetsProviderImpl import com.android.systemui.statusbar.phone.StatusBarSignalPolicy import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallLog +import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.ui.SystemBarUtilsProxyImpl import com.android.systemui.statusbar.window.MultiDisplayStatusBarWindowControllerStore import com.android.systemui.statusbar.window.SingleDisplayStatusBarWindowControllerStore @@ -108,5 +111,16 @@ abstract class StatusBarModule { fun provideOngoingCallLogBuffer(factory: LogBufferFactory): LogBuffer { return factory.create("OngoingCall", 75) } + + @Provides + @SysUISingleton + fun contentInsetsProvider( + factory: StatusBarContentInsetsProviderImpl.Factory, + context: Context, + configurationController: ConfigurationController, + sysUICutoutProvider: SysUICutoutProvider, + ): StatusBarContentInsetsProvider { + return factory.create(context, configurationController, sysUICutoutProvider) + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt index 613efaa148f5..c6f6bd90fce6 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarContentInsetsProvider.kt @@ -34,10 +34,10 @@ import com.android.systemui.Dumpable import com.android.systemui.StatusBarInsetsCommand import com.android.systemui.SysUICutoutInformation import com.android.systemui.SysUICutoutProvider -import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dump.DumpManager import com.android.systemui.res.R import com.android.systemui.statusbar.commandline.CommandRegistry +import com.android.systemui.statusbar.phone.StatusBarContentInsetsProviderImpl.CacheKey import com.android.systemui.statusbar.policy.CallbackController import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.util.leak.RotationUtils.ROTATION_LANDSCAPE @@ -47,9 +47,11 @@ import com.android.systemui.util.leak.RotationUtils.ROTATION_UPSIDE_DOWN import com.android.systemui.util.leak.RotationUtils.Rotation import com.android.systemui.util.leak.RotationUtils.getExactRotation import com.android.systemui.util.leak.RotationUtils.getResourcesForRotation +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject import java.io.PrintWriter import java.lang.Math.max -import javax.inject.Inject /** * Encapsulates logic that can solve for the left/right insets required for the status bar contents. @@ -64,19 +66,87 @@ import javax.inject.Inject * * NOTE: This class is not threadsafe */ -@SysUISingleton -class StatusBarContentInsetsProvider -@Inject +interface StatusBarContentInsetsProvider : + CallbackController<StatusBarContentInsetsChangedListener> { + + /** + * Some views may need to care about whether or not the current top display cutout is located in + * the corner rather than somewhere in the center. In the case of a corner cutout, the status + * bar area is contiguous. + */ + fun currentRotationHasCornerCutout(): Boolean + + /** + * Calculates the maximum bounding rectangle for the privacy chip animation + ongoing privacy + * dot in the coordinates relative to the given rotation. + * + * @param rotation the rotation for which the bounds are required. This is an absolute value + * (i.e., ROTATION_NONE will always return the same bounds regardless of the context from + * which this method is called) + */ + fun getBoundingRectForPrivacyChipForRotation( + @Rotation rotation: Int, + displayCutout: DisplayCutout?, + ): Rect + + /** + * Calculate the distance from the left, right and top edges of the screen to the status bar + * content area. This differs from the content area rects in that these values can be used + * directly as padding. + * + * @param rotation the target rotation for which to calculate insets + */ + fun getStatusBarContentInsetsForRotation(@Rotation rotation: Int): Insets + + /** + * Calculate the insets for the status bar content in the device's current rotation + * + * @see getStatusBarContentAreaForRotation + */ + fun getStatusBarContentInsetsForCurrentRotation(): Insets + + /** + * Calculates the area of the status bar contents invariant of the current device rotation, in + * the target rotation's coordinates + * + * @param rotation the rotation for which the bounds are required. This is an absolute value + * (i.e., ROTATION_NONE will always return the same bounds regardless of the context from + * which this method is called) + */ + fun getStatusBarContentAreaForRotation(@Rotation rotation: Int): Rect + + /** Get the status bar content area for the given rotation, in absolute bounds */ + fun getStatusBarContentAreaForCurrentRotation(): Rect + + fun getStatusBarPaddingTop(@Rotation rotation: Int? = null): Int + + interface Factory { + fun create( + context: Context, + configurationController: ConfigurationController, + sysUICutoutProvider: SysUICutoutProvider, + ): StatusBarContentInsetsProvider + } +} + +class StatusBarContentInsetsProviderImpl +@AssistedInject constructor( - val context: Context, - val configurationController: ConfigurationController, + @Assisted val context: Context, + @Assisted val configurationController: ConfigurationController, val dumpManager: DumpManager, val commandRegistry: CommandRegistry, - val sysUICutoutProvider: SysUICutoutProvider, -) : - CallbackController<StatusBarContentInsetsChangedListener>, - ConfigurationController.ConfigurationListener, - Dumpable { + @Assisted val sysUICutoutProvider: SysUICutoutProvider, +) : StatusBarContentInsetsProvider, ConfigurationController.ConfigurationListener, Dumpable { + + @AssistedFactory + interface Factory : StatusBarContentInsetsProvider.Factory { + override fun create( + context: Context, + configurationController: ConfigurationController, + sysUICutoutProvider: SysUICutoutProvider, + ): StatusBarContentInsetsProviderImpl + } // Limit cache size as potentially we may connect large number of displays // (e.g. network displays) @@ -95,7 +165,7 @@ constructor( object : StatusBarInsetsCommand.Callback { override fun onExecute( command: StatusBarInsetsCommand, - printWriter: PrintWriter + printWriter: PrintWriter, ) { executeCommand(command, printWriter) } @@ -133,12 +203,7 @@ constructor( listeners.forEach { it.onStatusBarContentInsetsChanged() } } - /** - * Some views may need to care about whether or not the current top display cutout is located in - * the corner rather than somewhere in the center. In the case of a corner cutout, the status - * bar area is contiguous. - */ - fun currentRotationHasCornerCutout(): Boolean { + override fun currentRotationHasCornerCutout(): Boolean { val cutout = checkNotNull(context.display).cutout ?: return false val topBounds = cutout.boundingRectTop @@ -148,17 +213,9 @@ constructor( return topBounds.left <= 0 || topBounds.right >= point.x } - /** - * Calculates the maximum bounding rectangle for the privacy chip animation + ongoing privacy - * dot in the coordinates relative to the given rotation. - * - * @param rotation the rotation for which the bounds are required. This is an absolute value - * (i.e., ROTATION_NONE will always return the same bounds regardless of the context from - * which this method is called) - */ - fun getBoundingRectForPrivacyChipForRotation( + override fun getBoundingRectForPrivacyChipForRotation( @Rotation rotation: Int, - displayCutout: DisplayCutout? + displayCutout: DisplayCutout?, ): Rect { val key = getCacheKey(rotation, displayCutout) var insets = insetsCache[key] @@ -176,14 +233,7 @@ constructor( return getPrivacyChipBoundingRectForInsets(insets, dotWidth, chipWidth, isRtl) } - /** - * Calculate the distance from the left, right and top edges of the screen to the status bar - * content area. This differs from the content area rects in that these values can be used - * directly as padding. - * - * @param rotation the target rotation for which to calculate insets - */ - fun getStatusBarContentInsetsForRotation(@Rotation rotation: Int): Insets = + override fun getStatusBarContentInsetsForRotation(@Rotation rotation: Int): Insets = traceSection(tag = "StatusBarContentInsetsProvider.getStatusBarContentInsetsForRotation") { val sysUICutout = sysUICutoutProvider.cutoutInfoForCurrentDisplayAndRotation() val displayCutout = sysUICutout?.cutout @@ -202,31 +252,17 @@ constructor( rotation, sysUICutout, getResourcesForRotation(rotation, context), - key + key, ) Insets.of(area.left, area.top, /* right= */ width - area.right, /* bottom= */ 0) } - /** - * Calculate the insets for the status bar content in the device's current rotation - * - * @see getStatusBarContentAreaForRotation - */ - fun getStatusBarContentInsetsForCurrentRotation(): Insets { + override fun getStatusBarContentInsetsForCurrentRotation(): Insets { return getStatusBarContentInsetsForRotation(getExactRotation(context)) } - /** - * Calculates the area of the status bar contents invariant of the current device rotation, in - * the target rotation's coordinates - * - * @param rotation the rotation for which the bounds are required. This is an absolute value - * (i.e., ROTATION_NONE will always return the same bounds regardless of the context from - * which this method is called) - */ - @JvmOverloads - fun getStatusBarContentAreaForRotation(@Rotation rotation: Int): Rect { + override fun getStatusBarContentAreaForRotation(@Rotation rotation: Int): Rect { val sysUICutout = sysUICutoutProvider.cutoutInfoForCurrentDisplayAndRotation() val displayCutout = sysUICutout?.cutout val key = getCacheKey(rotation, displayCutout) @@ -235,12 +271,11 @@ constructor( rotation, sysUICutout, getResourcesForRotation(rotation, context), - key + key, ) } - /** Get the status bar content area for the given rotation, in absolute bounds */ - fun getStatusBarContentAreaForCurrentRotation(): Rect { + override fun getStatusBarContentAreaForCurrentRotation(): Rect { val rotation = getExactRotation(context) return getStatusBarContentAreaForRotation(rotation) } @@ -249,7 +284,7 @@ constructor( @Rotation targetRotation: Int, sysUICutout: SysUICutoutInformation?, rotatedResources: Resources, - key: CacheKey + key: CacheKey, ): Rect { return getCalculatedAreaForRotation(sysUICutout, targetRotation, rotatedResources).also { insetsCache.put(key, it) @@ -259,7 +294,7 @@ constructor( private fun getCalculatedAreaForRotation( sysUICutout: SysUICutoutInformation?, @Rotation targetRotation: Int, - rotatedResources: Resources + rotatedResources: Resources, ): Rect { val currentRotation = getExactRotation(context) @@ -299,7 +334,7 @@ constructor( configurationController.isLayoutRtl, dotWidth, bottomAlignedMargin, - statusBarContentHeight + statusBarContentHeight, ) } @@ -349,7 +384,7 @@ constructor( return resources.getDimensionPixelSize(dimenRes) } - fun getStatusBarPaddingTop(@Rotation rotation: Int? = null): Int { + override fun getStatusBarPaddingTop(@Rotation rotation: Int?): Int { val res = rotation?.let { it -> getResourcesForRotation(it, context) } ?: context.resources return res.getDimensionPixelSize(R.dimen.status_bar_padding_top) } @@ -364,13 +399,13 @@ constructor( CacheKey( rotation = rotation, displaySize = Rect(context.resources.configuration.windowConfiguration.maxBounds), - displayCutout = displayCutout + displayCutout = displayCutout, ) private data class CacheKey( @Rotation val rotation: Int, val displaySize: Rect, - val displayCutout: DisplayCutout? + val displayCutout: DisplayCutout?, ) } @@ -395,21 +430,21 @@ fun getPrivacyChipBoundingRectForInsets( contentRect: Rect, dotWidth: Int, chipWidth: Int, - isRtl: Boolean + isRtl: Boolean, ): Rect { return if (isRtl) { Rect( contentRect.left - dotWidth, contentRect.top, contentRect.left + chipWidth, - contentRect.bottom + contentRect.bottom, ) } else { Rect( contentRect.right - chipWidth, contentRect.top, contentRect.right + dotWidth, - contentRect.bottom + contentRect.bottom, ) } } @@ -443,7 +478,7 @@ fun calculateInsetsForRotationWithRotatedResources( isRtl: Boolean, dotWidth: Int, bottomAlignedMargin: Int, - statusBarContentHeight: Int + statusBarContentHeight: Int, ): Rect { /* TODO: Check if this is ever used for devices with no rounded corners @@ -467,7 +502,7 @@ fun calculateInsetsForRotationWithRotatedResources( targetRotation, currentRotation, bottomAlignedMargin, - statusBarContentHeight + statusBarContentHeight, ) } @@ -503,7 +538,7 @@ private fun getStatusBarContentBounds( @Rotation targetRotation: Int, @Rotation currentRotation: Int, bottomAlignedMargin: Int, - statusBarContentHeight: Int + statusBarContentHeight: Int, ): Rect { val insetTop = getInsetTop(bottomAlignedMargin, statusBarContentHeight, sbHeight) @@ -597,7 +632,7 @@ private val DisplayCutout.boundingRectsLeftRightTop private fun getInsetTop( bottomAlignedMargin: Int, statusBarContentHeight: Int, - statusBarHeight: Int + statusBarHeight: Int, ): Int { val bottomAlignmentEnabled = bottomAlignedMargin >= 0 if (!bottomAlignmentEnabled) { @@ -610,7 +645,7 @@ private fun getInsetTop( private fun sbRect( @Rotation relativeRotation: Int, sbHeight: Int, - displaySize: Pair<Int, Int> + displaySize: Pair<Int, Int>, ): Rect { val w = displaySize.first val h = displaySize.second @@ -626,7 +661,7 @@ private fun shareShortEdge( sbRect: Rect, cutoutRect: Rect, currentWidth: Int, - currentHeight: Int + currentHeight: Int, ): Boolean { if (currentWidth < currentHeight) { // Check top/bottom edges by extending the width of the display cutout rect and checking diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerStore.kt b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerStore.kt index 7d0dadcf8c6e..7a88dcd92b88 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerStore.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerStore.kt @@ -17,78 +17,40 @@ package com.android.systemui.statusbar.window import android.content.Context -import android.view.Display import android.view.WindowManager import com.android.app.viewcapture.ViewCaptureAwareWindowManager -import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.display.data.repository.DisplayRepository import com.android.systemui.display.data.repository.DisplayWindowPropertiesRepository +import com.android.systemui.display.data.repository.PerDisplayStore +import com.android.systemui.display.data.repository.PerDisplayStoreImpl +import com.android.systemui.display.data.repository.SingleDisplayStore import com.android.systemui.statusbar.core.StatusBarConnectedDisplays -import java.util.concurrent.ConcurrentHashMap import javax.inject.Inject -import kotlinx.coroutines.CoroutineName import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.launch /** Store that allows to retrieve per display instances of [StatusBarWindowController]. */ -interface StatusBarWindowControllerStore { - /** - * The instance for the default/main display of the device. For example, on a phone or a tablet, - * the default display is the internal/built-in display of the device. - * - * Note that the id of the default display is [Display.DEFAULT_DISPLAY]. - */ - val defaultDisplay: StatusBarWindowController - - /** - * Returns an instance for a specific display id. - * - * @throws IllegalArgumentException if [displayId] doesn't match the id of any existing - * displays. - */ - fun forDisplay(displayId: Int): StatusBarWindowController -} +interface StatusBarWindowControllerStore : PerDisplayStore<StatusBarWindowController> @SysUISingleton class MultiDisplayStatusBarWindowControllerStore @Inject constructor( - @Background private val backgroundApplicationScope: CoroutineScope, + @Background backgroundApplicationScope: CoroutineScope, private val controllerFactory: StatusBarWindowController.Factory, private val displayWindowPropertiesRepository: DisplayWindowPropertiesRepository, private val viewCaptureAwareWindowManagerFactory: ViewCaptureAwareWindowManager.Factory, - private val displayRepository: DisplayRepository, -) : StatusBarWindowControllerStore, CoreStartable { + displayRepository: DisplayRepository, +) : + StatusBarWindowControllerStore, + PerDisplayStoreImpl<StatusBarWindowController>(backgroundApplicationScope, displayRepository) { init { StatusBarConnectedDisplays.assertInNewMode() } - private val perDisplayControllers = ConcurrentHashMap<Int, StatusBarWindowController>() - - override fun start() { - backgroundApplicationScope.launch(CoroutineName("StatusBarWindowController#start")) { - displayRepository.displayRemovalEvent.collect { displayId -> - perDisplayControllers.remove(displayId) - } - } - } - - override val defaultDisplay: StatusBarWindowController - get() = forDisplay(Display.DEFAULT_DISPLAY) - - override fun forDisplay(displayId: Int): StatusBarWindowController { - if (displayRepository.getDisplay(displayId) == null) { - throw IllegalArgumentException("Display with id $displayId doesn't exist.") - } - return perDisplayControllers.computeIfAbsent(displayId) { - createControllerForDisplay(displayId) - } - } - - private fun createControllerForDisplay(displayId: Int): StatusBarWindowController { + override fun createInstanceForDisplay(displayId: Int): StatusBarWindowController { val statusBarDisplayContext = displayWindowPropertiesRepository.get( displayId = displayId, @@ -101,6 +63,8 @@ constructor( viewCaptureAwareWindowManager, ) } + + override val instanceClass = StatusBarWindowController::class.java } @SysUISingleton @@ -110,16 +74,13 @@ constructor( context: Context, viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager, factory: StatusBarWindowControllerImpl.Factory, -) : StatusBarWindowControllerStore { +) : + StatusBarWindowControllerStore, + PerDisplayStore<StatusBarWindowController> by SingleDisplayStore( + factory.create(context, viewCaptureAwareWindowManager) + ) { init { StatusBarConnectedDisplays.assertInLegacyMode() } - - private val controller: StatusBarWindowController = - factory.create(context, viewCaptureAwareWindowManager) - - override val defaultDisplay = controller - - override fun forDisplay(displayId: Int) = controller } diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt index 75c66f234bdc..90c005139c56 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt @@ -67,7 +67,7 @@ class DistanceBasedGestureRecognizerProvider( val distanceThresholdPx = resources.getDimensionPixelSize( com.android.internal.R.dimen.system_gestures_distance_threshold - ) + ) * 5 return remember(distanceThresholdPx) { recognizerFactory(distanceThresholdPx, gestureStateChangedCallback) } @@ -77,7 +77,8 @@ class DistanceBasedGestureRecognizerProvider( fun GestureState.toTutorialActionState(): TutorialActionState { return when (this) { NotStarted -> TutorialActionState.NotStarted - is InProgress -> TutorialActionState.InProgress(progress) + // progress is disabled for now as views are not ready to handle varying progress + is InProgress -> TutorialActionState.InProgress(0f) Finished -> TutorialActionState.Finished } } diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizer.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizer.kt index 56e97a357d67..80f800390852 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizer.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureRecognizer.kt @@ -16,10 +16,14 @@ package com.android.systemui.touchpad.tutorial.ui.gesture +import android.util.MathUtils import android.view.MotionEvent import kotlin.math.abs -/** Recognizes touchpad back gesture, that is three fingers swiping left or right */ +/** + * Recognizes touchpad back gesture, that is - using three fingers on touchpad - swiping left or + * right. + */ class BackGestureRecognizer(private val gestureDistanceThresholdPx: Int) : GestureRecognizer { private val distanceTracker = DistanceTracker() @@ -36,7 +40,7 @@ class BackGestureRecognizer(private val gestureDistanceThresholdPx: Int) : Gestu gestureStateChangedCallback, gestureState, isFinished = { abs(it.deltaX) >= gestureDistanceThresholdPx }, - progress = { 0f }, + progress = { MathUtils.saturate(abs(it.deltaX / gestureDistanceThresholdPx)) }, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizer.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizer.kt index 3db9d7ccc8f7..2b84a4c50613 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizer.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureRecognizer.kt @@ -16,9 +16,10 @@ package com.android.systemui.touchpad.tutorial.ui.gesture +import android.util.MathUtils import android.view.MotionEvent -/** Recognizes touchpad home gesture, that is three fingers swiping up */ +/** Recognizes touchpad home gesture, that is - using three fingers on touchpad - swiping up. */ class HomeGestureRecognizer(private val gestureDistanceThresholdPx: Int) : GestureRecognizer { private val distanceTracker = DistanceTracker() @@ -35,7 +36,7 @@ class HomeGestureRecognizer(private val gestureDistanceThresholdPx: Int) : Gestu gestureStateChangedCallback, gestureState, isFinished = { -it.deltaY >= gestureDistanceThresholdPx }, - progress = { 0f }, + progress = { MathUtils.saturate(-it.deltaY / gestureDistanceThresholdPx) }, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizer.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizer.kt index a194ad6a8016..69b7c5edd750 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizer.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureRecognizer.kt @@ -16,13 +16,14 @@ package com.android.systemui.touchpad.tutorial.ui.gesture +import android.util.MathUtils import android.view.MotionEvent import kotlin.math.abs /** - * Recognizes apps gesture completion. That is - using three fingers on touchpad - swipe up over - * some distance threshold and then slow down gesture before fingers are lifted. Implementation is - * based on [com.android.quickstep.util.TriggerSwipeUpTouchTracker] + * Recognizes recent apps gesture, that is - using three fingers on touchpad - swipe up over some + * distance threshold and then slow down gesture before fingers are lifted. Implementation is based + * on [com.android.quickstep.util.TriggerSwipeUpTouchTracker] */ class RecentAppsGestureRecognizer( private val gestureDistanceThresholdPx: Int, @@ -49,7 +50,7 @@ class RecentAppsGestureRecognizer( -state.deltaY >= gestureDistanceThresholdPx && abs(velocityTracker.calculateVelocity().value) <= velocityThresholdPxPerMs }, - progress = { 0f }, + progress = { MathUtils.saturate(-it.deltaY / gestureDistanceThresholdPx) }, ) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt index 8b427fbc5fb8..071acfa44650 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/animation/TransitionAnimatorTest.kt @@ -156,7 +156,7 @@ class TransitionAnimatorTest(val useSpring: Boolean) : SysuiTestCase() { createEndState(transitionContainer), backgroundLayer, fadeWindowBackgroundLayer, - useSpring, + useSpring = useSpring, ) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/core/MultiDisplayStatusBarInitializerStoreTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/core/MultiDisplayStatusBarInitializerStoreTest.kt deleted file mode 100644 index 0d1d37af7e5b..000000000000 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/core/MultiDisplayStatusBarInitializerStoreTest.kt +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright (C) 2024 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.systemui.statusbar.core - -import android.platform.test.annotations.EnableFlags -import android.view.Display -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.SmallTest -import com.android.systemui.SysuiTestCase -import com.android.systemui.display.data.repository.displayRepository -import com.android.systemui.kosmos.testDispatcher -import com.android.systemui.kosmos.testScope -import com.android.systemui.kosmos.unconfinedTestDispatcher -import com.android.systemui.testKosmos -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith - -@RunWith(AndroidJUnit4::class) -@SmallTest -@EnableFlags(StatusBarConnectedDisplays.FLAG_NAME) -class MultiDisplayStatusBarInitializerStoreTest : SysuiTestCase() { - - private val kosmos = - testKosmos().also { - // Using unconfinedTestDispatcher to avoid having to call `runCurrent` in the tests. - it.testDispatcher = it.unconfinedTestDispatcher - } - private val testScope = kosmos.testScope - private val fakeDisplayRepository = kosmos.displayRepository - private val store = kosmos.multiDisplayStatusBarInitializerStore - - @Before - fun start() { - store.start() - } - - @Before - fun addDisplays() = runBlocking { - fakeDisplayRepository.addDisplay(DEFAULT_DISPLAY_ID) - fakeDisplayRepository.addDisplay(NON_DEFAULT_DISPLAY_ID) - } - - @Test - fun forDisplay_defaultDisplay_multipleCalls_returnsSameInstance() = - testScope.runTest { - val controller = store.defaultDisplay - - assertThat(store.defaultDisplay).isSameInstanceAs(controller) - } - - @Test - fun forDisplay_nonDefaultDisplay_multipleCalls_returnsSameInstance() = - testScope.runTest { - val controller = store.forDisplay(NON_DEFAULT_DISPLAY_ID) - - assertThat(store.forDisplay(NON_DEFAULT_DISPLAY_ID)).isSameInstanceAs(controller) - } - - @Test - fun forDisplay_nonDefaultDisplay_afterDisplayRemoved_returnsNewInstance() = - testScope.runTest { - val controller = store.forDisplay(NON_DEFAULT_DISPLAY_ID) - - fakeDisplayRepository.removeDisplay(NON_DEFAULT_DISPLAY_ID) - fakeDisplayRepository.addDisplay(NON_DEFAULT_DISPLAY_ID) - - assertThat(store.forDisplay(NON_DEFAULT_DISPLAY_ID)).isNotSameInstanceAs(controller) - } - - @Test(expected = IllegalArgumentException::class) - fun forDisplay_nonExistingDisplayId_throws() = - testScope.runTest { store.forDisplay(NON_EXISTING_DISPLAY_ID) } - - companion object { - private const val DEFAULT_DISPLAY_ID = Display.DEFAULT_DISPLAY - private const val NON_DEFAULT_DISPLAY_ID = Display.DEFAULT_DISPLAY + 1 - private const val NON_EXISTING_DISPLAY_ID = Display.DEFAULT_DISPLAY + 2 - } -} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorKosmos.kt index 3087d01a2479..77ec83871016 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerActionButtonInteractorKosmos.kt @@ -23,6 +23,7 @@ import com.android.internal.logging.metricsLogger import com.android.internal.util.emergencyAffordanceManager import com.android.systemui.authentication.domain.interactor.authenticationInteractor import com.android.systemui.bouncer.data.repository.emergencyServicesRepository +import com.android.systemui.haptics.msdl.bouncerHapticPlayer import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.kosmos.testDispatcher @@ -52,5 +53,6 @@ val Kosmos.bouncerActionButtonInteractor by Fixture { metricsLogger = metricsLogger, dozeLogger = mock(), sceneInteractor = { sceneInteractor }, + bouncerHapticPlayer, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/PerDisplayStoreKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/PerDisplayStoreKosmos.kt new file mode 100644 index 000000000000..e3797260ed6d --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/PerDisplayStoreKosmos.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2024 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.systemui.display.data.repository + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import kotlinx.coroutines.CoroutineScope + +class FakePerDisplayStore( + backgroundApplicationScope: CoroutineScope, + displayRepository: DisplayRepository, +) : PerDisplayStoreImpl<TestPerDisplayInstance>(backgroundApplicationScope, displayRepository) { + + val removalActions = mutableListOf<TestPerDisplayInstance>() + + override fun createInstanceForDisplay(displayId: Int): TestPerDisplayInstance { + return TestPerDisplayInstance(displayId) + } + + override val instanceClass = TestPerDisplayInstance::class.java + + override suspend fun onDisplayRemovalAction(instance: TestPerDisplayInstance) { + removalActions += instance + } +} + +data class TestPerDisplayInstance(val displayId: Int) + +val Kosmos.fakePerDisplayStore by + Kosmos.Fixture { + FakePerDisplayStore( + backgroundApplicationScope = applicationCoroutineScope, + displayRepository = displayRepository, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarInitializerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarInitializerKosmos.kt index 8066b9138c99..303529b7f7b0 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarInitializerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/core/StatusBarInitializerKosmos.kt @@ -34,8 +34,8 @@ val Kosmos.multiDisplayStatusBarInitializerStore by Kosmos.Fixture { MultiDisplayStatusBarInitializerStore( applicationCoroutineScope, - fakeStatusBarInitializerFactory, displayRepository, + fakeStatusBarInitializerFactory, fakeStatusBarWindowControllerStore, ) } diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java index 24950e65c174..afa7a6c51abb 100644 --- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java @@ -225,14 +225,9 @@ public class RavenwoodRuntimeEnvironmentController { ActivityManager.init$ravenwood(config.mCurrentUser); - final HandlerThread main; - if (config.mProvideMainThread) { - main = new HandlerThread(MAIN_THREAD_NAME); - main.start(); - Looper.setMainLooperForTest(main.getLooper()); - } else { - main = null; - } + final var main = new HandlerThread(MAIN_THREAD_NAME); + main.start(); + Looper.setMainLooperForTest(main.getLooper()); final boolean isSelfInstrumenting = Objects.equals(config.mTestPackageName, config.mTargetPackageName); @@ -324,10 +319,8 @@ public class RavenwoodRuntimeEnvironmentController { } sMockUiAutomation.dropShellPermissionIdentity(); - if (config.mProvideMainThread) { - Looper.getMainLooper().quit(); - Looper.clearMainLooperForTest(); - } + Looper.getMainLooper().quit(); + Looper.clearMainLooperForTest(); ActivityManager.reset$ravenwood(); diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodConfig.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodConfig.java index 446f819ad41b..1f6e11dd5cf2 100644 --- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodConfig.java +++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodConfig.java @@ -152,7 +152,10 @@ public final class RavenwoodConfig { /** * Configure a "main" thread to be available for the duration of the test, as defined * by {@code Looper.getMainLooper()}. Has no effect on non-Ravenwood environments. + * + * @deprecated */ + @Deprecated public Builder setProvideMainThread(boolean provideMainThread) { mConfig.mProvideMainThread = provideMainThread; return this; diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java index 4196d8e22610..93a6806ed1f4 100644 --- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java +++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java @@ -139,7 +139,10 @@ public final class RavenwoodRule implements TestRule { /** * Configure a "main" thread to be available for the duration of the test, as defined * by {@code Looper.getMainLooper()}. Has no effect on non-Ravenwood environments. + * + * @deprecated */ + @Deprecated public Builder setProvideMainThread(boolean provideMainThread) { mBuilder.setProvideMainThread(provideMainThread); return this; diff --git a/ravenwood/tests/bivalenttest/Android.bp b/ravenwood/tests/bivalenttest/Android.bp index ac499b966afe..d7f4b3e2955d 100644 --- a/ravenwood/tests/bivalenttest/Android.bp +++ b/ravenwood/tests/bivalenttest/Android.bp @@ -54,34 +54,36 @@ android_ravenwood_test { auto_gen_config: true, } -// TODO(b/371215487): migrate bivalenttest.ravenizer tests to another architecture +android_test { + name: "RavenwoodBivalentTest_device", -// android_test { -// name: "RavenwoodBivalentTest_device", -// -// srcs: [ -// "test/**/*.java", -// ], -// static_libs: [ -// "junit", -// "truth", -// -// "androidx.annotation_annotation", -// "androidx.test.ext.junit", -// "androidx.test.rules", -// -// "junit-params", -// "platform-parametric-runner-lib", -// -// "ravenwood-junit", -// ], -// jni_libs: [ -// "libravenwoodbivalenttest_jni", -// ], -// test_suites: [ -// "device-tests", -// ], -// optimize: { -// enabled: false, -// }, -// } + srcs: [ + "test/**/*.java", + ], + // TODO(b/371215487): migrate bivalenttest.ravenizer tests to another architecture + exclude_srcs: [ + "test/**/ravenizer/*.java", + ], + static_libs: [ + "junit", + "truth", + + "androidx.annotation_annotation", + "androidx.test.ext.junit", + "androidx.test.rules", + + "junit-params", + "platform-parametric-runner-lib", + + "ravenwood-junit", + ], + jni_libs: [ + "libravenwoodbivalenttest_jni", + ], + test_suites: [ + "device-tests", + ], + optimize: { + enabled: false, + }, +} diff --git a/services/Android.bp b/services/Android.bp index 653cd3c3b680..f04c692c12d0 100644 --- a/services/Android.bp +++ b/services/Android.bp @@ -195,7 +195,17 @@ soong_config_module_type { module_type: "java_library", config_namespace: "system_services", bool_variables: ["without_vibrator"], - properties: ["vintf_fragments"], + properties: ["vintf_fragment_modules"], +} + +vintf_fragment { + name: "manifest_services.xml", + src: "manifest_services.xml", +} + +vintf_fragment { + name: "manifest_services_android.frameworks.vibrator.xml", + src: "manifest_services_android.frameworks.vibrator.xml", } system_java_library { @@ -264,11 +274,11 @@ system_java_library { soong_config_variables: { without_vibrator: { - vintf_fragments: [ + vintf_fragment_modules: [ "manifest_services.xml", ], conditions_default: { - vintf_fragments: [ + vintf_fragment_modules: [ "manifest_services.xml", "manifest_services_android.frameworks.vibrator.xml", ], diff --git a/services/accessibility/accessibility.aconfig b/services/accessibility/accessibility.aconfig index 034127c0420e..7057cc361a1a 100644 --- a/services/accessibility/accessibility.aconfig +++ b/services/accessibility/accessibility.aconfig @@ -45,6 +45,16 @@ flag { } flag { + name: "clear_shortcuts_when_activity_updates_to_service" + namespace: "accessibility" + description: "When an a11y activity is updated to an a11y service, clears the associated shortcuts so that we don't skip the AccessibilityServiceWarning." + bug: "358092445" + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { name: "compute_window_changes_on_a11y_v2" namespace: "accessibility" description: "Computes accessibility window changes in accessibility instead of wm package." @@ -114,10 +124,13 @@ flag { } flag { - name: "enable_magnification_follows_mouse" + name: "enable_magnification_follows_mouse_bugfix" namespace: "accessibility" description: "Whether to enable mouse following for fullscreen magnification" bug: "354696546" + metadata { + purpose: PURPOSE_BUGFIX + } } flag { diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 1451dfaa7964..ec8908bc7c91 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -2513,6 +2513,19 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub private boolean readInstalledAccessibilityShortcutLocked(AccessibilityUserState userState, List<AccessibilityShortcutInfo> parsedAccessibilityShortcutInfos) { if (!parsedAccessibilityShortcutInfos.equals(userState.mInstalledShortcuts)) { + if (Flags.clearShortcutsWhenActivityUpdatesToService()) { + List<String> componentNames = userState.mInstalledShortcuts.stream() + .filter(a11yActivity -> + !parsedAccessibilityShortcutInfos.contains(a11yActivity)) + .map(a11yActivity -> a11yActivity.getComponentName().flattenToString()) + .toList(); + if (!componentNames.isEmpty()) { + enableShortcutsForTargets( + /* enable= */ false, UserShortcutType.ALL, + componentNames, userState.mUserId); + } + } + userState.mInstalledShortcuts.clear(); userState.mInstalledShortcuts.addAll(parsedAccessibilityShortcutInfos); userState.updateTileServiceMapForAccessibilityActivityLocked(); diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java index a19fdddea49c..963334b07ea6 100644 --- a/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java +++ b/services/accessibility/java/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandler.java @@ -345,7 +345,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH @Override void handleMouseOrStylusEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { - if (Flags.enableMagnificationFollowsMouse()) { + if (Flags.enableMagnificationFollowsMouseBugfix()) { if (mFullScreenMagnificationController.isActivated(mDisplayId)) { // TODO(b/354696546): Allow mouse/stylus to activate whichever display they are // over, rather than only interacting with the current display. @@ -1206,7 +1206,7 @@ public class FullScreenMagnificationGestureHandler extends MagnificationGestureH protected void cacheDelayedMotionEvent(MotionEvent event, MotionEvent rawEvent, int policyFlags) { - if (Flags.enableMagnificationFollowsMouse() + if (Flags.enableMagnificationFollowsMouseBugfix() && !event.isFromSource(SOURCE_TOUCHSCREEN)) { // Only touch events need to be cached and sent later. return; diff --git a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationGestureHandler.java b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationGestureHandler.java index 446123f07f64..fa86ba39bb1a 100644 --- a/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationGestureHandler.java +++ b/services/accessibility/java/com/android/server/accessibility/magnification/MagnificationGestureHandler.java @@ -146,7 +146,8 @@ public abstract class MagnificationGestureHandler extends BaseEventStreamTransfo } break; case SOURCE_MOUSE: case SOURCE_STYLUS: { - if (magnificationShortcutExists() && Flags.enableMagnificationFollowsMouse()) { + if (magnificationShortcutExists() + && Flags.enableMagnificationFollowsMouseBugfix()) { handleMouseOrStylusEvent(event, rawEvent, policyFlags); } } diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java index 28e57775523b..89f14b09d397 100644 --- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java +++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java @@ -68,7 +68,10 @@ import com.android.server.SystemService.TargetUser; import java.io.FileDescriptor; import java.io.PrintWriter; +import java.util.Collections; +import java.util.Map; import java.util.Objects; +import java.util.WeakHashMap; import java.util.concurrent.CompletionException; import java.util.concurrent.Executor; @@ -81,7 +84,8 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { private final ServiceHelper mInternalServiceHelper; private final ServiceConfig mServiceConfig; private final Context mContext; - private final Object mLock = new Object(); + private final Map<String, Object> mLocks = new WeakHashMap<>(); + public AppFunctionManagerServiceImpl(@NonNull Context context) { this( @@ -316,9 +320,7 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { THREAD_POOL_EXECUTOR.execute( () -> { try { - // TODO(357551503): Instead of holding a global lock, hold a per-package - // lock. - synchronized (mLock) { + synchronized (getLockForPackage(callingPackage)) { setAppFunctionEnabledInternalLocked( callingPackage, functionIdentifier, userHandle, enabledState); } @@ -346,7 +348,7 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { * process. */ @WorkerThread - @GuardedBy("mLock") + @GuardedBy("getLockForPackage(callingPackage)") private void setAppFunctionEnabledInternalLocked( @NonNull String callingPackage, @NonNull String functionIdentifier, @@ -541,6 +543,26 @@ public class AppFunctionManagerServiceImpl extends IAppFunctionManager.Stub { }); } } + /** + * Retrieves the lock object associated with the given package name. + * + * This method returns the lock object from the {@code mLocks} map if it exists. + * If no lock is found for the given package name, a new lock object is created, + * stored in the map, and returned. + */ + @VisibleForTesting + @NonNull + Object getLockForPackage(String callingPackage) { + // Synchronized the access to mLocks to prevent race condition. + synchronized (mLocks) { + // By using a WeakHashMap, we allow the garbage collector to reclaim memory by removing + // entries associated with unused callingPackage keys. Therefore, we remove the null + // values before getting/computing a new value. The goal is to not let the size of this + // map grow without an upper bound. + mLocks.values().removeAll(Collections.singleton(null)); // Remove null values + return mLocks.computeIfAbsent(callingPackage, k -> new Object()); + } + } private static class AppFunctionMetadataObserver implements ObserverCallback { @Nullable private final MetadataSyncAdapter mPerUserMetadataSyncAdapter; diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java index 7f1d912d9a79..746c55f8fc9d 100644 --- a/services/core/java/com/android/server/am/ActivityManagerService.java +++ b/services/core/java/com/android/server/am/ActivityManagerService.java @@ -5542,7 +5542,6 @@ public class ActivityManagerService extends IActivityManager.Stub public int sendIntentSender(IApplicationThread caller, IIntentSender target, IBinder allowlistToken, int code, Intent intent, String resolvedType, IIntentReceiver finishedReceiver, String requiredPermission, Bundle options) { - addCreatorToken(intent); if (target instanceof PendingIntentRecord) { final PendingIntentRecord originalRecord = (PendingIntentRecord) target; @@ -5584,19 +5583,23 @@ public class ActivityManagerService extends IActivityManager.Stub intent = new Intent(Intent.ACTION_MAIN); } try { + final int callingUid = Binder.getCallingUid(); + final String packageName; + final long token = Binder.clearCallingIdentity(); + try { + packageName = AppGlobals.getPackageManager().getNameForUid(callingUid); + } finally { + Binder.restoreCallingIdentity(token); + } + if (allowlistToken != null) { - final int callingUid = Binder.getCallingUid(); - final String packageName; - final long token = Binder.clearCallingIdentity(); - try { - packageName = AppGlobals.getPackageManager().getNameForUid(callingUid); - } finally { - Binder.restoreCallingIdentity(token); - } Slog.wtf(TAG, "Send a non-null allowlistToken to a non-PI target." + " Calling package: " + packageName + "; intent: " + intent + "; options: " + options); } + + addCreatorToken(intent, packageName); + target.send(code, intent, resolvedType, null, null, requiredPermission, options); } catch (RemoteException e) { @@ -12371,7 +12374,7 @@ public class ActivityManagerService extends IActivityManager.Stub continue; } endTime = SystemClock.currentThreadTimeMillis(); - hasSwapPss = mi.hasSwappedOutPss; + hasSwapPss = hasSwapPss || mi.hasSwappedOutPss; memtrackGraphics = mi.getOtherPrivate(Debug.MemoryInfo.OTHER_GRAPHICS); memtrackGl = mi.getOtherPrivate(Debug.MemoryInfo.OTHER_GL); } else { @@ -13049,7 +13052,7 @@ public class ActivityManagerService extends IActivityManager.Stub continue; } endTime = SystemClock.currentThreadTimeMillis(); - hasSwapPss = mi.hasSwappedOutPss; + hasSwapPss = hasSwapPss || mi.hasSwappedOutPss; } else { reportType = ProcessStats.ADD_PSS_EXTERNAL; startTime = SystemClock.currentThreadTimeMillis(); @@ -13628,7 +13631,7 @@ public class ActivityManagerService extends IActivityManager.Stub throws TransactionTooLargeException { enforceNotIsolatedCaller("startService"); enforceAllowedToStartOrBindServiceIfSdkSandbox(service); - addCreatorToken(service); + addCreatorToken(service, callingPackage); if (service != null) { // Refuse possible leaked file descriptors if (service.hasFileDescriptors()) { @@ -13890,7 +13893,7 @@ public class ActivityManagerService extends IActivityManager.Stub validateServiceInstanceName(instanceName); - addCreatorToken(service); + addCreatorToken(service, callingPackage); try { if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) { final ComponentName cn = service.getComponent(); @@ -17174,7 +17177,7 @@ public class ActivityManagerService extends IActivityManager.Stub Slog.v(TAG_SERVICE, "startServiceInPackage: " + service + " type=" + resolvedType); } - addCreatorToken(service); + addCreatorToken(service, callingPackage); final long origId = Binder.clearCallingIdentity(); ComponentName res; try { @@ -18002,8 +18005,8 @@ public class ActivityManagerService extends IActivityManager.Stub } @Override - public void addCreatorToken(Intent intent) { - ActivityManagerService.this.addCreatorToken(intent); + public void addCreatorToken(Intent intent, String creatorPackage) { + ActivityManagerService.this.addCreatorToken(intent, creatorPackage); } } @@ -19160,9 +19163,9 @@ public class ActivityManagerService extends IActivityManager.Stub private final Key mKeyFields; private final WeakReference<IntentCreatorToken> mRef; - public IntentCreatorToken(int creatorUid, Intent intent) { + public IntentCreatorToken(int creatorUid, String creatorPackage, Intent intent) { super(); - this.mKeyFields = new Key(creatorUid, intent); + this.mKeyFields = new Key(creatorUid, creatorPackage, intent); mRef = new WeakReference<>(this); } @@ -19170,7 +19173,10 @@ public class ActivityManagerService extends IActivityManager.Stub return mKeyFields.mCreatorUid; } - /** {@hide} */ + public String getCreatorPackage() { + return mKeyFields.mCreatorPackage; + } + public static boolean isValid(@NonNull Intent intent) { IBinder binder = intent.getCreatorToken(); IntentCreatorToken token = null; @@ -19178,7 +19184,8 @@ public class ActivityManagerService extends IActivityManager.Stub token = (IntentCreatorToken) binder; } return token != null && token.mKeyFields.equals( - new Key(token.mKeyFields.mCreatorUid, intent)); + new Key(token.mKeyFields.mCreatorUid, token.mKeyFields.mCreatorPackage, + intent)); } @Override @@ -19202,8 +19209,9 @@ public class ActivityManagerService extends IActivityManager.Stub } private static class Key { - private Key(int creatorUid, Intent intent) { + private Key(int creatorUid, String creatorPackage, Intent intent) { this.mCreatorUid = creatorUid; + this.mCreatorPackage = creatorPackage; this.mAction = intent.getAction(); this.mData = intent.getData(); this.mType = intent.getType(); @@ -19220,6 +19228,7 @@ public class ActivityManagerService extends IActivityManager.Stub } private final int mCreatorUid; + private final String mCreatorPackage; private final String mAction; private final Uri mData; private final String mType; @@ -19233,17 +19242,20 @@ public class ActivityManagerService extends IActivityManager.Stub if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; Key key = (Key) o; - return mCreatorUid == key.mCreatorUid && mFlags == key.mFlags && Objects.equals( - mAction, key.mAction) && Objects.equals(mData, key.mData) - && Objects.equals(mType, key.mType) && Objects.equals(mPackage, - key.mPackage) && Objects.equals(mComponent, key.mComponent) + return mCreatorUid == key.mCreatorUid && mFlags == key.mFlags + && Objects.equals(mCreatorPackage, key.mCreatorPackage) + && Objects.equals(mAction, key.mAction) + && Objects.equals(mData, key.mData) + && Objects.equals(mType, key.mType) + && Objects.equals(mPackage, key.mPackage) + && Objects.equals(mComponent, key.mComponent) && Objects.equals(mClipDataUris, key.mClipDataUris); } @Override public int hashCode() { - return Objects.hash(mCreatorUid, mAction, mData, mType, mPackage, mComponent, - mFlags, mClipDataUris); + return Objects.hash(mCreatorUid, mCreatorPackage, mAction, mData, mType, mPackage, + mComponent, mFlags, mClipDataUris); } } } @@ -19254,7 +19266,7 @@ public class ActivityManagerService extends IActivityManager.Stub * @param intent The given intent * @hide */ - public void addCreatorToken(@Nullable Intent intent) { + public void addCreatorToken(@Nullable Intent intent, String creatorPackage) { if (!preventIntentRedirect()) return; if (intent == null || intent.getExtraIntentKeys() == null) return; @@ -19267,7 +19279,7 @@ public class ActivityManagerService extends IActivityManager.Stub continue; } Slog.wtf(TAG, "A creator token is added to an intent."); - IBinder creatorToken = createIntentCreatorToken(extraIntent); + IBinder creatorToken = createIntentCreatorToken(extraIntent, creatorPackage); if (creatorToken != null) { extraIntent.setCreatorToken(creatorToken); } @@ -19280,15 +19292,15 @@ public class ActivityManagerService extends IActivityManager.Stub } } - private IBinder createIntentCreatorToken(Intent intent) { + private IBinder createIntentCreatorToken(Intent intent, String creatorPackage) { if (IntentCreatorToken.isValid(intent)) return null; int creatorUid = getCallingUid(); - IntentCreatorToken.Key key = new IntentCreatorToken.Key(creatorUid, intent); + IntentCreatorToken.Key key = new IntentCreatorToken.Key(creatorUid, creatorPackage, intent); IntentCreatorToken token; synchronized (sIntentCreatorTokenCache) { WeakReference<IntentCreatorToken> ref = sIntentCreatorTokenCache.get(key); if (ref == null || ref.get() == null) { - token = new IntentCreatorToken(creatorUid, intent); + token = new IntentCreatorToken(creatorUid, creatorPackage, intent); sIntentCreatorTokenCache.put(key, token.mRef); } else { token = ref.get(); diff --git a/services/core/java/com/android/server/am/BroadcastController.java b/services/core/java/com/android/server/am/BroadcastController.java index 15f1085b7125..a00cac6aba4f 100644 --- a/services/core/java/com/android/server/am/BroadcastController.java +++ b/services/core/java/com/android/server/am/BroadcastController.java @@ -258,6 +258,7 @@ class BroadcastController { final StringBuilder sb = new StringBuilder("registerReceiver: "); sb.append(Binder.getCallingUid()); sb.append('/'); sb.append(receiverId == null ? "null" : receiverId); sb.append('/'); + sb.append("p:"); sb.append(filter.getPriority()); sb.append('/'); final int actionsCount = filter.safeCountActions(); if (actionsCount > 0) { for (int i = 0; i < actionsCount; ++i) { diff --git a/services/core/java/com/android/server/am/OomAdjuster.java b/services/core/java/com/android/server/am/OomAdjuster.java index 776a3455acc4..f60ee66cb236 100644 --- a/services/core/java/com/android/server/am/OomAdjuster.java +++ b/services/core/java/com/android/server/am/OomAdjuster.java @@ -3283,7 +3283,12 @@ public class OomAdjuster { baseCapabilities = PROCESS_CAPABILITY_ALL; // BFSL allowed break; case PROCESS_STATE_BOUND_TOP: - baseCapabilities = PROCESS_CAPABILITY_BFSL; + if (app.getActiveInstrumentation() != null) { + baseCapabilities = PROCESS_CAPABILITY_BFSL | + PROCESS_CAPABILITY_ALL_IMPLICIT; + } else { + baseCapabilities = PROCESS_CAPABILITY_BFSL; + } break; case PROCESS_STATE_FOREGROUND_SERVICE: if (app.getActiveInstrumentation() != null) { diff --git a/services/core/java/com/android/server/am/PendingIntentRecord.java b/services/core/java/com/android/server/am/PendingIntentRecord.java index 6857b6bcde15..3fb06a75d79f 100644 --- a/services/core/java/com/android/server/am/PendingIntentRecord.java +++ b/services/core/java/com/android/server/am/PendingIntentRecord.java @@ -432,6 +432,14 @@ public final class PendingIntentRecord extends IIntentSender.Stub { } } + /** + * get package name of the PendingIntent sender. + * @return package name of the PendingIntent sender. + */ + public String getPackageName() { + return key.packageName; + } + @Deprecated public int sendInner(int code, Intent intent, String resolvedType, IBinder allowlistToken, IIntentReceiver finishedReceiver, String requiredPermission, IBinder resultTo, diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java index 7831c393844b..cdb01889c139 100644 --- a/services/core/java/com/android/server/am/ProcessList.java +++ b/services/core/java/com/android/server/am/ProcessList.java @@ -5187,6 +5187,7 @@ public final class ProcessList { if (ai != null) { if (ai.packageName.equals(app.info.packageName)) { app.info = ai; + app.getWindowProcessController().updateApplicationInfo(ai); PlatformCompatCache.getInstance() .onApplicationInfoChanged(ai); } diff --git a/services/core/java/com/android/server/integrity/AppIntegrityManagerServiceImpl.java b/services/core/java/com/android/server/integrity/AppIntegrityManagerServiceImpl.java index 636854b85ee4..d1576c5cca4f 100644 --- a/services/core/java/com/android/server/integrity/AppIntegrityManagerServiceImpl.java +++ b/services/core/java/com/android/server/integrity/AppIntegrityManagerServiceImpl.java @@ -17,118 +17,63 @@ package com.android.server.integrity; import static android.content.Intent.ACTION_PACKAGE_NEEDS_INTEGRITY_VERIFICATION; -import static android.content.Intent.EXTRA_LONG_VERSION_CODE; -import static android.content.Intent.EXTRA_ORIGINATING_UID; -import static android.content.Intent.EXTRA_PACKAGE_NAME; import static android.content.integrity.AppIntegrityManager.EXTRA_STATUS; import static android.content.integrity.AppIntegrityManager.STATUS_FAILURE; import static android.content.integrity.AppIntegrityManager.STATUS_SUCCESS; -import static android.content.integrity.InstallerAllowedByManifestFormula.INSTALLER_CERTIFICATE_NOT_EVALUATED; import static android.content.integrity.IntegrityUtils.getHexDigest; import static android.content.pm.PackageManager.EXTRA_VERIFICATION_ID; import android.annotation.BinderThread; -import android.annotation.NonNull; -import android.annotation.Nullable; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.IntentSender; -import android.content.integrity.AppInstallMetadata; import android.content.integrity.IAppIntegrityManager; import android.content.integrity.Rule; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; import android.content.pm.ParceledListSlice; -import android.content.pm.Signature; -import android.content.pm.SigningDetails; -import android.content.pm.parsing.result.ParseResult; -import android.content.pm.parsing.result.ParseTypeImpl; import android.net.Uri; import android.os.Binder; -import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; import android.provider.Settings; import android.util.Pair; import android.util.Slog; -import android.util.apk.SourceStampVerificationResult; -import android.util.apk.SourceStampVerifier; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; -import com.android.internal.pm.parsing.PackageParser2; -import com.android.internal.pm.pkg.parsing.ParsingPackageUtils; -import com.android.internal.util.ArrayUtils; import com.android.server.LocalServices; -import com.android.server.integrity.model.IntegrityCheckResult; import com.android.server.integrity.model.RuleMetadata; -import com.android.server.pm.PackageManagerServiceUtils; -import com.android.server.pm.parsing.PackageParserUtils; -import java.io.ByteArrayInputStream; import java.io.File; -import java.io.IOException; -import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.nio.file.Files; -import java.nio.file.Path; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateEncodingException; -import java.security.cert.CertificateException; -import java.security.cert.CertificateFactory; -import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.function.Supplier; -import java.util.stream.Collectors; import java.util.stream.Stream; /** Implementation of {@link AppIntegrityManagerService}. */ public class AppIntegrityManagerServiceImpl extends IAppIntegrityManager.Stub { - /** - * This string will be used as the "installer" for formula evaluation when the app's installer - * cannot be determined. - * - * <p>This may happen for various reasons. e.g., the installing app's package name may not match - * its UID. - */ - private static final String UNKNOWN_INSTALLER = ""; - /** - * This string will be used as the "installer" for formula evaluation when the app is being - * installed via ADB. - */ - public static final String ADB_INSTALLER = "adb"; private static final String TAG = "AppIntegrityManagerServiceImpl"; private static final String PACKAGE_MIME_TYPE = "application/vnd.android.package-archive"; - private static final String BASE_APK_FILE = "base.apk"; - private static final String ALLOWED_INSTALLERS_METADATA_NAME = "allowed-installers"; - private static final String ALLOWED_INSTALLER_DELIMITER = ","; - private static final String INSTALLER_PACKAGE_CERT_DELIMITER = "\\|"; public static final boolean DEBUG_INTEGRITY_COMPONENT = false; - private static final Set<String> PACKAGE_INSTALLER = - new HashSet<>( - Arrays.asList( - "com.google.android.packageinstaller", "com.android.packageinstaller")); - // Access to files inside mRulesDir is protected by mRulesLock; private final Context mContext; private final Handler mHandler; private final PackageManagerInternal mPackageManagerInternal; - private final Supplier<PackageParser2> mParserSupplier; private final IntegrityFileManager mIntegrityFileManager; /** Create an instance of {@link AppIntegrityManagerServiceImpl}. */ @@ -139,7 +84,6 @@ public class AppIntegrityManagerServiceImpl extends IAppIntegrityManager.Stub { return new AppIntegrityManagerServiceImpl( context, LocalServices.getService(PackageManagerInternal.class), - PackageParserUtils::forParsingFileWithDefaults, IntegrityFileManager.getInstance(), handlerThread.getThreadHandler()); } @@ -148,12 +92,10 @@ public class AppIntegrityManagerServiceImpl extends IAppIntegrityManager.Stub { AppIntegrityManagerServiceImpl( Context context, PackageManagerInternal packageManagerInternal, - Supplier<PackageParser2> parserSupplier, IntegrityFileManager integrityFileManager, Handler handler) { mContext = context; mPackageManagerInternal = packageManagerInternal; - mParserSupplier = parserSupplier; mIntegrityFileManager = integrityFileManager; mHandler = handler; @@ -263,148 +205,8 @@ public class AppIntegrityManagerServiceImpl extends IAppIntegrityManager.Stub { private void handleIntegrityVerification(Intent intent) { int verificationId = intent.getIntExtra(EXTRA_VERIFICATION_ID, -1); - - try { - if (DEBUG_INTEGRITY_COMPONENT) { - Slog.d(TAG, "Received integrity verification intent " + intent.toString()); - Slog.d(TAG, "Extras " + intent.getExtras()); - } - - String installerPackageName = getInstallerPackageName(intent); - - // Skip integrity verification if the verifier is doing the install. - if (!integrityCheckIncludesRuleProvider() && isRuleProvider(installerPackageName)) { - if (DEBUG_INTEGRITY_COMPONENT) { - Slog.i(TAG, "Verifier doing the install. Skipping integrity check."); - } - mPackageManagerInternal.setIntegrityVerificationResult( - verificationId, PackageManagerInternal.INTEGRITY_VERIFICATION_ALLOW); - return; - } - - String packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME); - - Pair<SigningDetails, Bundle> packageSigningAndMetadata = - getPackageSigningAndMetadata(intent.getData()); - if (packageSigningAndMetadata == null) { - Slog.w(TAG, "Cannot parse package " + packageName); - // We can't parse the package. - mPackageManagerInternal.setIntegrityVerificationResult( - verificationId, PackageManagerInternal.INTEGRITY_VERIFICATION_ALLOW); - return; - } - - var signingDetails = packageSigningAndMetadata.first; - List<String> appCertificates = getCertificateFingerprint(packageName, signingDetails); - List<String> appCertificateLineage = getCertificateLineage(packageName, signingDetails); - List<String> installerCertificates = - getInstallerCertificateFingerprint(installerPackageName); - - AppInstallMetadata.Builder builder = new AppInstallMetadata.Builder(); - - builder.setPackageName(getPackageNameNormalized(packageName)); - builder.setAppCertificates(appCertificates); - builder.setAppCertificateLineage(appCertificateLineage); - builder.setVersionCode(intent.getLongExtra(EXTRA_LONG_VERSION_CODE, -1)); - builder.setInstallerName(getPackageNameNormalized(installerPackageName)); - builder.setInstallerCertificates(installerCertificates); - builder.setIsPreInstalled(isSystemApp(packageName)); - - Map<String, String> allowedInstallers = - getAllowedInstallers(packageSigningAndMetadata.second); - builder.setAllowedInstallersAndCert(allowedInstallers); - extractSourceStamp(intent.getData(), builder); - - AppInstallMetadata appInstallMetadata = builder.build(); - - if (DEBUG_INTEGRITY_COMPONENT) { - Slog.i( - TAG, - "To be verified: " - + appInstallMetadata - + " installers " - + allowedInstallers); - } - IntegrityCheckResult result = IntegrityCheckResult.allow(); - if (!result.getMatchedRules().isEmpty() || DEBUG_INTEGRITY_COMPONENT) { - Slog.i( - TAG, - String.format( - "Integrity check of %s result: %s due to %s", - packageName, result.getEffect(), result.getMatchedRules())); - } - - mPackageManagerInternal.setIntegrityVerificationResult( - verificationId, - result.getEffect() == IntegrityCheckResult.Effect.ALLOW - ? PackageManagerInternal.INTEGRITY_VERIFICATION_ALLOW - : PackageManagerInternal.INTEGRITY_VERIFICATION_REJECT); - } catch (IllegalArgumentException e) { - // This exception indicates something is wrong with the input passed by package manager. - // e.g., someone trying to trick the system. We block installs in this case. - Slog.e(TAG, "Invalid input to integrity verification", e); - mPackageManagerInternal.setIntegrityVerificationResult( - verificationId, PackageManagerInternal.INTEGRITY_VERIFICATION_REJECT); - } catch (Exception e) { - // Other exceptions indicate an error within the integrity component implementation and - // we allow them. - Slog.e(TAG, "Error handling integrity verification", e); - mPackageManagerInternal.setIntegrityVerificationResult( - verificationId, PackageManagerInternal.INTEGRITY_VERIFICATION_ALLOW); - } - } - - /** - * Verify the UID and return the installer package name. - * - * @return the package name of the installer, or null if it cannot be determined or it is - * installed via adb. - */ - @Nullable - private String getInstallerPackageName(Intent intent) { - String installer = - intent.getStringExtra(PackageManager.EXTRA_VERIFICATION_INSTALLER_PACKAGE); - if (PackageManagerServiceUtils.isInstalledByAdb(installer)) { - return ADB_INSTALLER; - } - int installerUid = intent.getIntExtra(PackageManager.EXTRA_VERIFICATION_INSTALLER_UID, -1); - if (installerUid < 0) { - Slog.e( - TAG, - "Installer cannot be determined: installer: " - + installer - + " installer UID: " - + installerUid); - return UNKNOWN_INSTALLER; - } - - // Verify that the installer UID actually contains the package. Note that comparing UIDs - // is not safe since context's uid can change in different settings; e.g. Android Auto. - if (!getPackageListForUid(installerUid).contains(installer)) { - return UNKNOWN_INSTALLER; - } - - // At this time we can trust "installer". - - // A common way for apps to install packages is to send an intent to PackageInstaller. In - // that case, the installer will always show up as PackageInstaller which is not what we - // want. - if (PACKAGE_INSTALLER.contains(installer)) { - int originatingUid = intent.getIntExtra(EXTRA_ORIGINATING_UID, -1); - if (originatingUid < 0) { - Slog.e(TAG, "Installer is package installer but originating UID not found."); - return UNKNOWN_INSTALLER; - } - List<String> installerPackages = getPackageListForUid(originatingUid); - if (installerPackages.isEmpty()) { - Slog.e(TAG, "No package found associated with originating UID " + originatingUid); - return UNKNOWN_INSTALLER; - } - // In the case of multiple package sharing a UID, we just return the first one. - return installerPackages.get(0); - } - - return installer; + mPackageManagerInternal.setIntegrityVerificationResult( + verificationId, PackageManagerInternal.INTEGRITY_VERIFICATION_ALLOW); } /** We will use the SHA256 digest of a package name if it is more than 32 bytes long. */ @@ -422,264 +224,6 @@ public class AppIntegrityManagerServiceImpl extends IAppIntegrityManager.Stub { } } - private List<String> getInstallerCertificateFingerprint(String installer) { - if (installer.equals(ADB_INSTALLER) || installer.equals(UNKNOWN_INSTALLER)) { - return Collections.emptyList(); - } - var installerPkg = mPackageManagerInternal.getPackage(installer); - if (installerPkg == null) { - Slog.w(TAG, "Installer package " + installer + " not found."); - return Collections.emptyList(); - } - return getCertificateFingerprint(installerPkg.getPackageName(), - installerPkg.getSigningDetails()); - } - - private List<String> getCertificateFingerprint(@NonNull String packageName, - @NonNull SigningDetails signingDetails) { - ArrayList<String> certificateFingerprints = new ArrayList(); - for (Signature signature : getSignatures(packageName, signingDetails)) { - certificateFingerprints.add(getFingerprint(signature)); - } - return certificateFingerprints; - } - - private List<String> getCertificateLineage(@NonNull String packageName, - @NonNull SigningDetails signingDetails) { - ArrayList<String> certificateLineage = new ArrayList(); - for (Signature signature : getSignatureLineage(packageName, signingDetails)) { - certificateLineage.add(getFingerprint(signature)); - } - return certificateLineage; - } - - /** Get the allowed installers and their associated certificate hashes from <meta-data> tag. */ - private Map<String, String> getAllowedInstallers(@Nullable Bundle metaData) { - Map<String, String> packageCertMap = new HashMap<>(); - if (metaData != null) { - String allowedInstallers = metaData.getString(ALLOWED_INSTALLERS_METADATA_NAME); - if (allowedInstallers != null) { - // parse the metadata for certs. - String[] installerCertPairs = allowedInstallers.split(ALLOWED_INSTALLER_DELIMITER); - for (String packageCertPair : installerCertPairs) { - String[] packageAndCert = - packageCertPair.split(INSTALLER_PACKAGE_CERT_DELIMITER); - if (packageAndCert.length == 2) { - String packageName = getPackageNameNormalized(packageAndCert[0]); - String cert = packageAndCert[1]; - packageCertMap.put(packageName, cert); - } else if (packageAndCert.length == 1) { - packageCertMap.put( - getPackageNameNormalized(packageAndCert[0]), - INSTALLER_CERTIFICATE_NOT_EVALUATED); - } - } - } - } - - return packageCertMap; - } - - /** Extract the source stamp embedded in the APK, if present. */ - private void extractSourceStamp(Uri dataUri, AppInstallMetadata.Builder appInstallMetadata) { - File installationPath = getInstallationPath(dataUri); - if (installationPath == null) { - throw new IllegalArgumentException("Installation path is null, package not found"); - } - - SourceStampVerificationResult sourceStampVerificationResult; - if (installationPath.isDirectory()) { - try (Stream<Path> filesList = Files.list(installationPath.toPath())) { - List<String> apkFiles = - filesList - .map(path -> path.toAbsolutePath().toString()) - .filter(str -> str.endsWith(".apk")) - .collect(Collectors.toList()); - sourceStampVerificationResult = SourceStampVerifier.verify(apkFiles); - } catch (IOException e) { - throw new IllegalArgumentException("Could not read APK directory"); - } - } else { - sourceStampVerificationResult = - SourceStampVerifier.verify(installationPath.getAbsolutePath()); - } - - appInstallMetadata.setIsStampPresent(sourceStampVerificationResult.isPresent()); - appInstallMetadata.setIsStampVerified(sourceStampVerificationResult.isVerified()); - // A verified stamp is set to be trusted. - appInstallMetadata.setIsStampTrusted(sourceStampVerificationResult.isVerified()); - if (sourceStampVerificationResult.isVerified()) { - X509Certificate sourceStampCertificate = - (X509Certificate) sourceStampVerificationResult.getCertificate(); - // Sets source stamp certificate digest. - try { - MessageDigest digest = MessageDigest.getInstance("SHA-256"); - byte[] certificateDigest = digest.digest(sourceStampCertificate.getEncoded()); - appInstallMetadata.setStampCertificateHash(getHexDigest(certificateDigest)); - } catch (NoSuchAlgorithmException | CertificateEncodingException e) { - throw new IllegalArgumentException( - "Error computing source stamp certificate digest", e); - } - } - } - - private static Signature[] getSignatures(@NonNull String packageName, - @NonNull SigningDetails signingDetails) { - Signature[] signatures = signingDetails.getSignatures(); - if (signatures == null || signatures.length < 1) { - throw new IllegalArgumentException("Package signature not found in " + packageName); - } - - // We are only interested in evaluating the active signatures. - return signatures; - } - - private static Signature[] getSignatureLineage(@NonNull String packageName, - @NonNull SigningDetails signingDetails) { - // Obtain the active signatures of the package. - Signature[] signatureLineage = getSignatures(packageName, signingDetails); - - var pastSignatures = signingDetails.getPastSigningCertificates(); - // Obtain the past signatures of the package. - if (signatureLineage.length == 1 && !ArrayUtils.isEmpty(pastSignatures)) { - // Merge the signatures and return. - Signature[] allSignatures = - new Signature[signatureLineage.length + pastSignatures.length]; - int i; - for (i = 0; i < signatureLineage.length; i++) { - allSignatures[i] = signatureLineage[i]; - } - for (int j = 0; j < pastSignatures.length; j++) { - allSignatures[i] = pastSignatures[j]; - i++; - } - signatureLineage = allSignatures; - } - - return signatureLineage; - } - - private static String getFingerprint(Signature cert) { - InputStream input = new ByteArrayInputStream(cert.toByteArray()); - - CertificateFactory factory; - try { - factory = CertificateFactory.getInstance("X509"); - } catch (CertificateException e) { - throw new RuntimeException("Error getting CertificateFactory", e); - } - X509Certificate certificate = null; - try { - if (factory != null) { - certificate = (X509Certificate) factory.generateCertificate(input); - } - } catch (CertificateException e) { - throw new RuntimeException("Error getting X509Certificate", e); - } - - if (certificate == null) { - throw new RuntimeException("X509 Certificate not found"); - } - - try { - MessageDigest digest = MessageDigest.getInstance("SHA-256"); - byte[] publicKey = digest.digest(certificate.getEncoded()); - return getHexDigest(publicKey); - } catch (NoSuchAlgorithmException | CertificateEncodingException e) { - throw new IllegalArgumentException("Error error computing fingerprint", e); - } - } - - @Nullable - private Pair<SigningDetails, Bundle> getPackageSigningAndMetadata(Uri dataUri) { - File installationPath = getInstallationPath(dataUri); - if (installationPath == null) { - throw new IllegalArgumentException("Installation path is null, package not found"); - } - - try (PackageParser2 parser = mParserSupplier.get()) { - var pkg = parser.parsePackage(installationPath, 0, false); - // APK signatures is already verified elsewhere in PackageManager. We do not need to - // verify it again since it could cause a timeout for large APKs. - final ParseTypeImpl input = ParseTypeImpl.forDefaultParsing(); - final ParseResult<SigningDetails> result = ParsingPackageUtils.getSigningDetails( - input, pkg, /* skipVerify= */ true); - if (result.isError()) { - Slog.w(TAG, result.getErrorMessage(), result.getException()); - return null; - } - return Pair.create(result.getResult(), pkg.getMetaData()); - } catch (Exception e) { - Slog.w(TAG, "Exception reading " + dataUri, e); - return null; - } - } - - private PackageInfo getMultiApkInfo(File multiApkDirectory) { - // The base apk will normally be called base.apk - File baseFile = new File(multiApkDirectory, BASE_APK_FILE); - PackageInfo basePackageInfo = - mContext.getPackageManager() - .getPackageArchiveInfo( - baseFile.getAbsolutePath(), - PackageManager.GET_SIGNING_CERTIFICATES - | PackageManager.GET_META_DATA); - - if (basePackageInfo == null) { - for (File apkFile : multiApkDirectory.listFiles()) { - if (apkFile.isDirectory()) { - continue; - } - - // If we didn't find a base.apk, then try to parse each apk until we find the one - // that succeeds. - try { - basePackageInfo = - mContext.getPackageManager() - .getPackageArchiveInfo( - apkFile.getAbsolutePath(), - PackageManager.GET_SIGNING_CERTIFICATES - | PackageManager.GET_META_DATA); - } catch (Exception e) { - // Some of the splits may not contain a valid android manifest. It is an - // expected exception. We still log it nonetheless but we should keep looking. - Slog.w(TAG, "Exception reading " + apkFile, e); - } - if (basePackageInfo != null) { - Slog.i(TAG, "Found package info from " + apkFile); - break; - } - } - } - - if (basePackageInfo == null) { - throw new IllegalArgumentException( - "Base package info cannot be found from installation directory"); - } - - return basePackageInfo; - } - - private File getInstallationPath(Uri dataUri) { - if (dataUri == null) { - throw new IllegalArgumentException("Null data uri"); - } - - String scheme = dataUri.getScheme(); - if (!"file".equalsIgnoreCase(scheme)) { - throw new IllegalArgumentException("Unsupported scheme for " + dataUri); - } - - File installationPath = new File(dataUri.getPath()); - if (!installationPath.exists()) { - throw new IllegalArgumentException("Cannot find file for " + dataUri); - } - if (!installationPath.canRead()) { - throw new IllegalArgumentException("Cannot read file for " + dataUri); - } - return installationPath; - } - private String getCallerPackageNameOrThrow(int callingUid) { String callerPackageName = getCallingRulePusherPackageName(callingUid); if (callerPackageName == null) { @@ -715,15 +259,6 @@ public class AppIntegrityManagerServiceImpl extends IAppIntegrityManager.Stub { return allowedCallingPackages.isEmpty() ? null : allowedCallingPackages.get(0); } - private boolean isRuleProvider(String installerPackageName) { - for (String ruleProvider : getAllowedRuleProviderSystemApps()) { - if (ruleProvider.matches(installerPackageName)) { - return true; - } - } - return false; - } - private List<String> getAllowedRuleProviderSystemApps() { List<String> integrityRuleProviders = Arrays.asList( @@ -751,14 +286,6 @@ public class AppIntegrityManagerServiceImpl extends IAppIntegrityManager.Stub { } } - private boolean integrityCheckIncludesRuleProvider() { - return Settings.Global.getInt( - mContext.getContentResolver(), - Settings.Global.INTEGRITY_CHECK_INCLUDES_RULE_PROVIDER, - 0) - == 1; - } - private List<String> getPackageListForUid(int uid) { try { return Arrays.asList(mContext.getPackageManager().getPackagesForUid(uid)); diff --git a/services/core/java/com/android/server/notification/flags.aconfig b/services/core/java/com/android/server/notification/flags.aconfig index a24c743929b5..f79d9ef174ea 100644 --- a/services/core/java/com/android/server/notification/flags.aconfig +++ b/services/core/java/com/android/server/notification/flags.aconfig @@ -165,6 +165,13 @@ flag { } flag { + name: "notification_lock_screen_settings" + namespace: "systemui" + description: "This flag enables the new settings page for the notifications on lock screen." + bug: "367455695" +} + +flag { name: "notification_vibration_in_sound_uri" namespace: "systemui" description: "This flag enables sound uri with vibration source" diff --git a/services/core/java/com/android/server/pm/BroadcastHelper.java b/services/core/java/com/android/server/pm/BroadcastHelper.java index 369029adac59..84a5f2b0e8bc 100644 --- a/services/core/java/com/android/server/pm/BroadcastHelper.java +++ b/services/core/java/com/android/server/pm/BroadcastHelper.java @@ -80,11 +80,6 @@ import java.util.function.BiFunction; */ public final class BroadcastHelper { private static final boolean DEBUG_BROADCASTS = false; - /** - * Permissions required in order to receive instant application lifecycle broadcasts. - */ - private static final String[] INSTANT_APP_BROADCAST_PERMISSION = - new String[]{android.Manifest.permission.ACCESS_INSTANT_APPS}; private final UserManagerInternal mUmInternal; private final ActivityManagerInternal mAmInternal; @@ -115,7 +110,7 @@ public final class BroadcastHelper { SparseArray<int[]> broadcastAllowList = new SparseArray<>(); broadcastAllowList.put(userId, visibilityAllowList); broadcastIntent(intent, finishedReceiver, isInstantApp, userId, broadcastAllowList, - filterExtrasForReceiver, bOptions); + filterExtrasForReceiver, bOptions, null /* requiredPermissions */); } void sendPackageBroadcast(final String action, final String pkg, final Bundle extras, @@ -123,7 +118,7 @@ public final class BroadcastHelper { final int[] userIds, int[] instantUserIds, @Nullable SparseArray<int[]> broadcastAllowList, @Nullable BiFunction<Integer, Bundle, Bundle> filterExtrasForReceiver, - @Nullable Bundle bOptions) { + @Nullable Bundle bOptions, @Nullable String[] requiredPermissions) { try { final IActivityManager am = ActivityManager.getService(); if (am == null) return; @@ -137,12 +132,12 @@ public final class BroadcastHelper { if (ArrayUtils.isEmpty(instantUserIds)) { doSendBroadcast(action, pkg, extras, flags, targetPkg, finishedReceiver, resolvedUserIds, false /* isInstantApp */, broadcastAllowList, - filterExtrasForReceiver, bOptions); + filterExtrasForReceiver, bOptions, requiredPermissions); } else { // send restricted broadcasts for instant apps doSendBroadcast(action, pkg, extras, flags, targetPkg, finishedReceiver, - instantUserIds, true /* isInstantApp */, null, - null /* filterExtrasForReceiver */, bOptions); + instantUserIds, true /* isInstantApp */, null /* broadcastAllowList */, + null /* filterExtrasForReceiver */, bOptions, requiredPermissions); } } catch (RemoteException ex) { } @@ -166,7 +161,8 @@ public final class BroadcastHelper { boolean isInstantApp, @Nullable SparseArray<int[]> broadcastAllowList, @Nullable BiFunction<Integer, Bundle, Bundle> filterExtrasForReceiver, - @Nullable Bundle bOptions) { + @Nullable Bundle bOptions, + @Nullable String[] requiredPermissions) { for (int userId : userIds) { final Intent intent = new Intent(action, pkg != null ? Uri.fromParts(PACKAGE_SCHEME, pkg, null) : null); @@ -189,17 +185,18 @@ public final class BroadcastHelper { intent.putExtra(Intent.EXTRA_USER_HANDLE, userId); intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT | flags); broadcastIntent(intent, finishedReceiver, isInstantApp, userId, broadcastAllowList, - filterExtrasForReceiver, bOptions); + filterExtrasForReceiver, bOptions, requiredPermissions); } } - private void broadcastIntent(Intent intent, IIntentReceiver finishedReceiver, boolean isInstantApp, int userId, @Nullable SparseArray<int[]> broadcastAllowList, @Nullable BiFunction<Integer, Bundle, Bundle> filterExtrasForReceiver, - @Nullable Bundle bOptions) { - final String[] requiredPermissions = - isInstantApp ? INSTANT_APP_BROADCAST_PERMISSION : null; + @Nullable Bundle bOptions, @Nullable String[] requiredPermissions) { + if (isInstantApp) { + requiredPermissions = ArrayUtils.appendElement(String.class, requiredPermissions, + android.Manifest.permission.ACCESS_INSTANT_APPS); + } if (DEBUG_BROADCASTS) { RuntimeException here = new RuntimeException("here"); here.fillInStackTrace(); @@ -234,7 +231,7 @@ public final class BroadcastHelper { null /* instantUserIds */, null /* broadcastAllowList */, (callingUid, intentExtras) -> filterExtrasChangedPackageList( snapshot, callingUid, intentExtras), - null /* bOptions */); + null /* bOptions */, null /* requiredPermissions */); } /** @@ -294,14 +291,29 @@ public final class BroadcastHelper { return bOptions; } - private void sendPackageChangedBroadcast(@NonNull String packageName, - boolean dontKillApp, - @NonNull ArrayList<String> componentNames, - int packageUid, - @Nullable String reason, - @Nullable int[] userIds, - @Nullable int[] instantUserIds, - @Nullable SparseArray<int[]> broadcastAllowList) { + private void sendPackageChangedBroadcastInternal(@NonNull String packageName, + boolean dontKillApp, + @NonNull ArrayList<String> componentNames, + int packageUid, + @Nullable String reason, + @Nullable int[] userIds, + @Nullable int[] instantUserIds, + @Nullable SparseArray<int[]> broadcastAllowList) { + sendPackageChangedBroadcastWithPermissions(packageName, dontKillApp, componentNames, + packageUid, reason, userIds, instantUserIds, broadcastAllowList, + null /* targetPackageName */, null /* requiredPermissions */); + } + + private void sendPackageChangedBroadcastWithPermissions(@NonNull String packageName, + boolean dontKillApp, + @NonNull ArrayList<String> componentNames, + int packageUid, + @Nullable String reason, + @Nullable int[] userIds, + @Nullable int[] instantUserIds, + @Nullable SparseArray<int[]> broadcastAllowList, + @Nullable String targetPackageName, + @Nullable String[] requiredPermissions) { if (DEBUG_INSTALL) { Log.v(TAG, "Sending package changed: package=" + packageName + " components=" + componentNames); @@ -321,9 +333,10 @@ public final class BroadcastHelper { // little component state change. final int flags = !componentNames.contains(packageName) ? Intent.FLAG_RECEIVER_REGISTERED_ONLY : 0; - sendPackageBroadcast(Intent.ACTION_PACKAGE_CHANGED, packageName, extras, flags, null, null, - userIds, instantUserIds, broadcastAllowList, null /* filterExtrasForReceiver */, - null /* bOptions */); + sendPackageBroadcast(Intent.ACTION_PACKAGE_CHANGED, packageName, extras, flags, + targetPackageName, null /* finishedReceiver */, userIds, instantUserIds, + broadcastAllowList, null /* filterExtrasForReceiver */, null /* bOptions */, + requiredPermissions); } static void sendDeviceCustomizationReadyBroadcast() { @@ -680,7 +693,8 @@ public final class BroadcastHelper { sendPackageBroadcast(Intent.ACTION_PACKAGE_ADDED, packageName, extras, 0, null, null, userIds, instantUserIds, - broadcastAllowlist, null /* filterExtrasForReceiver */, null); + broadcastAllowlist, null /* filterExtrasForReceiver */, null /* bOptions */, + null /* requiredPermissions */); // Send to PermissionController for all new users, even if it may not be running for some // users if (isPrivacySafetyLabelChangeNotificationsEnabled(mContext)) { @@ -688,7 +702,8 @@ public final class BroadcastHelper { packageName, extras, 0, mContext.getPackageManager().getPermissionControllerPackageName(), null, userIds, instantUserIds, - broadcastAllowlist, null /* filterExtrasForReceiver */, null); + broadcastAllowlist, null /* filterExtrasForReceiver */, null /* bOptions */, + null /* requiredPermissions */); } } @@ -719,7 +734,8 @@ public final class BroadcastHelper { int[] userIds, int[] instantUserIds) { sendPackageBroadcast(Intent.ACTION_PACKAGE_FIRST_LAUNCH, pkgName, null, 0, installerPkg, null, userIds, instantUserIds, null /* broadcastAllowList */, - null /* filterExtrasForReceiver */, null); + null /* filterExtrasForReceiver */, null /* bOptions */, + null /* requiredPermissions */); } /** @@ -824,7 +840,7 @@ public final class BroadcastHelper { final int[] instantUserIds = isInstantApp ? new int[] { userId } : EMPTY_INT_ARRAY; final SparseArray<int[]> broadcastAllowList = isInstantApp ? null : snapshot.getVisibilityAllowLists(packageName, userIds); - mHandler.post(() -> sendPackageChangedBroadcast( + mHandler.post(() -> sendPackageChangedBroadcastInternal( packageName, dontKillApp, componentNames, packageUid, reason, userIds, instantUserIds, broadcastAllowList)); mPackageMonitorCallbackHelper.notifyPackageChanged(packageName, dontKillApp, componentNames, @@ -843,7 +859,7 @@ public final class BroadcastHelper { @Nullable Bundle bOptions) { mHandler.post(() -> sendPackageBroadcast(action, pkg, extras, flags, targetPkg, finishedReceiver, userIds, instantUserIds, broadcastAllowList, - null /* filterExtrasForReceiver */, bOptions)); + null /* filterExtrasForReceiver */, bOptions, null /* requiredPermissions */)); if (targetPkg == null) { // For some broadcast action, e.g. ACTION_PACKAGE_ADDED, this method will be called // many times to different targets, e.g. installer app, permission controller, other @@ -1014,7 +1030,7 @@ public final class BroadcastHelper { extras, flags, null /* targetPkg */, null /* finishedReceiver */, new int[]{userId}, null /* instantUserIds */, null /* broadcastAllowList */, filterExtrasForReceiver, - options)); + options, null /* requiredPermissions */)); notifyPackageMonitor(intent, null /* pkg */, extras, new int[]{userId}, null /* instantUserIds */, null /* broadcastAllowList */, filterExtrasForReceiver); } @@ -1046,9 +1062,12 @@ public final class BroadcastHelper { } else { intentExtras = null; } - doSendBroadcast(action, null, intentExtras, - Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND, packageName, null, - targetUserIds, false, null, null, null); + doSendBroadcast(action, null /* pkg */, intentExtras, + Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND, packageName, + null /* finishedReceiver */, + targetUserIds, false /* isInstantApp */, null /* broadcastAllowList */, + null /* filterExtrasForReceiver */, null /* bOptions */, + null /* requiredPermissions */); } }); } @@ -1077,7 +1096,7 @@ public final class BroadcastHelper { null /* broadcastAllowList */, (callingUid, intentExtras) -> filterExtrasChangedPackageList( snapshot, callingUid, intentExtras), - null /* bOptions */)); + null /* bOptions */, null /* requiredPermissions */)); } void sendResourcesChangedBroadcastAndNotify(@NonNull Computer snapshot, diff --git a/services/core/java/com/android/server/pm/PackageInstallerService.java b/services/core/java/com/android/server/pm/PackageInstallerService.java index 34d939b07187..f6a808b6c33e 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerService.java +++ b/services/core/java/com/android/server/pm/PackageInstallerService.java @@ -25,12 +25,14 @@ import static android.content.pm.PackageInstaller.UNARCHIVAL_ERROR_NO_CONNECTIVI import static android.content.pm.PackageInstaller.UNARCHIVAL_ERROR_USER_ACTION_NEEDED; import static android.content.pm.PackageInstaller.UNARCHIVAL_GENERIC_ERROR; import static android.content.pm.PackageInstaller.UNARCHIVAL_OK; +import static android.content.pm.PackageInstaller.VERIFICATION_POLICY_BLOCK_FAIL_WARN; import static android.content.pm.PackageManager.DELETE_ARCHIVE; import static android.content.pm.PackageManager.INSTALL_UNARCHIVE_DRAFT; import static android.os.Process.INVALID_UID; import static android.os.Process.SYSTEM_UID; import static com.android.server.pm.PackageArchiver.isArchivingEnabled; +import static com.android.server.pm.PackageInstallerSession.isValidVerificationPolicy; import static com.android.server.pm.PackageManagerService.SHELL_PACKAGE_NAME; import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT; @@ -150,6 +152,7 @@ import java.util.Set; import java.util.TreeMap; import java.util.TreeSet; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.IntPredicate; import java.util.function.Supplier; @@ -275,6 +278,13 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements } }; + /** + * Default verification policy for incoming installation sessions. + * TODO(b/360129657): update the default policy. + */ + private final AtomicInteger mVerificationPolicy = new AtomicInteger( + VERIFICATION_POLICY_BLOCK_FAIL_WARN); + private static final class Lifecycle extends SystemService { private final PackageInstallerService mPackageInstallerService; @@ -1042,7 +1052,7 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements userId, callingUid, installSource, params, createdMillis, 0L, stageDir, stageCid, null, null, false, false, false, false, null, SessionInfo.INVALID_ID, false, false, false, PackageManager.INSTALL_UNKNOWN, "", null, - mVerifierController); + mVerifierController, mVerificationPolicy.get()); synchronized (mSessions) { mSessions.put(sessionId, session); @@ -1866,6 +1876,34 @@ public class PackageInstallerService extends IPackageInstaller.Stub implements } } + @Override + public @PackageInstaller.VerificationPolicy int getVerificationPolicy() { + if (mContext.checkCallingOrSelfPermission(Manifest.permission.VERIFICATION_AGENT) + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("You need the " + + "com.android.permission.VERIFICATION_AGENT permission " + + "to get the verification policy"); + } + return mVerificationPolicy.get(); + } + + @Override + public boolean setVerificationPolicy(@PackageInstaller.VerificationPolicy int policy) { + if (mContext.checkCallingOrSelfPermission(Manifest.permission.VERIFICATION_AGENT) + != PackageManager.PERMISSION_GRANTED) { + throw new SecurityException("You need the " + + "com.android.permission.VERIFICATION_AGENT permission " + + "to set the verification policy"); + } + if (!isValidVerificationPolicy(policy)) { + return false; + } + if (policy != mVerificationPolicy.get()) { + mVerificationPolicy.set(policy); + } + return true; + } + private static int getSessionCount(SparseArray<PackageInstallerSession> sessions, int installerUid) { int count = 0; diff --git a/services/core/java/com/android/server/pm/PackageInstallerSession.java b/services/core/java/com/android/server/pm/PackageInstallerSession.java index 9e0ba8492ab9..04d0d182d3b2 100644 --- a/services/core/java/com/android/server/pm/PackageInstallerSession.java +++ b/services/core/java/com/android/server/pm/PackageInstallerSession.java @@ -21,9 +21,17 @@ import static android.app.admin.DevicePolicyResources.Strings.Core.PACKAGE_INSTA import static android.app.admin.DevicePolicyResources.Strings.Core.PACKAGE_UPDATED_BY_DO; import static android.content.pm.DataLoaderType.INCREMENTAL; import static android.content.pm.DataLoaderType.STREAMING; +import static android.content.pm.PackageInstaller.EXTRA_VERIFICATION_FAILURE_REASON; import static android.content.pm.PackageInstaller.LOCATION_DATA_APP; import static android.content.pm.PackageInstaller.UNARCHIVAL_OK; import static android.content.pm.PackageInstaller.UNARCHIVAL_STATUS_UNSET; +import static android.content.pm.PackageInstaller.VERIFICATION_FAILED_REASON_NETWORK_UNAVAILABLE; +import static android.content.pm.PackageInstaller.VERIFICATION_FAILED_REASON_PACKAGE_BLOCKED; +import static android.content.pm.PackageInstaller.VERIFICATION_FAILED_REASON_UNKNOWN; +import static android.content.pm.PackageInstaller.VERIFICATION_POLICY_BLOCK_FAIL_CLOSED; +import static android.content.pm.PackageInstaller.VERIFICATION_POLICY_BLOCK_FAIL_OPEN; +import static android.content.pm.PackageInstaller.VERIFICATION_POLICY_BLOCK_FAIL_WARN; +import static android.content.pm.PackageInstaller.VERIFICATION_POLICY_NONE; import static android.content.pm.PackageItemInfo.MAX_SAFE_LABEL_LENGTH; import static android.content.pm.PackageManager.INSTALL_FAILED_ABORTED; import static android.content.pm.PackageManager.INSTALL_FAILED_BAD_SIGNATURE; @@ -38,7 +46,7 @@ import static android.content.pm.PackageManager.INSTALL_FAILED_VERIFICATION_FAIL import static android.content.pm.PackageManager.INSTALL_PARSE_FAILED_NO_CERTIFICATES; import static android.content.pm.PackageManager.INSTALL_STAGED; import static android.content.pm.PackageManager.INSTALL_SUCCEEDED; -import static android.content.pm.verify.pkg.VerificationSession.VERIFICATION_INCOMPLETE_UNKNOWN; +import static android.content.pm.verify.pkg.VerificationSession.VERIFICATION_INCOMPLETE_NETWORK_UNAVAILABLE; import static android.os.Process.INVALID_UID; import static android.provider.DeviceConfig.NAMESPACE_PACKAGE_MANAGER_SERVICE; import static android.system.OsConstants.O_CREAT; @@ -313,6 +321,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { private static final String ATTR_APPLICATION_ENABLED_SETTING_PERSISTENT = "applicationEnabledSettingPersistent"; private static final String ATTR_DOMAIN = "domain"; + private static final String ATTR_VERIFICATION_POLICY = "verificationPolicy"; private static final String PROPERTY_NAME_INHERIT_NATIVE = "pi.inherit_native_on_dont_kill"; private static final int[] EMPTY_CHILD_SESSION_ARRAY = EmptyArray.INT; @@ -410,6 +419,11 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { private final PackageSessionProvider mSessionProvider; private final SilentUpdatePolicy mSilentUpdatePolicy; /** + * The verification policy applied to this session, which might be different from the default + * verification policy used by the system. + */ + private final AtomicInteger mVerificationPolicy; + /** * Note all calls must be done outside {@link #mLock} to prevent lock inversion. */ private final StagingManager mStagingManager; @@ -791,7 +805,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { if (errorMsg != null) { Slog.e(TAG, "verifySession error: " + errorMsg); setSessionFailed(INSTALL_FAILED_INTERNAL_ERROR, errorMsg); - onSessionVerificationFailure(INSTALL_FAILED_INTERNAL_ERROR, errorMsg); + onSessionVerificationFailure(INSTALL_FAILED_INTERNAL_ERROR, errorMsg, + /* extras= */ null); return false; } return true; @@ -1167,7 +1182,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { @Nullable int[] childSessionIds, int parentSessionId, boolean isReady, boolean isFailed, boolean isApplied, int sessionErrorCode, String sessionErrorMessage, DomainSet preVerifiedDomains, - @NonNull VerifierController verifierController) { + @NonNull VerifierController verifierController, + @PackageInstaller.VerificationPolicy int verificationPolicy) { mCallback = callback; mContext = context; mPm = pm; @@ -1177,6 +1193,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { mHandler = new Handler(looper, mHandlerCallback); mStagingManager = stagingManager; mVerifierController = verifierController; + mVerificationPolicy = new AtomicInteger(verificationPolicy); this.sessionId = sessionId; this.userId = userId; @@ -2580,10 +2597,10 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { dispatchSessionFinished(error, detailMessage, null); } - private void onSessionVerificationFailure(int error, String msg) { + private void onSessionVerificationFailure(int error, String msg, Bundle extras) { Slog.e(TAG, "Failed to verify session " + sessionId); // Dispatch message to remove session from PackageInstallerService. - dispatchSessionFinished(error, msg, null); + dispatchSessionFinished(error, msg, extras); maybeFinishChildSessions(error, msg); } @@ -2856,7 +2873,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { final String completeMsg = ExceptionUtils.getCompleteMessage(e); final String errorMsg = PackageManager.installStatusToString(e.error, completeMsg); setSessionFailed(e.error, errorMsg); - onSessionVerificationFailure(e.error, errorMsg); + onSessionVerificationFailure(e.error, errorMsg, /* extras= */ null); } if (Flags.verificationService()) { final Supplier<Computer> snapshotSupplier = mPm::snapshotComputer; @@ -2872,11 +2889,12 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { // the installation can proceed. if (!mVerifierController.startVerificationSession(snapshotSupplier, userId, sessionId, getPackageName(), Uri.fromFile(stageDir), signingInfo, - declaredLibraries, /* extensionParams= */ null, + declaredLibraries, mVerificationPolicy.get(), /* extensionParams= */ null, new VerifierCallback(), /* retry= */ false)) { // A verifier is installed but cannot be connected. Installation disallowed. onSessionVerificationFailure(INSTALL_FAILED_INTERNAL_ERROR, - "A verifier agent is available on device but cannot be connected."); + "A verifier agent is available on device but cannot be connected.", + /* extras= */ null); } } else { // Verifier is not installed. Let the installation pass for now. @@ -2917,7 +2935,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { final String completeMsg = ExceptionUtils.getCompleteMessage(e); final String errorMsg = PackageManager.installStatusToString(e.error, completeMsg); setSessionFailed(e.error, errorMsg); - onSessionVerificationFailure(e.error, errorMsg); + onSessionVerificationFailure(e.error, errorMsg, /* extras= */ null); } } @@ -2926,24 +2944,57 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { */ public class VerifierCallback { /** + * Called by the VerifierController when the verifier requests to get the current + * verification policy for this session. + */ + public @PackageInstaller.VerificationPolicy int getVerificationPolicy() { + return mVerificationPolicy.get(); + } + /** + * Called by the VerifierController when the verifier requests to change the verification + * policy for this session. + */ + public boolean setVerificationPolicy(@PackageInstaller.VerificationPolicy int policy) { + if (!isValidVerificationPolicy(policy)) { + return false; + } + mVerificationPolicy.set(policy); + return true; + } + /** * Called by the VerifierController when the connection has failed. */ public void onConnectionFailed() { - mHandler.post(() -> { - onSessionVerificationFailure(INSTALL_FAILED_VERIFICATION_FAILURE, - "A verifier agent is available on device but cannot be connected."); - }); + // TODO(b/360129657): prompt user on fail warning + handleNonPackageBlockedFailure( + /* onFailWarning= */ PackageInstallerSession.this::resumeVerify, + /* onFailClosed= */ () -> { + Bundle bundle = new Bundle(); + bundle.putInt(EXTRA_VERIFICATION_FAILURE_REASON, + VERIFICATION_FAILED_REASON_UNKNOWN); + onSessionVerificationFailure(INSTALL_FAILED_VERIFICATION_FAILURE, + "A verifier agent is available on device but cannot be connected.", + bundle); + + }); } /** * Called by the VerifierController when the verification request has timed out. */ public void onTimeout() { - mHandler.post(() -> { - mVerifierController.notifyVerificationTimeout(sessionId); - onSessionVerificationFailure(INSTALL_FAILED_VERIFICATION_FAILURE, - "Verification timed out; missing a response from the verifier within the" - + " time limit"); - }); + // Always notify the verifier, regardless of the policy. + mVerifierController.notifyVerificationTimeout(sessionId); + // TODO(b/360129657): prompt user on fail warning + handleNonPackageBlockedFailure( + /* onFailWarning= */ PackageInstallerSession.this::resumeVerify, + /* onFailClosed= */ () -> { + Bundle bundle = new Bundle(); + bundle.putInt(EXTRA_VERIFICATION_FAILURE_REASON, + VERIFICATION_FAILED_REASON_UNKNOWN); + onSessionVerificationFailure(INSTALL_FAILED_VERIFICATION_FAILURE, + "Verification timed out; missing a response from the verifier" + + " within the time limit", bundle); + }); } /** * Called by the VerifierController when the verification request has received a complete @@ -2953,17 +3004,22 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { @Nullable PersistableBundle extensionResponse) { // TODO: handle extension response mHandler.post(() -> { - if (statusReceived.isVerified()) { + if (statusReceived.isVerified() + || mVerificationPolicy.get() == VERIFICATION_POLICY_NONE) { // Continue with the rest of the verification and installation. resumeVerify(); - } else { - StringBuilder sb = new StringBuilder("Verifier rejected the installation"); - if (!TextUtils.isEmpty(statusReceived.getFailureMessage())) { - sb.append(" with message: ").append(statusReceived.getFailureMessage()); - } - onSessionVerificationFailure(INSTALL_FAILED_VERIFICATION_FAILURE, - sb.toString()); + return; + } + // Package is blocked. + StringBuilder sb = new StringBuilder("Verifier rejected the installation"); + if (!TextUtils.isEmpty(statusReceived.getFailureMessage())) { + sb.append(" with message: ").append(statusReceived.getFailureMessage()); } + Bundle bundle = new Bundle(); + bundle.putInt(EXTRA_VERIFICATION_FAILURE_REASON, + VERIFICATION_FAILED_REASON_PACKAGE_BLOCKED); + onSessionVerificationFailure(INSTALL_FAILED_VERIFICATION_FAILURE, + sb.toString(), bundle); }); } /** @@ -2971,16 +3027,51 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { * response. */ public void onVerificationIncompleteReceived(int incompleteReason) { - mHandler.post(() -> { - if (incompleteReason == VERIFICATION_INCOMPLETE_UNKNOWN) { - // TODO: change this to a user confirmation and handle other incomplete reasons - onSessionVerificationFailure(INSTALL_FAILED_INTERNAL_ERROR, - "Verification cannot be completed for unknown reasons."); + // TODO(b/360129657): prompt user on fail warning + handleNonPackageBlockedFailure( + /* onFailWarning= */ PackageInstallerSession.this::resumeVerify, + /* onFailClosed= */ () -> { + final int failureReason; + StringBuilder sb = new StringBuilder( + "Verification cannot be completed because of "); + if (incompleteReason == VERIFICATION_INCOMPLETE_NETWORK_UNAVAILABLE) { + failureReason = VERIFICATION_FAILED_REASON_NETWORK_UNAVAILABLE; + sb.append("unavailable network."); + } else { + failureReason = VERIFICATION_FAILED_REASON_UNKNOWN; + sb.append("unknown reasons."); + } + Bundle bundle = new Bundle(); + bundle.putInt(EXTRA_VERIFICATION_FAILURE_REASON, failureReason); + onSessionVerificationFailure(INSTALL_FAILED_VERIFICATION_FAILURE, + sb.toString(), bundle); + }); + } + + private void handleNonPackageBlockedFailure(Runnable onFailWarning, Runnable onFailClosed) { + final Runnable r = switch (mVerificationPolicy.get()) { + case VERIFICATION_POLICY_NONE, VERIFICATION_POLICY_BLOCK_FAIL_OPEN -> + PackageInstallerSession.this::resumeVerify; + case VERIFICATION_POLICY_BLOCK_FAIL_WARN -> onFailWarning; + case VERIFICATION_POLICY_BLOCK_FAIL_CLOSED -> onFailClosed; + default -> { + Log.wtf(TAG, "Unknown verification policy: " + mVerificationPolicy.get()); + yield onFailClosed; } - }); + }; + mHandler.post(r); } } + /** + * Returns whether a policy is a valid verification policy. + */ + public static boolean isValidVerificationPolicy( + @PackageInstaller.VerificationPolicy int policy) { + return policy >= VERIFICATION_POLICY_NONE + && policy <= VERIFICATION_POLICY_BLOCK_FAIL_CLOSED; + } + private IntentSender getRemoteStatusReceiver() { synchronized (mLock) { return mRemoteStatusReceiver; @@ -3156,7 +3247,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { if (error == INSTALL_SUCCEEDED) { onVerificationComplete(); } else { - onSessionVerificationFailure(error, msg); + onSessionVerificationFailure(error, msg, /* extras= */ null); } }); }); @@ -5328,6 +5419,14 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { } } + /** + * @return the current policy for the verification request associated with this session. + */ + @VisibleForTesting + public @PackageInstaller.VerificationPolicy int getVerificationPolicy() { + assertCallerIsOwnerOrRoot(); + return mVerificationPolicy.get(); + } void setSessionReady() { synchronized (mLock) { @@ -5631,6 +5730,10 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { if (!ArrayUtils.isEmpty(warnings)) { fillIn.putStringArrayListExtra(PackageInstaller.EXTRA_WARNINGS, warnings); } + if (extras.containsKey(EXTRA_VERIFICATION_FAILURE_REASON)) { + fillIn.putExtra(EXTRA_VERIFICATION_FAILURE_REASON, + extras.getInt(EXTRA_VERIFICATION_FAILURE_REASON)); + } } try { final BroadcastOptions options = BroadcastOptions.makeBasic(); @@ -5786,6 +5889,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { out.attributeInt(null, ATTR_INSTALL_REASON, params.installReason); writeBooleanAttribute(out, ATTR_APPLICATION_ENABLED_SETTING_PERSISTENT, params.applicationEnabledSettingPersistent); + out.attributeInt(null, ATTR_VERIFICATION_POLICY, mVerificationPolicy.get()); final boolean isDataLoader = params.dataLoaderParams != null; writeBooleanAttribute(out, ATTR_IS_DATALOADER, isDataLoader); @@ -5936,6 +6040,8 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { final boolean sealed = in.getAttributeBoolean(null, ATTR_SEALED, false); final int parentSessionId = in.getAttributeInt(null, ATTR_PARENT_SESSION_ID, SessionInfo.INVALID_ID); + final int verificationPolicy = in.getAttributeInt(null, ATTR_VERIFICATION_POLICY, + VERIFICATION_POLICY_NONE); final SessionParams params = new SessionParams( SessionParams.MODE_INVALID); @@ -6110,6 +6216,7 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub { installerUid, installSource, params, createdMillis, committedMillis, stageDir, stageCid, fileArray, checksumsMap, prepared, committed, destroyed, sealed, childSessionIdsArray, parentSessionId, isReady, isFailed, isApplied, - sessionErrorCode, sessionErrorMessage, preVerifiedDomains, verifierController); + sessionErrorCode, sessionErrorMessage, preVerifiedDomains, verifierController, + verificationPolicy); } } diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 455776993c56..d78f12217271 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -4709,10 +4709,11 @@ public class PackageManagerService implements PackageSender, TestUtilityService extras.putLong(Intent.EXTRA_TIME, SystemClock.elapsedRealtime()); mHandler.post(() -> { mBroadcastHelper.sendPackageBroadcast(Intent.ACTION_PACKAGE_UNSTOPPED, - packageName, extras, - Intent.FLAG_RECEIVER_REGISTERED_ONLY, null, null, - userIds, null, broadcastAllowList, null, - null); + packageName, extras, Intent.FLAG_RECEIVER_REGISTERED_ONLY, + null /* targetPkg */, null /* finishedReceiver */, userIds, + null /* instantUserIds */, broadcastAllowList, + null /* filterExtrasForReceiver */, null /* bOptions */, + null /* requiredPermissions */); }); mPackageMonitorCallbackHelper.notifyPackageMonitor(Intent.ACTION_PACKAGE_UNSTOPPED, packageName, extras, userIds, null /* instantUserIds */, @@ -7169,17 +7170,17 @@ public class PackageManagerService implements PackageSender, TestUtilityService // Sent async using the PM handler, to maintain ordering with PACKAGE_UNSTOPPED mHandler.post(() -> { mBroadcastHelper.sendPackageBroadcast(Intent.ACTION_PACKAGE_RESTARTED, - packageName, extras, - flags, null, null, - userIds, null, broadcastAllowList, null, - null); + packageName, extras, flags, null /* targetPkg */, + null /* finishedReceiver */, userIds, null /* instantUserIds */, + broadcastAllowList, null /* filterExtrasForReceiver */, + null /* bOptions */, null /* requiredPermissions */); }); } else { mBroadcastHelper.sendPackageBroadcast(Intent.ACTION_PACKAGE_RESTARTED, - packageName, extras, - flags, null, null, - userIds, null, broadcastAllowList, null, - null); + packageName, extras, flags, null /* targetPkg */, + null /* finishedReceiver */, userIds, null /* instantUserIds */, + broadcastAllowList, null /* filterExtrasForReceiver */, null /* bOptions */, + null /* requiredPermissions */); } mPackageMonitorCallbackHelper.notifyPackageMonitor(Intent.ACTION_PACKAGE_RESTARTED, packageName, extras, userIds, null /* instantUserIds */, diff --git a/services/core/java/com/android/server/pm/verify/pkg/VerifierController.java b/services/core/java/com/android/server/pm/verify/pkg/VerifierController.java index 7eac940933c2..b7cc7ccead89 100644 --- a/services/core/java/com/android/server/pm/verify/pkg/VerifierController.java +++ b/services/core/java/com/android/server/pm/verify/pkg/VerifierController.java @@ -30,6 +30,7 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.ApplicationInfo; +import android.content.pm.PackageInstaller; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.SharedLibraryInfo; @@ -94,9 +95,22 @@ public class VerifierController { // Max duration allowed to wait for a verifier to respond to a verification request. private static final long DEFAULT_MAX_VERIFICATION_REQUEST_EXTENDED_TIMEOUT_MILLIS = TimeUnit.MINUTES.toMillis(10); + /** + * Configurable maximum amount of time in milliseconds for the system to wait from the moment + * when the installation session requires a verification, till when the request is delivered to + * the verifier, pending the connection to be established. If the request has not been delivered + * to the verifier within this amount of time, e.g., because the verifier has crashed or ANR'd, + * the controller then sends a failure status back to the installation session. + * Flag type: {@code long} + * Namespace: NAMESPACE_PACKAGE_MANAGER_SERVICE + */ + private static final String PROPERTY_VERIFIER_CONNECTION_TIMEOUT_MILLIS = + "verifier_connection_timeout_millis"; // The maximum amount of time to wait from the moment when the session requires a verification, // till when the request is delivered to the verifier, pending the connection to be established. - private static final long CONNECTION_TIMEOUT_SECONDS = 10; + private static final long DEFAULT_VERIFIER_CONNECTION_TIMEOUT_MILLIS = + TimeUnit.SECONDS.toMillis(10); + // The maximum amount of time to wait before the system unbinds from the verifier. private static final long UNBIND_TIMEOUT_MILLIS = TimeUnit.HOURS.toMillis(6); @@ -271,6 +285,7 @@ public class VerifierController { int installationSessionId, String packageName, Uri stagedPackageUri, SigningInfo signingInfo, List<SharedLibraryInfo> declaredLibraries, + @PackageInstaller.VerificationPolicy int verificationPolicy, PersistableBundle extensionParams, PackageInstallerSession.VerifierCallback callback, boolean retry) { // Try connecting to the verifier if not already connected @@ -292,7 +307,7 @@ public class VerifierController { /* id= */ verificationId, /* installSessionId= */ installationSessionId, packageName, stagedPackageUri, signingInfo, declaredLibraries, extensionParams, - new VerificationSessionInterface(), + verificationPolicy, new VerificationSessionInterface(callback), new VerificationSessionCallback(callback)); AndroidFuture<Void> unusedFuture = mRemoteService.post(service -> { if (!retry) { @@ -306,7 +321,8 @@ public class VerifierController { } service.onVerificationRetry(session); } - }).orTimeout(CONNECTION_TIMEOUT_SECONDS, TimeUnit.SECONDS).whenComplete((res, err) -> { + }).orTimeout(mInjector.getVerifierConnectionTimeoutMillis(), TimeUnit.MILLISECONDS) + .whenComplete((res, err) -> { if (err != null) { Slog.e(TAG, "Error notifying verification request for session " + verificationId, err); @@ -407,6 +423,12 @@ public class VerifierController { // This class handles requests from the remote verifier private class VerificationSessionInterface extends IVerificationSessionInterface.Stub { + private final PackageInstallerSession.VerifierCallback mCallback; + + VerificationSessionInterface(PackageInstallerSession.VerifierCallback callback) { + mCallback = callback; + } + @Override public long getTimeoutTime(int verificationId) { checkCallerPermission(); @@ -432,6 +454,20 @@ public class VerifierController { return tracker.extendTimeRemaining(additionalMs); } } + + @Override + public boolean setVerificationPolicy(int verificationId, + @PackageInstaller.VerificationPolicy int policy) { + checkCallerPermission(); + synchronized (mVerificationStatus) { + final VerificationStatusTracker tracker = mVerificationStatus.get(verificationId); + if (tracker == null) { + throw new IllegalStateException("Verification session " + verificationId + + " doesn't exist or has finished"); + } + } + return mCallback.setVerificationPolicy(policy); + } } private class VerificationSessionCallback extends IVerificationSessionCallback.Stub { @@ -451,8 +487,8 @@ public class VerifierController { throw new IllegalStateException("Verification session " + id + " doesn't exist or has finished"); } - mCallback.onVerificationIncompleteReceived(reason); } + mCallback.onVerificationIncompleteReceived(reason); // Remove status tracking and stop the timeout countdown removeStatusTracker(id); } @@ -630,6 +666,14 @@ public class VerifierController { return getMaxVerificationExtendedTimeoutMillisFromDeviceConfig(); } + /** + * This is added so that we can mock the maximum connection timeout duration without + * calling into DeviceConfig. + */ + public long getVerifierConnectionTimeoutMillis() { + return getVerifierConnectionTimeoutMillisFromDeviceConfig(); + } + private static long getVerificationRequestTimeoutMillisFromDeviceConfig() { return DeviceConfig.getLong(NAMESPACE_PACKAGE_MANAGER_SERVICE, PROPERTY_VERIFICATION_REQUEST_TIMEOUT_MILLIS, @@ -641,5 +685,11 @@ public class VerifierController { PROPERTY_MAX_VERIFICATION_REQUEST_EXTENDED_TIMEOUT_MILLIS, DEFAULT_MAX_VERIFICATION_REQUEST_EXTENDED_TIMEOUT_MILLIS); } + + private static long getVerifierConnectionTimeoutMillisFromDeviceConfig() { + return DeviceConfig.getLong(NAMESPACE_PACKAGE_MANAGER_SERVICE, + PROPERTY_VERIFIER_CONNECTION_TIMEOUT_MILLIS, + DEFAULT_VERIFIER_CONNECTION_TIMEOUT_MILLIS); + } } } diff --git a/services/core/java/com/android/server/rotationresolver/RotationResolverManagerService.java b/services/core/java/com/android/server/rotationresolver/RotationResolverManagerService.java index 8f603fc34b32..075a31f3b24c 100644 --- a/services/core/java/com/android/server/rotationresolver/RotationResolverManagerService.java +++ b/services/core/java/com/android/server/rotationresolver/RotationResolverManagerService.java @@ -191,8 +191,7 @@ public class RotationResolverManagerService extends SensorPrivacyManager.Sensors.CAMERA); if (mIsServiceEnabled && isCameraAvailable) { final RotationResolverManagerPerUserService service = - getServiceForUserLocked( - UserHandle.getCallingUserId()); + getServiceForUserLocked(UserHandle.USER_CURRENT); final RotationResolutionRequest request; if (packageName == null) { request = new RotationResolutionRequest(/* packageName */ "", diff --git a/services/core/java/com/android/server/security/forensic/ForensicService.java b/services/core/java/com/android/server/security/forensic/ForensicService.java new file mode 100644 index 000000000000..07639d1a3945 --- /dev/null +++ b/services/core/java/com/android/server/security/forensic/ForensicService.java @@ -0,0 +1,294 @@ +/* + * Copyright (C) 2024 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.server.security.forensic; + +import android.annotation.NonNull; +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.os.RemoteException; +import android.security.forensic.IForensicService; +import android.security.forensic.IForensicServiceCommandCallback; +import android.security.forensic.IForensicServiceStateCallback; +import android.util.Slog; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.server.ServiceThread; +import com.android.server.SystemService; + +import java.util.ArrayList; + +/** + * @hide + */ +public class ForensicService extends SystemService { + private static final String TAG = "ForensicService"; + + private static final int MSG_MONITOR_STATE = 0; + private static final int MSG_MAKE_VISIBLE = 1; + private static final int MSG_MAKE_INVISIBLE = 2; + private static final int MSG_ENABLE = 3; + private static final int MSG_DISABLE = 4; + private static final int MSG_BACKUP = 5; + + private static final int STATE_UNKNOWN = IForensicServiceStateCallback.State.UNKNOWN; + private static final int STATE_INVISIBLE = IForensicServiceStateCallback.State.INVISIBLE; + private static final int STATE_VISIBLE = IForensicServiceStateCallback.State.VISIBLE; + private static final int STATE_ENABLED = IForensicServiceStateCallback.State.ENABLED; + + private static final int ERROR_UNKNOWN = IForensicServiceCommandCallback.ErrorCode.UNKNOWN; + private static final int ERROR_PERMISSION_DENIED = + IForensicServiceCommandCallback.ErrorCode.PERMISSION_DENIED; + private static final int ERROR_INVALID_STATE_TRANSITION = + IForensicServiceCommandCallback.ErrorCode.INVALID_STATE_TRANSITION; + private static final int ERROR_BACKUP_TRANSPORT_UNAVAILABLE = + IForensicServiceCommandCallback.ErrorCode.BACKUP_TRANSPORT_UNAVAILABLE; + private static final int ERROR_DATA_SOURCE_UNAVAILABLE = + IForensicServiceCommandCallback.ErrorCode.DATA_SOURCE_UNAVAILABLE; + + private final Context mContext; + private final Handler mHandler; + private final BinderService mBinderService; + + private final ArrayList<IForensicServiceStateCallback> mStateMonitors = new ArrayList<>(); + private volatile int mState = STATE_INVISIBLE; + + public ForensicService(@NonNull Context context) { + this(new InjectorImpl(context)); + } + + @VisibleForTesting + ForensicService(@NonNull Injector injector) { + super(injector.getContext()); + mContext = injector.getContext(); + mHandler = new EventHandler(injector.getLooper(), this); + mBinderService = new BinderService(this); + } + + @VisibleForTesting + protected void setState(int state) { + mState = state; + } + + private static final class BinderService extends IForensicService.Stub { + final ForensicService mService; + + BinderService(ForensicService service) { + mService = service; + } + + @Override + public void monitorState(IForensicServiceStateCallback callback) { + mService.mHandler.obtainMessage(MSG_MONITOR_STATE, callback).sendToTarget(); + } + + @Override + public void makeVisible(IForensicServiceCommandCallback callback) { + mService.mHandler.obtainMessage(MSG_MAKE_VISIBLE, callback).sendToTarget(); + } + + @Override + public void makeInvisible(IForensicServiceCommandCallback callback) { + mService.mHandler.obtainMessage(MSG_MAKE_INVISIBLE, callback).sendToTarget(); + } + + @Override + public void enable(IForensicServiceCommandCallback callback) { + mService.mHandler.obtainMessage(MSG_ENABLE, callback).sendToTarget(); + } + + @Override + public void disable(IForensicServiceCommandCallback callback) { + mService.mHandler.obtainMessage(MSG_DISABLE, callback).sendToTarget(); + } + } + + private static class EventHandler extends Handler { + private final ForensicService mService; + + EventHandler(Looper looper, ForensicService service) { + super(looper); + mService = service; + } + + @Override + public void handleMessage(Message msg) { + switch (msg.what) { + case MSG_MONITOR_STATE: + try { + mService.monitorState( + (IForensicServiceStateCallback) msg.obj); + } catch (RemoteException e) { + Slog.e(TAG, "RemoteException", e); + } + break; + case MSG_MAKE_VISIBLE: + try { + mService.makeVisible((IForensicServiceCommandCallback) msg.obj); + } catch (RemoteException e) { + Slog.e(TAG, "RemoteException", e); + } + break; + case MSG_MAKE_INVISIBLE: + try { + mService.makeInvisible((IForensicServiceCommandCallback) msg.obj); + } catch (RemoteException e) { + Slog.e(TAG, "RemoteException", e); + } + break; + case MSG_ENABLE: + try { + mService.enable((IForensicServiceCommandCallback) msg.obj); + } catch (RemoteException e) { + Slog.e(TAG, "RemoteException", e); + } + break; + case MSG_DISABLE: + try { + mService.disable((IForensicServiceCommandCallback) msg.obj); + } catch (RemoteException e) { + Slog.e(TAG, "RemoteException", e); + } + break; + default: + Slog.w(TAG, "Unknown message: " + msg.what); + } + } + } + + private void monitorState(IForensicServiceStateCallback callback) throws RemoteException { + for (int i = 0; i < mStateMonitors.size(); i++) { + if (mStateMonitors.get(i).asBinder() == callback.asBinder()) { + return; + } + } + mStateMonitors.add(callback); + callback.onStateChange(mState); + } + + private void notifyStateMonitors() throws RemoteException { + for (int i = 0; i < mStateMonitors.size(); i++) { + mStateMonitors.get(i).onStateChange(mState); + } + } + + private void makeVisible(IForensicServiceCommandCallback callback) throws RemoteException { + switch (mState) { + case STATE_INVISIBLE: + mState = STATE_VISIBLE; + notifyStateMonitors(); + callback.onSuccess(); + break; + case STATE_VISIBLE: + callback.onSuccess(); + break; + default: + callback.onFailure(ERROR_INVALID_STATE_TRANSITION); + } + } + + private void makeInvisible(IForensicServiceCommandCallback callback) throws RemoteException { + switch (mState) { + case STATE_VISIBLE: + case STATE_ENABLED: + mState = STATE_INVISIBLE; + notifyStateMonitors(); + callback.onSuccess(); + break; + case STATE_INVISIBLE: + callback.onSuccess(); + break; + default: + callback.onFailure(ERROR_INVALID_STATE_TRANSITION); + } + } + + private void enable(IForensicServiceCommandCallback callback) throws RemoteException { + switch (mState) { + case STATE_VISIBLE: + mState = STATE_ENABLED; + notifyStateMonitors(); + callback.onSuccess(); + break; + case STATE_ENABLED: + callback.onSuccess(); + break; + default: + callback.onFailure(ERROR_INVALID_STATE_TRANSITION); + } + } + + private void disable(IForensicServiceCommandCallback callback) throws RemoteException { + switch (mState) { + case STATE_ENABLED: + mState = STATE_VISIBLE; + notifyStateMonitors(); + callback.onSuccess(); + break; + case STATE_VISIBLE: + callback.onSuccess(); + break; + default: + callback.onFailure(ERROR_INVALID_STATE_TRANSITION); + } + } + + @Override + public void onStart() { + try { + publishBinderService(Context.FORENSIC_SERVICE, mBinderService); + } catch (Throwable t) { + Slog.e(TAG, "Could not start the ForensicService.", t); + } + } + + @VisibleForTesting + IForensicService getBinderService() { + return mBinderService; + } + + interface Injector { + Context getContext(); + + Looper getLooper(); + } + + private static final class InjectorImpl implements Injector { + private final Context mContext; + + InjectorImpl(Context context) { + mContext = context; + } + + @Override + public Context getContext() { + return mContext; + } + + + @Override + public Looper getLooper() { + ServiceThread serviceThread = + new ServiceThread( + TAG, android.os.Process.THREAD_PRIORITY_FOREGROUND, true /* allowIo */); + serviceThread.start(); + return serviceThread.getLooper(); + } + } +} + diff --git a/services/core/java/com/android/server/tv/TvInputManagerService.java b/services/core/java/com/android/server/tv/TvInputManagerService.java index 91a17a9e1c31..4589d26261dc 100644 --- a/services/core/java/com/android/server/tv/TvInputManagerService.java +++ b/services/core/java/com/android/server/tv/TvInputManagerService.java @@ -381,10 +381,12 @@ public final class TvInputManagerService extends SystemService { // service to populate the hardware list. serviceState = new ServiceState(component, userId); userState.serviceStateMap.put(component, serviceState); - updateServiceConnectionLocked(component, userId); } else { inputList.addAll(serviceState.hardwareInputMap.values()); } + if (serviceState.needInit) { + updateServiceConnectionLocked(component, userId); + } } else { try { TvInputInfo info = new TvInputInfo.Builder(mContext, ri).build(); @@ -489,6 +491,27 @@ public final class TvInputManagerService extends SystemService { } } + @GuardedBy("mLock") + private void cleanUpHdmiDevices(int userId) { + if (DEBUG) { + Slog.d(TAG, "cleanUpHdmiDevices: user " + userId); + } + UserState userState = getOrCreateUserStateLocked(userId); + for (ServiceState serviceState : userState.serviceStateMap.values()) { + for (HdmiDeviceInfo device : mTvInputHardwareManager.getHdmiDeviceList()) { + try { + if (serviceState.service != null) { + serviceState.service.notifyHdmiDeviceRemoved(device); + } else { + serviceState.hdmiDeviceRemovedBuffer.add(device); + } + } catch (RemoteException e) { + Slog.e(TAG, "error in notifyHdmiDeviceRemoved", e); + } + } + } + } + private void startUser(int userId) { synchronized (mLock) { if (userId == mCurrentUserId || mRunningProfiles.contains(userId)) { @@ -500,9 +523,13 @@ public final class TvInputManagerService extends SystemService { if (userInfo.isProfile() && parentInfo != null && parentInfo.id == mCurrentUserId) { - // only the children of the current user can be started in background + int prevUserId = mCurrentUserId; mCurrentUserId = userId; - startProfileLocked(userId); + // only the children of the current user can be started in background + releaseSessionOfUserLocked(prevUserId); + cleanUpHdmiDevices(prevUserId); + unbindServiceOfUserLocked(prevUserId); + startProfileLocked(mCurrentUserId); } } } @@ -515,6 +542,7 @@ public final class TvInputManagerService extends SystemService { } releaseSessionOfUserLocked(userId); + cleanUpHdmiDevices(userId); unbindServiceOfUserLocked(userId); mRunningProfiles.remove(userId); } @@ -543,15 +571,19 @@ public final class TvInputManagerService extends SystemService { unbindServiceOfUserLocked(runningId); } mRunningProfiles.clear(); - releaseSessionOfUserLocked(mCurrentUserId); - unbindServiceOfUserLocked(mCurrentUserId); + int prevUserId = mCurrentUserId; mCurrentUserId = userId; - buildTvInputListLocked(userId, null); - buildTvContentRatingSystemListLocked(userId); + + releaseSessionOfUserLocked(prevUserId); + cleanUpHdmiDevices(prevUserId); + unbindServiceOfUserLocked(prevUserId); + + buildTvInputListLocked(mCurrentUserId, null); + buildTvContentRatingSystemListLocked(mCurrentUserId); mMessageHandler .obtainMessage(MessageHandler.MSG_SWITCH_CONTENT_RESOLVER, - getContentResolverForUser(userId)) + getContentResolverForUser(mCurrentUserId)) .sendToTarget(); } } @@ -590,6 +622,9 @@ public final class TvInputManagerService extends SystemService { @GuardedBy("mLock") private void unbindServiceOfUserLocked(int userId) { + if (DEBUG) { + Slog.d(TAG, "unbindServiceOfUserLocked: user " + userId); + } UserState userState = getUserStateLocked(userId); if (userState == null) { return; @@ -600,7 +635,12 @@ public final class TvInputManagerService extends SystemService { ServiceState serviceState = userState.serviceStateMap.get(component); if (serviceState != null && serviceState.sessionTokens.isEmpty()) { unbindService(serviceState); - it.remove(); + if (!serviceState.isHardware) { + it.remove(); + } else { + serviceState.hardwareInputMap.clear(); + serviceState.needInit = true; + } } } } @@ -774,7 +814,7 @@ public final class TvInputManagerService extends SystemService { boolean shouldBind; if (userId == mCurrentUserId || mRunningProfiles.contains(userId)) { shouldBind = !serviceState.sessionTokens.isEmpty() - || (serviceState.isHardware && serviceState.neverConnected); + || (serviceState.isHardware && serviceState.needInit); } else { // For a non-current user, // if sessionTokens is not empty, it contains recording sessions only @@ -3404,13 +3444,13 @@ public final class TvInputManagerService extends SystemService { private ServiceCallback callback; private boolean bound; private boolean reconnecting; - private boolean neverConnected; + private boolean needInit; private ServiceState(ComponentName component, int userId) { this.component = component; this.connection = new InputServiceConnection(component, userId); this.isHardware = hasHardwarePermission(mContext.getPackageManager(), component); - this.neverConnected = true; + this.needInit = true; } } @@ -3618,11 +3658,9 @@ public final class TvInputManagerService extends SystemService { } ComponentName component = mTvInputHardwareManager.getInputMap().get(inputId).getComponent(); ServiceState serviceState = getServiceStateLocked(component, userId); - boolean removed = serviceState.hardwareInputMap.remove(inputId) != null; - if (removed) { - buildTvInputListLocked(userId, null); - mTvInputHardwareManager.removeHardwareInput(inputId); - } + serviceState.hardwareInputMap.remove(inputId); + buildTvInputListLocked(userId, null); + mTvInputHardwareManager.removeHardwareInput(inputId); } private final class InputServiceConnection implements ServiceConnection { @@ -3648,7 +3686,7 @@ public final class TvInputManagerService extends SystemService { } ServiceState serviceState = userState.serviceStateMap.get(mComponent); serviceState.service = ITvInputService.Stub.asInterface(service); - serviceState.neverConnected = false; + serviceState.needInit = false; // Register a callback, if we need to. if (serviceState.isHardware && serviceState.callback == null) { @@ -3841,9 +3879,12 @@ public final class TvInputManagerService extends SystemService { final long identity = Binder.clearCallingIdentity(); try { synchronized (mLock) { - Slog.d(TAG, "ServiceCallback: removeHardwareInput, inputId: " + inputId + - " by " + mComponent + ", userId: " + mUserId); - removeHardwareInputLocked(inputId, mUserId); + if (mUserId == mCurrentUserId) { + Slog.d(TAG, + "ServiceCallback: removeHardwareInput, inputId: " + inputId + " by " + + mComponent + ", userId: " + mUserId); + removeHardwareInputLocked(inputId, mUserId); + } } } finally { Binder.restoreCallingIdentity(identity); @@ -4578,6 +4619,11 @@ public final class TvInputManagerService extends SystemService { private final class HardwareListener implements TvInputHardwareManager.Listener { @Override public void onStateChanged(String inputId, int state) { + if (DEBUG) { + Slog.d(TAG, + "onStateChanged: inputId " + (inputId != null ? inputId : "null") + + ", state " + state); + } synchronized (mLock) { setStateLocked(inputId, state, mCurrentUserId); } @@ -4585,6 +4631,11 @@ public final class TvInputManagerService extends SystemService { @Override public void onHardwareDeviceAdded(TvInputHardwareInfo info) { + if (DEBUG) { + Slog.d(TAG, + "onHardwareDeviceAdded: TvInputHardwareInfo " + + (info != null ? info.toString() : "null")); + } synchronized (mLock) { UserState userState = getOrCreateUserStateLocked(mCurrentUserId); // Broadcast the event to all hardware inputs. @@ -4607,6 +4658,11 @@ public final class TvInputManagerService extends SystemService { @Override public void onHardwareDeviceRemoved(TvInputHardwareInfo info) { + if (DEBUG) { + Slog.d(TAG, + "onHardwareDeviceRemoved: TvInputHardwareInfo " + + (info != null ? info.toString() : "null")); + } synchronized (mLock) { String relatedInputId = mTvInputHardwareManager.getHardwareInputIdMap().get(info.getDeviceId()); @@ -4634,6 +4690,11 @@ public final class TvInputManagerService extends SystemService { @Override public void onHdmiDeviceAdded(HdmiDeviceInfo deviceInfo) { + if (DEBUG) { + Slog.d(TAG, + "onHdmiDeviceAdded: HdmiDeviceInfo " + + (deviceInfo != null ? deviceInfo.toString() : "null")); + } synchronized (mLock) { UserState userState = getOrCreateUserStateLocked(mCurrentUserId); // Broadcast the event to all hardware inputs. @@ -4656,6 +4717,11 @@ public final class TvInputManagerService extends SystemService { @Override public void onHdmiDeviceRemoved(HdmiDeviceInfo deviceInfo) { + if (DEBUG) { + Slog.d(TAG, + "onHdmiDeviceRemoved: HdmiDeviceInfo " + + (deviceInfo != null ? deviceInfo.toString() : "null")); + } synchronized (mLock) { String relatedInputId = mTvInputHardwareManager.getHdmiInputIdMap().get(deviceInfo.getId()); @@ -4683,6 +4749,12 @@ public final class TvInputManagerService extends SystemService { @Override public void onHdmiDeviceUpdated(String inputId, HdmiDeviceInfo deviceInfo) { + if (DEBUG) { + Slog.d(TAG, + "onHdmiDeviceUpdated: inputId " + (inputId != null ? inputId : "null") + + ", deviceInfo: " + + (deviceInfo != null ? deviceInfo.toString() : "null")); + } synchronized (mLock) { Integer state; switch (deviceInfo.getDevicePowerStatus()) { diff --git a/services/core/java/com/android/server/uri/NeededUriGrants.java b/services/core/java/com/android/server/uri/NeededUriGrants.java index 8c8f55304fbb..2fe61e00c97e 100644 --- a/services/core/java/com/android/server/uri/NeededUriGrants.java +++ b/services/core/java/com/android/server/uri/NeededUriGrants.java @@ -17,10 +17,13 @@ package com.android.server.uri; import android.util.ArraySet; +import android.util.Slog; import android.util.proto.ProtoOutputStream; import com.android.server.am.NeededUriGrantsProto; +import java.util.Objects; + /** List of {@link GrantUri} a process needs. */ public class NeededUriGrants { final String targetPkg; @@ -35,6 +38,20 @@ public class NeededUriGrants { this.uris = new ArraySet<>(); } + public void merge(NeededUriGrants other) { + if (other == null) return; + if (!Objects.equals(this.targetPkg, other.targetPkg) + || this.targetUid != other.targetUid || this.flags != other.flags) { + Slog.wtf("NeededUriGrants", + "The other NeededUriGrants does not share the same targetUid, targetPkg or " + + "flags. It cannot be merged into this NeededUriGrants. This " + + "NeededUriGrants: " + this.toStringWithoutUri() + + ". Other NeededUriGrants: " + other.toStringWithoutUri()); + } else { + this.uris.addAll(other.uris); + } + } + public void dumpDebug(ProtoOutputStream proto, long fieldId) { long token = proto.start(fieldId); proto.write(NeededUriGrantsProto.TARGET_PACKAGE, targetPkg); @@ -47,4 +64,12 @@ public class NeededUriGrants { } proto.end(token); } + + public String toStringWithoutUri() { + return "NeededUriGrants{" + + "targetPkg='" + targetPkg + '\'' + + ", targetUid=" + targetUid + + ", flags=" + flags + + '}'; + } } diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java index ae30fcde39e1..d119a08b0c85 100644 --- a/services/core/java/com/android/server/wm/ActivityClientController.java +++ b/services/core/java/com/android/server/wm/ActivityClientController.java @@ -442,8 +442,6 @@ class ActivityClientController extends IActivityClientController.Stub { throw new IllegalArgumentException("File descriptors passed in Intent"); } - mService.mAmInternal.addCreatorToken(resultData); - final ActivityRecord r; synchronized (mGlobalLock) { r = ActivityRecord.isInRootTaskLocked(token); @@ -502,6 +500,8 @@ class ActivityClientController extends IActivityClientController.Stub { r.app.setLastActivityFinishTimeIfNeeded(SystemClock.uptimeMillis()); } + mService.mAmInternal.addCreatorToken(resultData, r.packageName); + final long origId = Binder.clearCallingIdentity(); Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "finishActivity"); try { diff --git a/services/core/java/com/android/server/wm/ActivityStartController.java b/services/core/java/com/android/server/wm/ActivityStartController.java index 0580d4a5a4a3..c1f5a27b81e7 100644 --- a/services/core/java/com/android/server/wm/ActivityStartController.java +++ b/services/core/java/com/android/server/wm/ActivityStartController.java @@ -25,6 +25,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; import static android.os.FactoryTest.FACTORY_TEST_LOW_LEVEL; +import static com.android.server.wm.ActivityStarter.Request.DEFAULT_INTENT_CREATOR_UID; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM; import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.server.wm.ActivityTaskSupervisor.ON_TOP; @@ -441,6 +442,17 @@ public class ActivityStartController { 0 /* startFlags */, null /* profilerInfo */, userId, filterCallingUid, callingPid); aInfo = mService.mAmInternal.getActivityInfoForUser(aInfo, userId); + int creatorUid = DEFAULT_INTENT_CREATOR_UID; + String creatorPackage = null; + if (ActivityManagerService.IntentCreatorToken.isValid(intent)) { + ActivityManagerService.IntentCreatorToken creatorToken = + (ActivityManagerService.IntentCreatorToken) intent.getCreatorToken(); + if (creatorToken.getCreatorUid() != filterCallingUid) { + creatorUid = creatorToken.getCreatorUid(); + creatorPackage = creatorToken.getCreatorPackage(); + } + // leave creatorUid as -1 if the intent creator is the same as the launcher + } if (aInfo != null) { try { @@ -454,6 +466,24 @@ public class ActivityStartController { return START_CANCELED; } + if (creatorUid != DEFAULT_INTENT_CREATOR_UID) { + try { + NeededUriGrants creatorIntentGrants = mSupervisor.mService.mUgmInternal + .checkGrantUriPermissionFromIntent(intent, creatorUid, + aInfo.applicationInfo.packageName, + UserHandle.getUserId(aInfo.applicationInfo.uid)); + if (intentGrants == null) { + intentGrants = creatorIntentGrants; + } else { + intentGrants.merge(creatorIntentGrants); + } + } catch (SecurityException securityException) { + ActivityStarter.logForIntentRedirect( + "Creator URI Grant Caused Exception.", intent, creatorUid, + creatorPackage, filterCallingUid, callingPackage); + // TODO b/368559093 - rethrow the securityException. + } + } if ((aInfo.applicationInfo.privateFlags & ApplicationInfo.PRIVATE_FLAG_CANT_SAVE_STATE) != 0) { throw new IllegalArgumentException( @@ -477,6 +507,8 @@ public class ActivityStartController { .setCallingUid(callingUid) .setCallingPackage(callingPackage) .setCallingFeatureId(callingFeatureId) + .setIntentCreatorUid(creatorUid) + .setIntentCreatorPackage(creatorPackage) .setRealCallingPid(realCallingPid) .setRealCallingUid(realCallingUid) .setActivityOptions(checkedOptions) diff --git a/services/core/java/com/android/server/wm/ActivityStarter.java b/services/core/java/com/android/server/wm/ActivityStarter.java index 5b5bb88cac98..5d3ae54f0934 100644 --- a/services/core/java/com/android/server/wm/ActivityStarter.java +++ b/services/core/java/com/android/server/wm/ActivityStarter.java @@ -132,6 +132,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.HeavyWeightSwitcherActivity; import com.android.internal.app.IVoiceInteractor; import com.android.internal.protolog.ProtoLog; +import com.android.server.am.ActivityManagerService.IntentCreatorToken; import com.android.server.am.PendingIntentRecord; import com.android.server.pm.InstantAppResolver; import com.android.server.pm.PackageArchiver; @@ -384,6 +385,7 @@ class ActivityStarter { private static final int DEFAULT_CALLING_PID = 0; static final int DEFAULT_REAL_CALLING_UID = -1; static final int DEFAULT_REAL_CALLING_PID = 0; + static final int DEFAULT_INTENT_CREATOR_UID = -1; IApplicationThread caller; Intent intent; @@ -404,6 +406,8 @@ class ActivityStarter { @Nullable String callingFeatureId; int realCallingPid = DEFAULT_REAL_CALLING_PID; int realCallingUid = DEFAULT_REAL_CALLING_UID; + int intentCreatorUid = DEFAULT_INTENT_CREATOR_UID; + String intentCreatorPackage; int startFlags; SafeActivityOptions activityOptions; boolean ignoreTargetSecurity; @@ -464,6 +468,8 @@ class ActivityStarter { callingPid = DEFAULT_CALLING_PID; callingUid = DEFAULT_CALLING_UID; callingPackage = null; + intentCreatorUid = DEFAULT_INTENT_CREATOR_UID; + intentCreatorPackage = null; callingFeatureId = null; realCallingPid = DEFAULT_REAL_CALLING_PID; realCallingUid = DEFAULT_REAL_CALLING_UID; @@ -556,12 +562,14 @@ class ActivityStarter { // "resolved" calling UID, where we try our best to identify the // actual caller that is starting this activity int resolvedCallingUid = callingUid; + String resolvedCallingPackage = callingPackage; if (caller != null) { synchronized (supervisor.mService.mGlobalLock) { final WindowProcessController callerApp = supervisor.mService .getProcessController(caller); if (callerApp != null) { resolvedCallingUid = callerApp.mInfo.uid; + resolvedCallingPackage = callerApp.mInfo.packageName; } } } @@ -597,7 +605,23 @@ class ActivityStarter { // Collect information about the target of the Intent. activityInfo = supervisor.resolveActivity(intent, resolveInfo, startFlags, profilerInfo); - + // Check if the Intent was redirected + if ((intent.getExtendedFlags() & Intent.EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN) + != 0) { + ActivityStarter.logForIntentRedirect( + "Unparceled intent does not have a creator token set.", intent, + intentCreatorUid, + intentCreatorPackage, resolvedCallingUid, resolvedCallingPackage); + // TODO b/368559093 - eventually ramp up to throw SecurityException + } + if (IntentCreatorToken.isValid(intent)) { + IntentCreatorToken creatorToken = (IntentCreatorToken) intent.getCreatorToken(); + if (creatorToken.getCreatorUid() != resolvedCallingUid) { + intentCreatorUid = creatorToken.getCreatorUid(); + intentCreatorPackage = creatorToken.getCreatorPackage(); + } + // leave intentCreatorUid as -1 if the intent creator is the same as the launcher + } // Carefully collect grants without holding lock if (activityInfo != null) { if (android.security.Flags.contentUriPermissionApis()) { @@ -607,11 +631,52 @@ class ActivityStarter { UserHandle.getUserId(activityInfo.applicationInfo.uid), activityInfo.requireContentUriPermissionFromCaller, /* requestHashCode */ this.hashCode()); + if (intentCreatorUid != DEFAULT_INTENT_CREATOR_UID) { + try { + NeededUriGrants creatorIntentGrants = supervisor.mService.mUgmInternal + .checkGrantUriPermissionFromIntent(intent, intentCreatorUid, + activityInfo.applicationInfo.packageName, + UserHandle.getUserId(activityInfo.applicationInfo.uid), + activityInfo.requireContentUriPermissionFromCaller, + /* requestHashCode */ this.hashCode()); + if (intentGrants == null) { + intentGrants = creatorIntentGrants; + } else { + intentGrants.merge(creatorIntentGrants); + } + } catch (SecurityException securityException) { + ActivityStarter.logForIntentRedirect( + "Creator URI Grant Caused Exception.", intent, intentCreatorUid, + intentCreatorPackage, resolvedCallingUid, + resolvedCallingPackage); + // TODO b/368559093 - rethrow the securityException. + } + } } else { intentGrants = supervisor.mService.mUgmInternal .checkGrantUriPermissionFromIntent(intent, resolvedCallingUid, activityInfo.applicationInfo.packageName, UserHandle.getUserId(activityInfo.applicationInfo.uid)); + if (intentCreatorUid != DEFAULT_INTENT_CREATOR_UID && intentGrants != null) { + try { + NeededUriGrants creatorIntentGrants = supervisor.mService.mUgmInternal + .checkGrantUriPermissionFromIntent(intent, intentCreatorUid, + activityInfo.applicationInfo.packageName, + UserHandle.getUserId( + activityInfo.applicationInfo.uid)); + if (intentGrants == null) { + intentGrants = creatorIntentGrants; + } else { + intentGrants.merge(creatorIntentGrants); + } + } catch (SecurityException securityException) { + ActivityStarter.logForIntentRedirect( + "Creator URI Grant Caused Exception.", intent, intentCreatorUid, + intentCreatorPackage, resolvedCallingUid, + resolvedCallingPackage); + // TODO b/368559093 - rethrow the securityException. + } + } } } } @@ -978,7 +1043,9 @@ class ActivityStarter { int requestCode = request.requestCode; int callingPid = request.callingPid; int callingUid = request.callingUid; - String callingPackage = request.callingPackage; + int intentCreatorUid = request.intentCreatorUid; + String intentCreatorPackage = request.intentCreatorPackage; + String intentCallingPackage = request.callingPackage; String callingFeatureId = request.callingFeatureId; final int realCallingPid = request.realCallingPid; final int realCallingUid = request.realCallingUid; @@ -1063,7 +1130,7 @@ class ActivityStarter { // launched in the app flow to redirect to an activity picked by the user, where // we want the final activity to consider it to have been launched by the // previous app activity. - callingPackage = sourceRecord.launchedFromPackage; + intentCallingPackage = sourceRecord.launchedFromPackage; callingFeatureId = sourceRecord.launchedFromFeatureId; } } @@ -1085,7 +1152,7 @@ class ActivityStarter { if (packageArchiver.isIntentResolvedToArchivedApp(intent, mRequest.userId)) { err = packageArchiver .requestUnarchiveOnActivityStart( - intent, callingPackage, mRequest.userId, realCallingUid); + intent, intentCallingPackage, mRequest.userId, realCallingUid); } } } @@ -1144,7 +1211,7 @@ class ActivityStarter { boolean abort; try { abort = !mSupervisor.checkStartAnyActivityPermission(intent, aInfo, resultWho, - requestCode, callingPid, callingUid, callingPackage, callingFeatureId, + requestCode, callingPid, callingUid, intentCallingPackage, callingFeatureId, request.ignoreTargetSecurity, inTask != null, callerApp, resultRecord, resultRootTask); } catch (SecurityException e) { @@ -1172,7 +1239,47 @@ class ActivityStarter { abort |= !mService.mIntentFirewall.checkStartActivity(intent, callingUid, callingPid, resolvedType, aInfo.applicationInfo); abort |= !mService.getPermissionPolicyInternal().checkStartActivity(intent, callingUid, - callingPackage); + intentCallingPackage); + + if (intentCreatorUid != Request.DEFAULT_INTENT_CREATOR_UID) { + try { + if (!mSupervisor.checkStartAnyActivityPermission(intent, aInfo, resultWho, + requestCode, 0, intentCreatorUid, intentCreatorPackage, "", + request.ignoreTargetSecurity, inTask != null, null, resultRecord, + resultRootTask)) { + logForIntentRedirect("Creator checkStartAnyActivityPermission Caused abortion.", + intent, intentCreatorUid, intentCreatorPackage, callingUid, + intentCallingPackage); + // TODO b/368559093 - set abort to true. + // abort = true; + } + } catch (SecurityException e) { + logForIntentRedirect("Creator checkStartAnyActivityPermission Caused Exception.", + intent, intentCreatorUid, intentCreatorPackage, callingUid, + intentCallingPackage); + // TODO b/368559093 - rethrow the exception. + //throw e; + } + if (!mService.mIntentFirewall.checkStartActivity(intent, intentCreatorUid, 0, + resolvedType, aInfo.applicationInfo)) { + logForIntentRedirect("Creator IntentFirewall.checkStartActivity Caused abortion.", + intent, intentCreatorUid, intentCreatorPackage, callingUid, + intentCallingPackage); + // TODO b/368559093 - set abort to true. + // abort = true; + } + + if (!mService.getPermissionPolicyInternal().checkStartActivity(intent, intentCreatorUid, + intentCreatorPackage)) { + logForIntentRedirect( + "Creator PermissionPolicyService.checkStartActivity Caused abortion.", + intent, intentCreatorUid, intentCreatorPackage, callingUid, + intentCallingPackage); + // TODO b/368559093 - set abort to true. + // abort = true; + } + intent.removeCreatorTokenInfo(); + } // Merge the two options bundles, while realCallerOptions takes precedence. ActivityOptions checkedOptions = options != null @@ -1189,7 +1296,7 @@ class ActivityStarter { balController.checkBackgroundActivityStart( callingUid, callingPid, - callingPackage, + intentCallingPackage, realCallingUid, realCallingPid, callerApp, @@ -1210,7 +1317,7 @@ class ActivityStarter { if (request.allowPendingRemoteAnimationRegistryLookup) { checkedOptions = mService.getActivityStartController() .getPendingRemoteAnimationRegistry() - .overrideOptionsIfNeeded(callingPackage, checkedOptions); + .overrideOptionsIfNeeded(intentCallingPackage, checkedOptions); } if (mService.mController != null) { try { @@ -1226,7 +1333,8 @@ class ActivityStarter { final TaskDisplayArea suggestedLaunchDisplayArea = computeSuggestedLaunchDisplayArea(inTask, sourceRecord, checkedOptions); - mInterceptor.setStates(userId, realCallingPid, realCallingUid, startFlags, callingPackage, + mInterceptor.setStates(userId, realCallingPid, realCallingUid, startFlags, + intentCallingPackage, callingFeatureId); if (mInterceptor.intercept(intent, rInfo, aInfo, resolvedType, inTask, inTaskFragment, callingPid, callingUid, checkedOptions, suggestedLaunchDisplayArea)) { @@ -1264,7 +1372,8 @@ class ActivityStarter { if (mService.getPackageManagerInternalLocked().isPermissionsReviewRequired( aInfo.packageName, userId)) { final IIntentSender target = mService.getIntentSenderLocked( - ActivityManager.INTENT_SENDER_ACTIVITY, callingPackage, callingFeatureId, + ActivityManager.INTENT_SENDER_ACTIVITY, intentCallingPackage, + callingFeatureId, callingUid, userId, null, null, 0, new Intent[]{intent}, new String[]{resolvedType}, PendingIntent.FLAG_CANCEL_CURRENT | PendingIntent.FLAG_ONE_SHOT, null); @@ -1327,7 +1436,8 @@ class ActivityStarter { // app [on install success]. if (rInfo != null && rInfo.auxiliaryInfo != null) { intent = createLaunchIntent(rInfo.auxiliaryInfo, request.ephemeralIntent, - callingPackage, callingFeatureId, verificationBundle, resolvedType, userId); + intentCallingPackage, callingFeatureId, verificationBundle, resolvedType, + userId); resolvedType = null; callingUid = realCallingUid; callingPid = realCallingPid; @@ -1350,7 +1460,7 @@ class ActivityStarter { .setCaller(callerApp) .setLaunchedFromPid(callingPid) .setLaunchedFromUid(callingUid) - .setLaunchedFromPackage(callingPackage) + .setLaunchedFromPackage(intentCallingPackage) .setLaunchedFromFeature(callingFeatureId) .setIntent(intent) .setResolvedType(resolvedType) @@ -3308,6 +3418,16 @@ class ActivityStarter { return this; } + ActivityStarter setIntentCreatorUid(int uid) { + mRequest.intentCreatorUid = uid; + return this; + } + + ActivityStarter setIntentCreatorPackage(String intentCreatorPackage) { + mRequest.intentCreatorPackage = intentCreatorPackage; + return this; + } + /** * Sets the pid of the caller who requested to launch the activity. * @@ -3467,4 +3587,19 @@ class ActivityStarter { pw.print(" mInTaskFragment="); pw.println(mInTaskFragment); } + + static void logForIntentRedirect(String message, Intent intent, int intentCreatorUid, + String intentCreatorPackage, int callingUid, String callingPackage) { + String msg = getIntentRedirectPreventedLogMessage(message, intent, intentCreatorUid, + intentCreatorPackage, callingUid, callingPackage); + Slog.wtf(TAG, msg); + } + + private static String getIntentRedirectPreventedLogMessage(String message, Intent intent, + int intentCreatorUid, String intentCreatorPackage, int callingUid, + String callingPackage) { + return "[IntentRedirect]" + message + " intentCreatorUid: " + intentCreatorUid + + "; intentCreatorPackage: " + intentCreatorPackage + "; callingUid: " + callingUid + + "; callingPackage: " + callingPackage + "; intent: " + intent; + } } diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 4db478a13c92..5339753624d8 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -1228,7 +1228,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { String callingFeatureId, Intent intent, String resolvedType, IBinder resultTo, String resultWho, int requestCode, int startFlags, ProfilerInfo profilerInfo, Bundle bOptions) { - mAmInternal.addCreatorToken(intent); + mAmInternal.addCreatorToken(intent, callingPackage); return startActivityAsUser(caller, callingPackage, callingFeatureId, intent, resolvedType, resultTo, resultWho, requestCode, startFlags, profilerInfo, bOptions, UserHandle.getCallingUserId()); @@ -1243,7 +1243,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { enforceNotIsolatedCaller(reason); if (intents != null) { for (Intent intent : intents) { - mAmInternal.addCreatorToken(intent); + mAmInternal.addCreatorToken(intent, callingPackage); } } userId = handleIncomingUser(Binder.getCallingPid(), Binder.getCallingUid(), userId, reason); @@ -1275,7 +1275,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { @Nullable String callingFeatureId, Intent intent, String resolvedType, IBinder resultTo, String resultWho, int requestCode, int startFlags, ProfilerInfo profilerInfo, Bundle bOptions, int userId, boolean validateIncomingUser) { - mAmInternal.addCreatorToken(intent); + mAmInternal.addCreatorToken(intent, callingPackage); final SafeActivityOptions opts = SafeActivityOptions.fromBundle(bOptions); assertPackageMatchesCallingUid(callingPackage); @@ -1330,7 +1330,6 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { } // Remove existing mismatch flag so it can be properly updated later fillInIntent.removeExtendedFlags(Intent.EXTENDED_FLAG_FILTER_MISMATCH); - mAmInternal.addCreatorToken(fillInIntent); } if (!(target instanceof PendingIntentRecord)) { @@ -1339,6 +1338,10 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { PendingIntentRecord pir = (PendingIntentRecord) target; + if (fillInIntent != null) { + mAmInternal.addCreatorToken(fillInIntent, pir.getPackageName()); + } + synchronized (mGlobalLock) { // If this is coming from the currently resumed activity, it is // effectively saying that app switches are allowed at this point. @@ -1349,6 +1352,7 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { mAppSwitchesState = APP_SWITCH_ALLOW; } } + return pir.sendInner(caller, 0, fillInIntent, resolvedType, allowlistToken, null, null, resultTo, resultWho, requestCode, flagsMask, flagsValues, bOptions); } @@ -1361,8 +1365,6 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { throw new IllegalArgumentException("File descriptors passed in Intent"); } - mAmInternal.addCreatorToken(intent); - SafeActivityOptions options = SafeActivityOptions.fromBundle(bOptions); synchronized (mGlobalLock) { @@ -1376,6 +1378,9 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { SafeActivityOptions.abort(options); return false; } + + mAmInternal.addCreatorToken(intent, r.packageName); + intent = new Intent(intent); // Remove existing mismatch flag so it can be properly updated later intent.removeExtendedFlags(Intent.EXTENDED_FLAG_FILTER_MISMATCH); diff --git a/services/core/java/com/android/server/wm/AppTaskImpl.java b/services/core/java/com/android/server/wm/AppTaskImpl.java index 4d17ed24e734..eee4c86bc483 100644 --- a/services/core/java/com/android/server/wm/AppTaskImpl.java +++ b/services/core/java/com/android/server/wm/AppTaskImpl.java @@ -160,7 +160,7 @@ class AppTaskImpl extends IAppTask.Stub { Intent intent, String resolvedType, Bundle bOptions) { checkCallerOrSystemOrRoot(); mService.assertPackageMatchesCallingUid(callingPackage); - mService.mAmInternal.addCreatorToken(intent); + mService.mAmInternal.addCreatorToken(intent, callingPackage); int callingUser = UserHandle.getCallingUserId(); Task task; diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index 94cd2e64b057..70f9ebb0e61e 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -34,6 +34,7 @@ import static com.android.server.wm.BackNavigationProto.LAST_BACK_TYPE; import static com.android.server.wm.BackNavigationProto.MAIN_OPEN_ACTIVITY; import static com.android.server.wm.BackNavigationProto.SHOW_WALLPAPER; import static com.android.server.wm.SurfaceAnimator.ANIMATION_TYPE_PREDICT_BACK; +import static com.android.server.wm.WindowContainer.SYNC_STATE_NONE; import android.annotation.NonNull; import android.annotation.Nullable; @@ -1237,8 +1238,9 @@ class BackNavigationController { } allWindowDrawn &= next.mAppWindowDrawn; } - // Do not remove until transition ready. - if (!activity.isVisible()) { + // Do not remove windowless surfaces if the transaction has not been applied. + if (activity.getSyncTransactionCommitCallbackDepth() > 0 + || activity.mSyncState != SYNC_STATE_NONE) { return; } if (allWindowDrawn) { diff --git a/services/core/java/com/android/server/wm/InputManagerCallback.java b/services/core/java/com/android/server/wm/InputManagerCallback.java index a4fe0647ea79..9d21183c6c03 100644 --- a/services/core/java/com/android/server/wm/InputManagerCallback.java +++ b/services/core/java/com/android/server/wm/InputManagerCallback.java @@ -131,7 +131,9 @@ final class InputManagerCallback implements InputManagerService.WindowManagerCal final boolean changed = !com.android.window.flags.Flags.filterIrrelevantInputDeviceChange() || updateLastInputConfigurationSources(); - if (changed) { + // Even if the input devices are not changed, there could be other pending changes + // during booting. It's fine to apply earlier. + if (changed || !mService.mDisplayEnabled) { synchronized (mService.mGlobalLock) { Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "inputDeviceConfigChanged"); mService.mRoot.forAllDisplays(DisplayContent::sendNewConfiguration); diff --git a/services/core/java/com/android/server/wm/InsetsSourceProvider.java b/services/core/java/com/android/server/wm/InsetsSourceProvider.java index 6067a9972bbe..1d4d6eb82c44 100644 --- a/services/core/java/com/android/server/wm/InsetsSourceProvider.java +++ b/services/core/java/com/android/server/wm/InsetsSourceProvider.java @@ -409,7 +409,7 @@ class InsetsSourceProvider { } final Point position = getWindowFrameSurfacePosition(); if (!mPosition.equals(position)) { - mPosition.set(position.x, position.y); + mPosition.set(position); if (windowState != null && windowState.getWindowFrames().didFrameSizeChange() && windowState.mWinAnimator.getShown() && mWindowContainer.okToDisplay()) { mHasPendingPosition = true; @@ -553,6 +553,7 @@ class InsetsSourceProvider { } boolean initiallyVisible = mClientVisible; final Point surfacePosition = getWindowFrameSurfacePosition(); + mPosition.set(surfacePosition); mAdapter = new ControlAdapter(surfacePosition); if (mSource.getType() == WindowInsets.Type.ime()) { if (android.view.inputmethod.Flags.refactorInsetsController()) { diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index a4e4deb9ed7d..4861341f830a 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -5742,9 +5742,9 @@ class Task extends TaskFragment { } private boolean canMoveTaskToBack(Task task) { - // Checks whether a task is a child of this task because it can be reparetned when + // Checks whether a task is a child of this task because it can be reparented when // transition is deferred. - if (task != this && task.getParent() != this) { + if (task != this && !task.isDescendantOf(this)) { return false; } diff --git a/services/core/java/com/android/server/wm/WindowProcessController.java b/services/core/java/com/android/server/wm/WindowProcessController.java index 86adc1944371..1a107c24a16a 100644 --- a/services/core/java/com/android/server/wm/WindowProcessController.java +++ b/services/core/java/com/android/server/wm/WindowProcessController.java @@ -133,7 +133,7 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio private int mRapidActivityLaunchCount; // all about the first app in the process - final ApplicationInfo mInfo; + volatile ApplicationInfo mInfo; final String mName; final int mUid; @@ -1805,12 +1805,17 @@ public class WindowProcessController extends ConfigurationContainer<Configuratio Configuration overrideConfig = new Configuration(r.getRequestedOverrideConfiguration()); overrideConfig.assetsSeq = assetSeq; r.onRequestedOverrideConfigurationChanged(overrideConfig); + r.updateApplicationInfo(mInfo); if (r.isVisibleRequested()) { r.ensureActivityConfiguration(); } } } + public void updateApplicationInfo(ApplicationInfo aInfo) { + mInfo = aInfo; + } + /** * This is called for sending {@link android.app.servertransaction.LaunchActivityItem}. * The caller must call {@link #setLastReportedConfiguration} if the delivered configuration diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java index b9727f9f3970..4103c477a8ae 100644 --- a/services/java/com/android/server/SystemServer.java +++ b/services/java/com/android/server/SystemServer.java @@ -327,8 +327,6 @@ public final class SystemServer implements Dumpable { * Implementation class names for services in the {@code SYSTEMSERVERCLASSPATH} * from {@code PRODUCT_SYSTEM_SERVER_JARS} that are *not* in {@code services.jar}. */ - private static final String ARC_NETWORK_SERVICE_CLASS = - "com.android.server.arc.net.ArcNetworkService"; private static final String ARC_PERSISTENT_DATA_BLOCK_SERVICE_CLASS = "com.android.server.arc.persistent_data_block.ArcPersistentDataBlockService"; private static final String ARC_SYSTEM_HEALTH_SERVICE = @@ -2101,24 +2099,13 @@ public final class SystemServer implements Dumpable { if (context.getPackageManager().hasSystemFeature( PackageManager.FEATURE_WIFI)) { // Wifi Service must be started first for wifi-related services. - if (!isArc) { - t.traceBegin("StartWifi"); - mSystemServiceManager.startServiceFromJar( - WIFI_SERVICE_CLASS, WIFI_APEX_SERVICE_JAR_PATH); - t.traceEnd(); - t.traceBegin("StartWifiScanning"); - mSystemServiceManager.startServiceFromJar( - WIFI_SCANNING_SERVICE_CLASS, WIFI_APEX_SERVICE_JAR_PATH); - t.traceEnd(); - } - } - - // ARC - ArcNetworkService registers the ARC network stack and replaces the - // stock WiFi service in both ARC++ container and ARCVM. Always starts the ARC network - // stack regardless of whether FEATURE_WIFI is enabled/disabled (b/254755875). - if (isArc) { - t.traceBegin("StartArcNetworking"); - mSystemServiceManager.startService(ARC_NETWORK_SERVICE_CLASS); + t.traceBegin("StartWifi"); + mSystemServiceManager.startServiceFromJar( + WIFI_SERVICE_CLASS, WIFI_APEX_SERVICE_JAR_PATH); + t.traceEnd(); + t.traceBegin("StartWifiScanning"); + mSystemServiceManager.startServiceFromJar( + WIFI_SCANNING_SERVICE_CLASS, WIFI_APEX_SERVICE_JAR_PATH); t.traceEnd(); } diff --git a/services/people/java/com/android/server/people/data/CallLogQueryHelper.java b/services/people/java/com/android/server/people/data/CallLogQueryHelper.java index ff901af3defa..30df4c821134 100644 --- a/services/people/java/com/android/server/people/data/CallLogQueryHelper.java +++ b/services/people/java/com/android/server/people/data/CallLogQueryHelper.java @@ -96,6 +96,8 @@ class CallLogQueryHelper { } catch (SecurityException ex) { Slog.e(TAG, "Query call log failed: " + ex); return false; + } catch (Exception e) { + Slog.e(TAG, "Exception when querying call log.", e); } return hasResults; } diff --git a/services/people/java/com/android/server/people/data/ContactsQueryHelper.java b/services/people/java/com/android/server/people/data/ContactsQueryHelper.java index 2505abf2d160..2bd9d87b0124 100644 --- a/services/people/java/com/android/server/people/data/ContactsQueryHelper.java +++ b/services/people/java/com/android/server/people/data/ContactsQueryHelper.java @@ -151,9 +151,11 @@ class ContactsQueryHelper { found = true; } } catch (SQLiteException exception) { - Slog.w("SQLite exception when querying contacts.", exception); + Slog.w(TAG, "SQLite exception when querying contacts.", exception); } catch (IllegalArgumentException exception) { - Slog.w("Illegal Argument exception when querying contacts.", exception); + Slog.w(TAG, "Illegal Argument exception when querying contacts.", exception); + } catch (Exception exception) { + Slog.e(TAG, "Exception when querying contacts.", exception); } if (found && lookupKey != null && hasPhoneNumber) { return queryPhoneNumber(lookupKey); @@ -181,6 +183,8 @@ class ContactsQueryHelper { mPhoneNumber = cursor.getString(phoneNumIdx); } } + } catch (Exception exception) { + Slog.e(TAG, "Exception when querying contact phone number.", exception); } return true; } diff --git a/services/people/java/com/android/server/people/data/MmsQueryHelper.java b/services/people/java/com/android/server/people/data/MmsQueryHelper.java index 39dba9c73ba2..414a523fb186 100644 --- a/services/people/java/com/android/server/people/data/MmsQueryHelper.java +++ b/services/people/java/com/android/server/people/data/MmsQueryHelper.java @@ -100,6 +100,8 @@ class MmsQueryHelper { } } } + } catch (Exception e) { + Slog.e(TAG, "Exception when querying MMS table.", e); } finally { Binder.defaultBlockingForCurrentThread(); } @@ -133,6 +135,8 @@ class MmsQueryHelper { address = cursor.getString(addrIndex); } } + } catch (Exception e) { + Slog.e(TAG, "Exception when querying MMS address table.", e); } if (!Mms.isPhoneNumber(address)) { return null; diff --git a/services/people/java/com/android/server/people/data/SmsQueryHelper.java b/services/people/java/com/android/server/people/data/SmsQueryHelper.java index a5eb3a581616..f8ff3abc8e4c 100644 --- a/services/people/java/com/android/server/people/data/SmsQueryHelper.java +++ b/services/people/java/com/android/server/people/data/SmsQueryHelper.java @@ -98,6 +98,8 @@ class SmsQueryHelper { } } } + } catch (Exception e) { + Slog.e(TAG, "Exception when querying SMS table.", e); } finally { Binder.defaultBlockingForCurrentThread(); } diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.kt b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.kt index cbca434a6bb6..8af5b2081b81 100644 --- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.kt +++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/PackageInstallerSessionTest.kt @@ -21,6 +21,7 @@ import android.content.pm.PackageInstaller.SessionParams import android.content.pm.PackageInstaller.SessionParams.PERMISSION_STATE_DEFAULT import android.content.pm.PackageInstaller.SessionParams.PERMISSION_STATE_DENIED import android.content.pm.PackageInstaller.SessionParams.PERMISSION_STATE_GRANTED +import android.content.pm.PackageInstaller.VERIFICATION_POLICY_BLOCK_FAIL_CLOSED import android.content.pm.PackageManager import android.content.pm.verify.domain.DomainSet import android.os.Parcel @@ -33,6 +34,11 @@ import com.android.internal.os.BackgroundThread import com.android.server.pm.verify.pkg.VerifierController import com.android.server.testutils.whenever import com.google.common.truth.Truth.assertThat +import java.io.File +import java.io.FileInputStream +import java.io.FileNotFoundException +import java.io.FileOutputStream +import java.io.IOException import libcore.io.IoUtils import org.junit.Before import org.junit.Rule @@ -46,11 +52,6 @@ import org.mockito.Mockito.mock import org.mockito.MockitoAnnotations import org.xmlpull.v1.XmlPullParser import org.xmlpull.v1.XmlPullParserException -import java.io.File -import java.io.FileInputStream -import java.io.FileNotFoundException -import java.io.FileOutputStream -import java.io.IOException @Presubmit class PackageInstallerSessionTest { @@ -197,7 +198,8 @@ class PackageInstallerSessionTest { /* stagedSessionErrorCode */ PackageManager.INSTALL_FAILED_VERIFICATION_FAILURE, /* stagedSessionErrorMessage */ "some error", /* preVerifiedDomains */ DomainSet(setOf("com.foo", "com.bar")), - /* VerifierController */ mock(VerifierController::class.java) + /* VerifierController */ mock(VerifierController::class.java), + VERIFICATION_POLICY_BLOCK_FAIL_CLOSED ) } @@ -339,6 +341,7 @@ class PackageInstallerSessionTest { assertThat(expected.childSessionIds).asList() .containsExactlyElementsIn(actual.childSessionIds.toList()) assertThat(expected.preVerifiedDomains).isEqualTo(actual.preVerifiedDomains) + assertThat(expected.verificationPolicy).isEqualTo(actual.verificationPolicy) } private fun assertInstallSourcesEquivalent(expected: InstallSource, actual: InstallSource) { diff --git a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/verify/pkg/VerifierControllerTest.java b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/verify/pkg/VerifierControllerTest.java index be094b0152bc..37b23b107ecd 100644 --- a/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/verify/pkg/VerifierControllerTest.java +++ b/services/tests/PackageManagerServiceTests/server/src/com/android/server/pm/verify/pkg/VerifierControllerTest.java @@ -16,6 +16,9 @@ package com.android.server.pm.verify.pkg; +import static android.content.pm.PackageInstaller.VERIFICATION_POLICY_BLOCK_FAIL_CLOSED; +import static android.content.pm.PackageInstaller.VERIFICATION_POLICY_BLOCK_FAIL_OPEN; + import static com.google.common.truth.Truth.assertThat; import static org.mockito.ArgumentMatchers.any; @@ -92,6 +95,9 @@ public class VerifierControllerTest { private static final long TEST_TIMEOUT_DURATION_MILLIS = TimeUnit.MINUTES.toMillis(1); private static final long TEST_MAX_TIMEOUT_DURATION_MILLIS = TimeUnit.MINUTES.toMillis(10); + private static final long TEST_VERIFIER_CONNECTION_TIMEOUT_DURATION_MILLIS = + TimeUnit.SECONDS.toMillis(10); + private static final int TEST_POLICY = VERIFICATION_POLICY_BLOCK_FAIL_CLOSED; private final ArrayList<SharedLibraryInfo> mTestDeclaredLibraries = new ArrayList<>(); private final PersistableBundle mTestExtensionParams = new PersistableBundle(); @@ -124,6 +130,9 @@ public class VerifierControllerTest { TEST_TIMEOUT_DURATION_MILLIS); when(mInjector.getMaxVerificationExtendedTimeoutMillis()).thenReturn( TEST_MAX_TIMEOUT_DURATION_MILLIS); + when(mInjector.getVerifierConnectionTimeoutMillis()).thenReturn( + TEST_VERIFIER_CONNECTION_TIMEOUT_DURATION_MILLIS + ); // Mock time forward as the code continues to check for the current time when(mInjector.getCurrentTimeMillis()) .thenReturn(TEST_REQUEST_START_TIME) @@ -159,12 +168,12 @@ public class VerifierControllerTest { .isFalse(); assertThat(mVerifierController.startVerificationSession( mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI, - TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback, - /* retry= */ false)).isFalse(); + TEST_SIGNING_INFO, mTestDeclaredLibraries, TEST_POLICY, mTestExtensionParams, + mSessionCallback, /* retry= */ false)).isFalse(); assertThat(mVerifierController.startVerificationSession( mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI, - TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback, - /* retry= */ true)).isFalse(); + TEST_SIGNING_INFO, mTestDeclaredLibraries, TEST_POLICY, mTestExtensionParams, + mSessionCallback, /* retry= */ true)).isFalse(); verifyZeroInteractions(mSessionCallback); } @@ -200,8 +209,8 @@ public class VerifierControllerTest { ServiceConnector.ServiceLifecycleCallbacks<IVerifierService> callbacks = captor.getValue(); assertThat(mVerifierController.startVerificationSession( mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI, - TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback, - /* retry= */ false)).isTrue(); + TEST_SIGNING_INFO, mTestDeclaredLibraries, TEST_POLICY, mTestExtensionParams, + mSessionCallback, /* retry= */ false)).isTrue(); verify(mMockService, times(1)).onVerificationRequired(any(VerificationSession.class)); callbacks.onBinderDied(); // Test that nothing crashes if the service connection is lost @@ -212,12 +221,12 @@ public class VerifierControllerTest { verifyNoMoreInteractions(mMockService); assertThat(mVerifierController.startVerificationSession( mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI, - TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback, - /* retry= */ false)).isTrue(); + TEST_SIGNING_INFO, mTestDeclaredLibraries, TEST_POLICY, mTestExtensionParams, + mSessionCallback, /* retry= */ false)).isTrue(); assertThat(mVerifierController.startVerificationSession( mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI, - TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback, - /* retry= */ true)).isTrue(); + TEST_SIGNING_INFO, mTestDeclaredLibraries, TEST_POLICY, mTestExtensionParams, + mSessionCallback, /* retry= */ true)).isTrue(); mVerifierController.notifyVerificationTimeout(TEST_ID); verify(mMockService, times(1)).onVerificationTimeout(eq(TEST_ID)); } @@ -226,8 +235,8 @@ public class VerifierControllerTest { public void testNotifyPackageNameAvailable() throws Exception { assertThat(mVerifierController.startVerificationSession( mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI, - TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback, - /* retry= */ false)).isTrue(); + TEST_SIGNING_INFO, mTestDeclaredLibraries, TEST_POLICY, mTestExtensionParams, + mSessionCallback, /* retry= */ false)).isTrue(); mVerifierController.notifyPackageNameAvailable(TEST_PACKAGE_NAME); verify(mMockService).onPackageNameAvailable(eq(TEST_PACKAGE_NAME)); } @@ -236,8 +245,8 @@ public class VerifierControllerTest { public void testNotifyVerificationCancelled() throws Exception { assertThat(mVerifierController.startVerificationSession( mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI, - TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback, - /* retry= */ false)).isTrue(); + TEST_SIGNING_INFO, mTestDeclaredLibraries, TEST_POLICY, mTestExtensionParams, + mSessionCallback, /* retry= */ false)).isTrue(); mVerifierController.notifyVerificationCancelled(TEST_PACKAGE_NAME); verify(mMockService).onVerificationCancelled(eq(TEST_PACKAGE_NAME)); } @@ -248,8 +257,8 @@ public class VerifierControllerTest { ArgumentCaptor.forClass(VerificationSession.class); assertThat(mVerifierController.startVerificationSession( mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI, - TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback, - /* retry= */ false)).isTrue(); + TEST_SIGNING_INFO, mTestDeclaredLibraries, TEST_POLICY, mTestExtensionParams, + mSessionCallback, /* retry= */ false)).isTrue(); verify(mMockService).onVerificationRequired(captor.capture()); VerificationSession session = captor.getValue(); assertThat(session.getId()).isEqualTo(TEST_ID); @@ -276,8 +285,8 @@ public class VerifierControllerTest { ArgumentCaptor.forClass(VerificationSession.class); assertThat(mVerifierController.startVerificationSession( mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI, - TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback, - /* retry= */ true)).isTrue(); + TEST_SIGNING_INFO, mTestDeclaredLibraries, TEST_POLICY, mTestExtensionParams, + mSessionCallback, /* retry= */ true)).isTrue(); verify(mMockService).onVerificationRetry(captor.capture()); VerificationSession session = captor.getValue(); assertThat(session.getId()).isEqualTo(TEST_ID); @@ -302,8 +311,8 @@ public class VerifierControllerTest { public void testNotifyVerificationTimeout() throws Exception { assertThat(mVerifierController.startVerificationSession( mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI, - TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback, - /* retry= */ true)).isTrue(); + TEST_SIGNING_INFO, mTestDeclaredLibraries, TEST_POLICY, mTestExtensionParams, + mSessionCallback, /* retry= */ true)).isTrue(); mVerifierController.notifyVerificationTimeout(TEST_ID); verify(mMockService).onVerificationTimeout(eq(TEST_ID)); } @@ -320,8 +329,8 @@ public class VerifierControllerTest { ArgumentCaptor<Message> messageCaptor = ArgumentCaptor.forClass(Message.class); assertThat(mVerifierController.startVerificationSession( mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI, - TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback, - /* retry= */ false)).isTrue(); + TEST_SIGNING_INFO, mTestDeclaredLibraries, TEST_POLICY, mTestExtensionParams, + mSessionCallback, /* retry= */ false)).isTrue(); verify(mHandler, times(1)).sendMessageAtTime(any(Message.class), anyLong()); verify(mSessionCallback, times(1)).onTimeout(); verify(mInjector, times(2)).getCurrentTimeMillis(); @@ -339,8 +348,8 @@ public class VerifierControllerTest { .thenAnswer(i -> true); assertThat(mVerifierController.startVerificationSession( mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI, - TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback, - /* retry= */ false)).isTrue(); + TEST_SIGNING_INFO, mTestDeclaredLibraries, TEST_POLICY, mTestExtensionParams, + mSessionCallback, /* retry= */ false)).isTrue(); verify(mHandler, times(1)).sendMessageAtTime(any(Message.class), anyLong()); verify(mSessionCallback, times(1)).onTimeout(); verify(mInjector, times(2)).getCurrentTimeMillis(); @@ -350,8 +359,8 @@ public class VerifierControllerTest { ArgumentCaptor.forClass(VerificationSession.class); assertThat(mVerifierController.startVerificationSession( mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI, - TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback, - /* retry= */ true)).isTrue(); + TEST_SIGNING_INFO, mTestDeclaredLibraries, TEST_POLICY, mTestExtensionParams, + mSessionCallback, /* retry= */ true)).isTrue(); verify(mMockService).onVerificationRetry(captor.capture()); VerificationSession session = captor.getValue(); VerificationStatus status = new VerificationStatus.Builder().setVerified(true).build(); @@ -367,8 +376,8 @@ public class VerifierControllerTest { ArgumentCaptor.forClass(VerificationSession.class); assertThat(mVerifierController.startVerificationSession( mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI, - TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback, - /* retry= */ false)).isTrue(); + TEST_SIGNING_INFO, mTestDeclaredLibraries, TEST_POLICY, mTestExtensionParams, + mSessionCallback, /* retry= */ false)).isTrue(); verify(mMockService).onVerificationRequired(captor.capture()); VerificationSession session = captor.getValue(); session.reportVerificationIncomplete(VerificationSession.VERIFICATION_INCOMPLETE_UNKNOWN); @@ -383,8 +392,8 @@ public class VerifierControllerTest { ArgumentCaptor.forClass(VerificationSession.class); assertThat(mVerifierController.startVerificationSession( mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI, - TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback, - /* retry= */ false)).isTrue(); + TEST_SIGNING_INFO, mTestDeclaredLibraries, TEST_POLICY, mTestExtensionParams, + mSessionCallback, /* retry= */ false)).isTrue(); verify(mMockService).onVerificationRequired(captor.capture()); VerificationSession session = captor.getValue(); VerificationStatus status = new VerificationStatus.Builder().setVerified(true).build(); @@ -401,8 +410,8 @@ public class VerifierControllerTest { ArgumentCaptor.forClass(VerificationSession.class); assertThat(mVerifierController.startVerificationSession( mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI, - TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback, - /* retry= */ false)).isTrue(); + TEST_SIGNING_INFO, mTestDeclaredLibraries, TEST_POLICY, mTestExtensionParams, + mSessionCallback, /* retry= */ false)).isTrue(); verify(mMockService).onVerificationRequired(captor.capture()); VerificationSession session = captor.getValue(); VerificationStatus status = new VerificationStatus.Builder() @@ -421,8 +430,8 @@ public class VerifierControllerTest { ArgumentCaptor.forClass(VerificationSession.class); assertThat(mVerifierController.startVerificationSession( mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI, - TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback, - /* retry= */ false)).isTrue(); + TEST_SIGNING_INFO, mTestDeclaredLibraries, TEST_POLICY, mTestExtensionParams, + mSessionCallback, /* retry= */ false)).isTrue(); verify(mMockService).onVerificationRequired(captor.capture()); VerificationSession session = captor.getValue(); VerificationStatus status = new VerificationStatus.Builder().setVerified(true).build(); @@ -439,8 +448,8 @@ public class VerifierControllerTest { ArgumentCaptor.forClass(VerificationSession.class); mVerifierController.startVerificationSession( mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI, - TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback, - /* retry= */ false); + TEST_SIGNING_INFO, mTestDeclaredLibraries, TEST_POLICY, mTestExtensionParams, + mSessionCallback, /* retry= */ false); verify(mMockService).onVerificationRequired(captor.capture()); VerificationSession session = captor.getValue(); final long initialTimeoutTime = TEST_REQUEST_START_TIME + TEST_TIMEOUT_DURATION_MILLIS; @@ -456,8 +465,8 @@ public class VerifierControllerTest { ArgumentCaptor.forClass(VerificationSession.class); mVerifierController.startVerificationSession( mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI, - TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback, - /* retry= */ false); + TEST_SIGNING_INFO, mTestDeclaredLibraries, TEST_POLICY, mTestExtensionParams, + mSessionCallback, /* retry= */ false); verify(mMockService).onVerificationRequired(captor.capture()); VerificationSession session = captor.getValue(); final long initialTimeoutTime = TEST_REQUEST_START_TIME + TEST_TIMEOUT_DURATION_MILLIS; @@ -493,10 +502,27 @@ public class VerifierControllerTest { .thenReturn(TEST_REQUEST_START_TIME + TEST_TIMEOUT_DURATION_MILLIS + 1); mVerifierController.startVerificationSession( mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI, - TEST_SIGNING_INFO, mTestDeclaredLibraries, mTestExtensionParams, mSessionCallback, - /* retry= */ false); + TEST_SIGNING_INFO, mTestDeclaredLibraries, TEST_POLICY, mTestExtensionParams, + mSessionCallback, /* retry= */ false); verify(mHandler, times(3)).sendMessageAtTime(any(Message.class), anyLong()); verify(mInjector, times(6)).getCurrentTimeMillis(); verify(mSessionCallback, times(1)).onTimeout(); } + + @Test + public void testPolicyOverride() throws Exception { + ArgumentCaptor<VerificationSession> captor = + ArgumentCaptor.forClass(VerificationSession.class); + mVerifierController.startVerificationSession( + mSnapshotSupplier, 0, TEST_ID, TEST_PACKAGE_NAME, TEST_PACKAGE_URI, + TEST_SIGNING_INFO, mTestDeclaredLibraries, TEST_POLICY, mTestExtensionParams, + mSessionCallback, /* retry= */ false); + verify(mMockService).onVerificationRequired(captor.capture()); + VerificationSession session = captor.getValue(); + final int policy = VERIFICATION_POLICY_BLOCK_FAIL_OPEN; + when(mSessionCallback.setVerificationPolicy(eq(policy))).thenReturn(true); + assertThat(session.setVerificationPolicy(policy)).isTrue(); + assertThat(session.getVerificationPolicy()).isEqualTo(policy); + verify(mSessionCallback, times(1)).setVerificationPolicy(eq(policy)); + } } diff --git a/services/tests/appfunctions/Android.bp b/services/tests/appfunctions/Android.bp index c841643c6654..836f90b992d6 100644 --- a/services/tests/appfunctions/Android.bp +++ b/services/tests/appfunctions/Android.bp @@ -36,7 +36,9 @@ android_test { "androidx.test.core", "androidx.test.runner", "androidx.test.ext.truth", + "androidx.core_core-ktx", "kotlin-test", + "kotlinx_coroutines_test", "platform-test-annotations", "services.appfunctions", "servicestests-core-utils", diff --git a/services/tests/appfunctions/src/com/android/server/appfunctions/AppFunctionManagerServiceImplTest.kt b/services/tests/appfunctions/src/com/android/server/appfunctions/AppFunctionManagerServiceImplTest.kt new file mode 100644 index 000000000000..a69e9025bfa0 --- /dev/null +++ b/services/tests/appfunctions/src/com/android/server/appfunctions/AppFunctionManagerServiceImplTest.kt @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2024 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.server.appfunctions + +import android.app.appfunctions.flags.Flags +import android.content.Context +import android.platform.test.annotations.RequiresFlagsEnabled +import android.platform.test.flag.junit.CheckFlagsRule +import android.platform.test.flag.junit.DeviceFlagsValueProvider +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +@RequiresFlagsEnabled(Flags.FLAG_ENABLE_APP_FUNCTION_MANAGER) +class AppFunctionManagerServiceImplTest { + @get:Rule + val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() + + private val context: Context + get() = ApplicationProvider.getApplicationContext() + + private val serviceImpl = AppFunctionManagerServiceImpl(context) + + @Test + fun testGetLockForPackage_samePackage() { + val packageName = "com.example.app" + val lock1 = serviceImpl.getLockForPackage(packageName) + val lock2 = serviceImpl.getLockForPackage(packageName) + + // Assert that the same lock object is returned for the same package name + assertThat(lock1).isEqualTo(lock2) + } + + @Test + fun testGetLockForPackage_differentPackages() { + val packageName1 = "com.example.app1" + val packageName2 = "com.example.app2" + val lock1 = serviceImpl.getLockForPackage(packageName1) + val lock2 = serviceImpl.getLockForPackage(packageName2) + + // Assert that different lock objects are returned for different package names + assertThat(lock1).isNotEqualTo(lock2) + } + + @Ignore("Hard to deterministically trigger the garbage collector.") + @Test + fun testWeakReference_garbageCollected_differentLockAfterGC() = runTest { + // Create a large number of temporary objects to put pressure on the GC + val tempObjects = MutableList<Any?>(10000000) { Any() } + var callingPackage: String? = "com.example.app" + var lock1: Any? = serviceImpl.getLockForPackage(callingPackage) + callingPackage = null // Set the key to null + val lock1Hash = lock1.hashCode() + lock1 = null + + // Create memory pressure + repeat(3) { + for (i in 1..100) { + "a".repeat(10000) + } + System.gc() // Suggest garbage collection + System.runFinalization() + } + // Get the lock again - it should be a different object now + val lock2 = serviceImpl.getLockForPackage("com.example.app") + // Assert that the lock objects are different + assertThat(lock1Hash).isNotEqualTo(lock2.hashCode()) + } +} diff --git a/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java index 6ccc03709b4f..2a825f35bf62 100644 --- a/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/am/ActivityManagerServiceTest.java @@ -107,6 +107,7 @@ import android.os.IBinder; import android.os.IProgressListener; import android.os.Looper; import android.os.Message; +import android.os.Parcel; import android.os.Process; import android.os.RemoteException; import android.os.SystemClock; @@ -1309,12 +1310,13 @@ public class ActivityManagerServiceTest { intent.putExtra("EXTRA_INTENT0", extraIntent); intent.collectExtraIntentKeys(); - mAms.addCreatorToken(intent); + mAms.addCreatorToken(intent, TEST_PACKAGE); ActivityManagerService.IntentCreatorToken token = (ActivityManagerService.IntentCreatorToken) extraIntent.getCreatorToken(); assertThat(token).isNotNull(); assertThat(token.getCreatorUid()).isEqualTo(mInjector.getCallingUid()); + assertThat(token.getCreatorPackage()).isEqualTo(TEST_PACKAGE); } @Test @@ -1330,7 +1332,7 @@ public class ActivityManagerServiceTest { fillinIntent.collectExtraIntentKeys(); intent.fillIn(fillinIntent, FILL_IN_ACTION); - mAms.addCreatorToken(fillinIntent); + mAms.addCreatorToken(fillinIntent, TEST_PACKAGE); fillinExtraIntent = intent.getParcelableExtra("FILLIN_EXTRA_INTENT0", Intent.class); @@ -1338,6 +1340,49 @@ public class ActivityManagerServiceTest { (ActivityManagerService.IntentCreatorToken) fillinExtraIntent.getCreatorToken(); assertThat(token).isNotNull(); assertThat(token.getCreatorUid()).isEqualTo(mInjector.getCallingUid()); + assertThat(token.getCreatorPackage()).isEqualTo(TEST_PACKAGE); + } + + @Test + @RequiresFlagsEnabled(android.security.Flags.FLAG_PREVENT_INTENT_REDIRECT) + public void testCheckCreatorToken() { + Intent intent = new Intent(); + Intent extraIntent = new Intent("EXTRA_INTENT_ACTION"); + intent.putExtra("EXTRA_INTENT", extraIntent); + + intent.collectExtraIntentKeys(); + + // mimic client hack and sneak in an extra intent without going thru collectExtraIntentKeys. + Intent extraIntent2 = new Intent("EXTRA_INTENT_ACTION2"); + intent.putExtra("EXTRA_INTENT2", extraIntent2); + + // mock parceling on the client side, unparcling on the system server side, then + // addCreatorToken on system server side. + final Parcel parcel = Parcel.obtain(); + intent.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + Intent newIntent = new Intent(); + newIntent.readFromParcel(parcel); + intent = newIntent; + mAms.addCreatorToken(intent, TEST_PACKAGE); + // entering the target app's process. + intent.checkCreatorToken(); + + Intent extraIntent3 = new Intent("EXTRA_INTENT_ACTION3"); + intent.putExtra("EXTRA_INTENT3", extraIntent3); + + extraIntent = intent.getParcelableExtra("EXTRA_INTENT", Intent.class); + extraIntent2 = intent.getParcelableExtra("EXTRA_INTENT2", Intent.class); + extraIntent3 = intent.getParcelableExtra("EXTRA_INTENT3", Intent.class); + + assertThat(extraIntent.getExtendedFlags() + & Intent.EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN).isEqualTo(0); + // sneaked in intent should have EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN set. + assertThat(extraIntent2.getExtendedFlags() + & Intent.EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN).isNotEqualTo(0); + // local created intent should not have EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN set. + assertThat(extraIntent3.getExtendedFlags() + & Intent.EXTENDED_FLAG_MISSING_CREATOR_OR_INVALID_TOKEN).isEqualTo(0); } private void verifyWaitingForNetworkStateUpdate(long curProcStateSeq, diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java index 9781851da7e6..5c718d982476 100644 --- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java @@ -25,7 +25,6 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSess import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.when; import static com.android.server.job.Flags.FLAG_COUNT_QUOTA_FIX; -import static com.android.server.job.Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_FGS_JOBS; import static com.android.server.job.JobSchedulerService.ACTIVE_INDEX; import static com.android.server.job.JobSchedulerService.EXEMPTED_INDEX; import static com.android.server.job.JobSchedulerService.FREQUENT_INDEX; @@ -77,9 +76,12 @@ import android.os.Handler; import android.os.Looper; import android.os.RemoteException; import android.os.SystemClock; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.RequiresFlagsEnabled; import android.platform.test.flag.junit.CheckFlagsRule; import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.platform.test.flag.junit.SetFlagsRule; import android.provider.DeviceConfig; import android.util.ArraySet; import android.util.SparseBooleanArray; @@ -90,6 +92,7 @@ import androidx.test.runner.AndroidJUnit4; import com.android.internal.util.ArrayUtils; import com.android.server.LocalServices; import com.android.server.PowerAllowlistInternal; +import com.android.server.job.Flags; import com.android.server.job.JobSchedulerInternal; import com.android.server.job.JobSchedulerService; import com.android.server.job.JobStore; @@ -131,6 +134,7 @@ public class QuotaControllerTest { private static final String SOURCE_PACKAGE = "com.android.frameworks.mockingservicestests"; private static final int SOURCE_USER_ID = 0; + @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); private QuotaController mQuotaController; private QuotaController.QcConstants mQcConstants; private JobSchedulerService.Constants mConstants = new JobSchedulerService.Constants(); @@ -930,7 +934,8 @@ public class QuotaControllerTest { * Tests that getExecutionStatsLocked returns the correct stats. */ @Test - public void testGetExecutionStatsLocked_Values() { + @DisableFlags(Flags.FLAG_ADJUST_QUOTA_DEFAULT_CONSTANTS) + public void testGetExecutionStatsLocked_Values_LegacyDefaultBucketWindowSizes() { final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); mQuotaController.saveTimingSession(0, "com.android.test", createTimingSession(now - (mQcConstants.WINDOW_SIZE_RARE_MS - HOUR_IN_MILLIS), @@ -1015,11 +1020,112 @@ public class QuotaControllerTest { } } + @Test + @EnableFlags(Flags.FLAG_ADJUST_QUOTA_DEFAULT_CONSTANTS) + public void testGetExecutionStatsLocked_Values_NewDefaultBucketWindowSizes() { + final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); + mQuotaController.saveTimingSession(0, "com.android.test", + createTimingSession(now - (23 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5), false); + mQuotaController.saveTimingSession(0, "com.android.test", + createTimingSession(now - (7 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5), false); + mQuotaController.saveTimingSession(0, "com.android.test", + createTimingSession(now - (2 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5), false); + mQuotaController.saveTimingSession(0, "com.android.test", + createTimingSession(now - (6 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5), false); + + ExecutionStats expectedStats = new ExecutionStats(); + + // Exempted + expectedStats.allowedTimePerPeriodMs = mQcConstants.ALLOWED_TIME_PER_PERIOD_ACTIVE_MS; + expectedStats.windowSizeMs = mQcConstants.WINDOW_SIZE_EXEMPTED_MS; + expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_EXEMPTED; + expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_EXEMPTED; + expectedStats.expirationTimeElapsed = now + 14 * MINUTE_IN_MILLIS; + expectedStats.executionTimeInWindowMs = 3 * MINUTE_IN_MILLIS; + expectedStats.bgJobCountInWindow = 5; + expectedStats.executionTimeInMaxPeriodMs = 33 * MINUTE_IN_MILLIS; + expectedStats.bgJobCountInMaxPeriod = 20; + expectedStats.sessionCountInWindow = 1; + synchronized (mQuotaController.mLock) { + assertEquals(expectedStats, + mQuotaController.getExecutionStatsLocked(0, "com.android.test", + EXEMPTED_INDEX)); + } + + // Active + expectedStats.windowSizeMs = mQcConstants.WINDOW_SIZE_ACTIVE_MS; + expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_ACTIVE; + expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_ACTIVE; + // There is only one session in the past active bucket window, the empty time for this + // window is the bucket window size - duration of the session. + expectedStats.expirationTimeElapsed = now + 24 * MINUTE_IN_MILLIS; + synchronized (mQuotaController.mLock) { + assertEquals(expectedStats, + mQuotaController.getExecutionStatsLocked(0, "com.android.test", + ACTIVE_INDEX)); + } + + // Working + expectedStats.windowSizeMs = mQcConstants.WINDOW_SIZE_WORKING_MS; + expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_WORKING; + expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_WORKING; + expectedStats.expirationTimeElapsed = now + HOUR_IN_MILLIS; + expectedStats.executionTimeInWindowMs = 13 * MINUTE_IN_MILLIS; + expectedStats.bgJobCountInWindow = 10; + expectedStats.executionTimeInMaxPeriodMs = 33 * MINUTE_IN_MILLIS; + expectedStats.bgJobCountInMaxPeriod = 20; + expectedStats.sessionCountInWindow = 2; + expectedStats.inQuotaTimeElapsed = now + 2 * HOUR_IN_MILLIS + 3 * MINUTE_IN_MILLIS + + mQcConstants.IN_QUOTA_BUFFER_MS; + synchronized (mQuotaController.mLock) { + assertEquals(expectedStats, + mQuotaController.getExecutionStatsLocked(0, "com.android.test", + WORKING_INDEX)); + } + + // Frequent + expectedStats.windowSizeMs = mQcConstants.WINDOW_SIZE_FREQUENT_MS; + expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_FREQUENT; + expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_FREQUENT; + expectedStats.expirationTimeElapsed = now + HOUR_IN_MILLIS; + expectedStats.executionTimeInWindowMs = 23 * MINUTE_IN_MILLIS; + expectedStats.bgJobCountInWindow = 15; + expectedStats.executionTimeInMaxPeriodMs = 33 * MINUTE_IN_MILLIS; + expectedStats.bgJobCountInMaxPeriod = 20; + expectedStats.sessionCountInWindow = 3; + expectedStats.inQuotaTimeElapsed = now + 10 * HOUR_IN_MILLIS + 3 * MINUTE_IN_MILLIS + + mQcConstants.IN_QUOTA_BUFFER_MS; + synchronized (mQuotaController.mLock) { + assertEquals(expectedStats, + mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", FREQUENT_INDEX)); + } + + // Rare + expectedStats.windowSizeMs = mQcConstants.WINDOW_SIZE_RARE_MS; + expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_RARE; + expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_RARE; + expectedStats.expirationTimeElapsed = now + HOUR_IN_MILLIS; + expectedStats.executionTimeInWindowMs = 33 * MINUTE_IN_MILLIS; + expectedStats.bgJobCountInWindow = 20; + expectedStats.executionTimeInMaxPeriodMs = 33 * MINUTE_IN_MILLIS; + expectedStats.bgJobCountInMaxPeriod = 20; + expectedStats.sessionCountInWindow = 4; + expectedStats.inQuotaTimeElapsed = now + 22 * HOUR_IN_MILLIS + 3 * MINUTE_IN_MILLIS + + mQcConstants.IN_QUOTA_BUFFER_MS; + synchronized (mQuotaController.mLock) { + assertEquals(expectedStats, + mQuotaController.getExecutionStatsLocked(0, "com.android.test", + RARE_INDEX)); + } + } + /** * Tests that getExecutionStatsLocked returns the correct stats soon after device startup. */ @Test - public void testGetExecutionStatsLocked_Values_BeginningOfTime() { + @DisableFlags(Flags.FLAG_ADJUST_QUOTA_DEFAULT_CONSTANTS) + public void testGetExecutionStatsLocked_Values_BeginningOfTime_LegacyDefaultBucketWindowSizes() { // Set time to 3 minutes after boot. advanceElapsedClock(-JobSchedulerService.sElapsedRealtimeClock.millis()); advanceElapsedClock(3 * MINUTE_IN_MILLIS); @@ -1042,7 +1148,8 @@ public class QuotaControllerTest { expectedStats.sessionCountInWindow = 1; synchronized (mQuotaController.mLock) { assertEquals(expectedStats, - mQuotaController.getExecutionStatsLocked(0, "com.android.test", ACTIVE_INDEX)); + mQuotaController.getExecutionStatsLocked(0, "com.android.test", + ACTIVE_INDEX)); } // Working @@ -1052,7 +1159,8 @@ public class QuotaControllerTest { expectedStats.expirationTimeElapsed = 2 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS; synchronized (mQuotaController.mLock) { assertEquals(expectedStats, - mQuotaController.getExecutionStatsLocked(0, "com.android.test", WORKING_INDEX)); + mQuotaController.getExecutionStatsLocked(0, "com.android.test", + WORKING_INDEX)); } // Frequent @@ -1073,7 +1181,83 @@ public class QuotaControllerTest { expectedStats.expirationTimeElapsed = 24 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS; synchronized (mQuotaController.mLock) { assertEquals(expectedStats, - mQuotaController.getExecutionStatsLocked(0, "com.android.test", RARE_INDEX)); + mQuotaController.getExecutionStatsLocked(0, "com.android.test", + RARE_INDEX)); + } + } + + @Test + @EnableFlags(Flags.FLAG_ADJUST_QUOTA_DEFAULT_CONSTANTS) + public void testGetExecutionStatsLocked_Values_BeginningOfTime_NewDefaultBucketWindowSizes() { + // Set time to 3 minutes after boot. + advanceElapsedClock(-JobSchedulerService.sElapsedRealtimeClock.millis()); + advanceElapsedClock(3 * MINUTE_IN_MILLIS); + + mQuotaController.saveTimingSession(0, "com.android.test", + createTimingSession(MINUTE_IN_MILLIS, MINUTE_IN_MILLIS, 2), false); + + ExecutionStats expectedStats = new ExecutionStats(); + + // Exempted + expectedStats.allowedTimePerPeriodMs = mQcConstants.ALLOWED_TIME_PER_PERIOD_ACTIVE_MS; + expectedStats.windowSizeMs = mQcConstants.WINDOW_SIZE_EXEMPTED_MS; + expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_ACTIVE; + expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_ACTIVE; + expectedStats.expirationTimeElapsed = 10 * MINUTE_IN_MILLIS + 11 * MINUTE_IN_MILLIS; + expectedStats.executionTimeInWindowMs = MINUTE_IN_MILLIS; + expectedStats.bgJobCountInWindow = 2; + expectedStats.executionTimeInMaxPeriodMs = MINUTE_IN_MILLIS; + expectedStats.bgJobCountInMaxPeriod = 2; + expectedStats.sessionCountInWindow = 1; + synchronized (mQuotaController.mLock) { + assertEquals(expectedStats, + mQuotaController.getExecutionStatsLocked(0, "com.android.test", + EXEMPTED_INDEX)); + } + + // Active + expectedStats.allowedTimePerPeriodMs = mQcConstants.ALLOWED_TIME_PER_PERIOD_ACTIVE_MS; + expectedStats.windowSizeMs = mQcConstants.WINDOW_SIZE_ACTIVE_MS; + expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_ACTIVE; + expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_ACTIVE; + expectedStats.expirationTimeElapsed = 20 * MINUTE_IN_MILLIS + 11 * MINUTE_IN_MILLIS; + synchronized (mQuotaController.mLock) { + assertEquals(expectedStats, + mQuotaController.getExecutionStatsLocked(0, "com.android.test", + ACTIVE_INDEX)); + } + + // Working + expectedStats.windowSizeMs = mQcConstants.WINDOW_SIZE_WORKING_MS; + expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_WORKING; + expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_WORKING; + expectedStats.expirationTimeElapsed = 4 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS; + synchronized (mQuotaController.mLock) { + assertEquals(expectedStats, + mQuotaController.getExecutionStatsLocked(0, "com.android.test", + WORKING_INDEX)); + } + + // Frequent + expectedStats.windowSizeMs = mQcConstants.WINDOW_SIZE_FREQUENT_MS; + expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_FREQUENT; + expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_FREQUENT; + expectedStats.expirationTimeElapsed = 12 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS; + synchronized (mQuotaController.mLock) { + assertEquals(expectedStats, + mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", FREQUENT_INDEX)); + } + + // Rare + expectedStats.windowSizeMs = mQcConstants.WINDOW_SIZE_RARE_MS; + expectedStats.jobCountLimit = mQcConstants.MAX_JOB_COUNT_RARE; + expectedStats.sessionCountLimit = mQcConstants.MAX_SESSION_COUNT_RARE; + expectedStats.expirationTimeElapsed = 24 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS; + synchronized (mQuotaController.mLock) { + assertEquals(expectedStats, + mQuotaController.getExecutionStatsLocked(0, "com.android.test", + RARE_INDEX)); } } @@ -1081,7 +1265,8 @@ public class QuotaControllerTest { * Tests that getExecutionStatsLocked returns the correct timing session stats when coalescing. */ @Test - public void testGetExecutionStatsLocked_CoalescingSessions() { + @DisableFlags(Flags.FLAG_ADJUST_QUOTA_DEFAULT_CONSTANTS) + public void testGetExecutionStatsLocked_CoalescingSessions_LegacyDefaultBucketWindowSizes() { for (int i = 0; i < 10; ++i) { mQuotaController.saveTimingSession(0, "com.android.test", createTimingSession( @@ -1098,12 +1283,14 @@ public class QuotaControllerTest { advanceElapsedClock(54 * SECOND_IN_MILLIS); mQuotaController.saveTimingSession(0, "com.android.test", createTimingSession( - JobSchedulerService.sElapsedRealtimeClock.millis(), 500, 1), false); + JobSchedulerService.sElapsedRealtimeClock.millis(), + 500, 1), false); advanceElapsedClock(500); advanceElapsedClock(400); mQuotaController.saveTimingSession(0, "com.android.test", createTimingSession( - JobSchedulerService.sElapsedRealtimeClock.millis(), 100, 1), false); + JobSchedulerService.sElapsedRealtimeClock.millis(), + 100, 1), false); advanceElapsedClock(100); advanceElapsedClock(5 * SECOND_IN_MILLIS); } @@ -1229,6 +1416,164 @@ public class QuotaControllerTest { } } + @Test + @EnableFlags(Flags.FLAG_ADJUST_QUOTA_DEFAULT_CONSTANTS) + public void testGetExecutionStatsLocked_CoalescingSessions_NewDefaultBucketWindowSizes() { + for (int i = 0; i < 20; ++i) { + mQuotaController.saveTimingSession(0, "com.android.test", + createTimingSession( + JobSchedulerService.sElapsedRealtimeClock.millis(), + 5 * MINUTE_IN_MILLIS, 5), false); + advanceElapsedClock(5 * MINUTE_IN_MILLIS); + advanceElapsedClock(5 * MINUTE_IN_MILLIS); + for (int j = 0; j < 5; ++j) { + mQuotaController.saveTimingSession(0, "com.android.test", + createTimingSession( + JobSchedulerService.sElapsedRealtimeClock.millis(), + MINUTE_IN_MILLIS, 2), false); + advanceElapsedClock(MINUTE_IN_MILLIS); + advanceElapsedClock(54 * SECOND_IN_MILLIS); + mQuotaController.saveTimingSession(0, "com.android.test", + createTimingSession( + JobSchedulerService.sElapsedRealtimeClock.millis(), 500, 1), false); + advanceElapsedClock(500); + advanceElapsedClock(400); + mQuotaController.saveTimingSession(0, "com.android.test", + createTimingSession( + JobSchedulerService.sElapsedRealtimeClock.millis(), 100, 1), false); + advanceElapsedClock(100); + advanceElapsedClock(5 * SECOND_IN_MILLIS); + } + advanceElapsedClock(40 * MINUTE_IN_MILLIS); + } + + setDeviceConfigLong(QcConstants.KEY_TIMING_SESSION_COALESCING_DURATION_MS, 0); + + synchronized (mQuotaController.mLock) { + mQuotaController.invalidateAllExecutionStatsLocked(); + assertEquals(0, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", ACTIVE_INDEX).sessionCountInWindow); + assertEquals(64, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", WORKING_INDEX).sessionCountInWindow); + assertEquals(192, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", FREQUENT_INDEX).sessionCountInWindow); + assertEquals(320, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", RARE_INDEX).sessionCountInWindow); + } + + setDeviceConfigLong(QcConstants.KEY_TIMING_SESSION_COALESCING_DURATION_MS, 500); + + synchronized (mQuotaController.mLock) { + mQuotaController.invalidateAllExecutionStatsLocked(); + assertEquals(0, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", ACTIVE_INDEX).sessionCountInWindow); + // WINDOW_SIZE_WORKING_MS * 5 TimingSessions are coalesced + assertEquals(44, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", WORKING_INDEX).sessionCountInWindow); + // WINDOW_SIZE_FREQUENT_MS * 5 TimingSessions are coalesced + assertEquals(132, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", FREQUENT_INDEX).sessionCountInWindow); + // WINDOW_SIZE_RARE_MS * 5 TimingSessions are coalesced + assertEquals(220, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", RARE_INDEX).sessionCountInWindow); + } + + setDeviceConfigLong(QcConstants.KEY_TIMING_SESSION_COALESCING_DURATION_MS, 1000); + + synchronized (mQuotaController.mLock) { + mQuotaController.invalidateAllExecutionStatsLocked(); + assertEquals(0, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", ACTIVE_INDEX).sessionCountInWindow); + assertEquals(44, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", WORKING_INDEX).sessionCountInWindow); + assertEquals(132, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", FREQUENT_INDEX).sessionCountInWindow); + assertEquals(220, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", RARE_INDEX).sessionCountInWindow); + } + + setDeviceConfigLong(QcConstants.KEY_TIMING_SESSION_COALESCING_DURATION_MS, + 5 * SECOND_IN_MILLIS); + + synchronized (mQuotaController.mLock) { + mQuotaController.invalidateAllExecutionStatsLocked(); + assertEquals(0, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", ACTIVE_INDEX).sessionCountInWindow); + // WINDOW_SIZE_WORKING_MS * 9 TimingSessions are coalesced + assertEquals(28, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", WORKING_INDEX).sessionCountInWindow); + // WINDOW_SIZE_FREQUENT_MS * 9 TimingSessions are coalesced + assertEquals(84, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", FREQUENT_INDEX).sessionCountInWindow); + // WINDOW_SIZE_RARE_MS * 9 TimingSessions are coalesced + assertEquals(140, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", RARE_INDEX).sessionCountInWindow); + } + + setDeviceConfigLong(QcConstants.KEY_TIMING_SESSION_COALESCING_DURATION_MS, + MINUTE_IN_MILLIS); + + // Only two TimingSessions there for every hour. + synchronized (mQuotaController.mLock) { + mQuotaController.invalidateAllExecutionStatsLocked(); + assertEquals(0, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", ACTIVE_INDEX).sessionCountInWindow); + assertEquals(8, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", WORKING_INDEX).sessionCountInWindow); + assertEquals(24, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", FREQUENT_INDEX).sessionCountInWindow); + assertEquals(40, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", RARE_INDEX).sessionCountInWindow); + } + + setDeviceConfigLong(QcConstants.KEY_TIMING_SESSION_COALESCING_DURATION_MS, + 5 * MINUTE_IN_MILLIS); + + // Only one TimingSessions there for every hour + synchronized (mQuotaController.mLock) { + mQuotaController.invalidateAllExecutionStatsLocked(); + assertEquals(0, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", ACTIVE_INDEX).sessionCountInWindow); + assertEquals(4, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", WORKING_INDEX).sessionCountInWindow); + assertEquals(12, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", FREQUENT_INDEX).sessionCountInWindow); + assertEquals(20, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", RARE_INDEX).sessionCountInWindow); + } + + setDeviceConfigLong(QcConstants.KEY_TIMING_SESSION_COALESCING_DURATION_MS, + 15 * MINUTE_IN_MILLIS); + + synchronized (mQuotaController.mLock) { + mQuotaController.invalidateAllExecutionStatsLocked(); + assertEquals(0, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", ACTIVE_INDEX).sessionCountInWindow); + assertEquals(4, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", WORKING_INDEX).sessionCountInWindow); + assertEquals(12, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", FREQUENT_INDEX).sessionCountInWindow); + assertEquals(20, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", RARE_INDEX).sessionCountInWindow); + } + + // QuotaController caps the duration at 15 minutes, so there shouldn't be any difference + // between an hour and 15 minutes. + setDeviceConfigLong(QcConstants.KEY_TIMING_SESSION_COALESCING_DURATION_MS, HOUR_IN_MILLIS); + + synchronized (mQuotaController.mLock) { + mQuotaController.invalidateAllExecutionStatsLocked(); + assertEquals(0, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", ACTIVE_INDEX).sessionCountInWindow); + assertEquals(4, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", WORKING_INDEX).sessionCountInWindow); + assertEquals(12, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", FREQUENT_INDEX).sessionCountInWindow); + assertEquals(20, mQuotaController.getExecutionStatsLocked( + 0, "com.android.test", RARE_INDEX).sessionCountInWindow); + } + } + /** * Tests that getExecutionStatsLocked properly caches the stats and returns the cached object. */ @@ -1454,7 +1799,8 @@ public class QuotaControllerTest { } @Test - public void testGetMaxJobExecutionTimeLocked_EJ() { + @DisableFlags(Flags.FLAG_ADJUST_QUOTA_DEFAULT_CONSTANTS) + public void testGetMaxJobExecutionTimeLocked_EJ_LegacyDefaultEJLimits() { final long timeUsedMs = 3 * MINUTE_IN_MILLIS; mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, createTimingSession(sElapsedRealtimeClock.millis() - (6 * MINUTE_IN_MILLIS), @@ -1530,11 +1876,91 @@ public class QuotaControllerTest { } } + @Test + @EnableFlags(Flags.FLAG_ADJUST_QUOTA_DEFAULT_CONSTANTS) + public void testGetMaxJobExecutionTimeLocked_EJ_NewDefaultEJLimits() { + final long timeUsedMs = 3 * MINUTE_IN_MILLIS; + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession(sElapsedRealtimeClock.millis() - (6 * MINUTE_IN_MILLIS), + timeUsedMs, 5), true); + JobStatus job = createExpeditedJobStatus("testGetMaxJobExecutionTimeLocked_EJ", 0); + setStandbyBucket(RARE_INDEX, job); + synchronized (mQuotaController.mLock) { + mQuotaController.maybeStartTrackingJobLocked(job, null); + } + + setCharging(); + synchronized (mQuotaController.mLock) { + assertEquals(JobSchedulerService.Constants.DEFAULT_RUNTIME_FREE_QUOTA_MAX_LIMIT_MS, + mQuotaController.getMaxJobExecutionTimeMsLocked(job)); + } + + setDischarging(); + setProcessState(getProcessStateQuotaFreeThreshold()); + synchronized (mQuotaController.mLock) { + assertEquals(mQcConstants.EJ_LIMIT_WORKING_MS / 2, + mQuotaController.getMaxJobExecutionTimeMsLocked(job)); + } + + // Top-started job + setProcessState(ActivityManager.PROCESS_STATE_TOP); + synchronized (mQuotaController.mLock) { + mQuotaController.prepareForExecutionLocked(job); + } + setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND); + synchronized (mQuotaController.mLock) { + assertEquals(mQcConstants.EJ_LIMIT_ACTIVE_MS / 2, + mQuotaController.getMaxJobExecutionTimeMsLocked(job)); + mQuotaController.maybeStopTrackingJobLocked(job, null); + } + + setProcessState(ActivityManager.PROCESS_STATE_RECEIVER); + synchronized (mQuotaController.mLock) { + assertEquals(mQcConstants.EJ_LIMIT_RARE_MS - timeUsedMs, + mQuotaController.getMaxJobExecutionTimeMsLocked(job)); + } + + // Test used quota rolling out of window. + synchronized (mQuotaController.mLock) { + mQuotaController.clearAppStatsLocked(SOURCE_USER_ID, SOURCE_PACKAGE); + } + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession(sElapsedRealtimeClock.millis() - mQcConstants.EJ_WINDOW_SIZE_MS, + timeUsedMs, 5), true); + + setProcessState(getProcessStateQuotaFreeThreshold()); + synchronized (mQuotaController.mLock) { + // max of 50% WORKING limit and remaining quota + assertEquals(10 * MINUTE_IN_MILLIS, + mQuotaController.getMaxJobExecutionTimeMsLocked(job)); + } + + // Top-started job + setProcessState(ActivityManager.PROCESS_STATE_TOP); + synchronized (mQuotaController.mLock) { + mQuotaController.maybeStartTrackingJobLocked(job, null); + mQuotaController.prepareForExecutionLocked(job); + } + setProcessState(ActivityManager.PROCESS_STATE_IMPORTANT_BACKGROUND); + synchronized (mQuotaController.mLock) { + assertEquals(mQcConstants.EJ_LIMIT_ACTIVE_MS / 2, + mQuotaController.getMaxJobExecutionTimeMsLocked(job)); + mQuotaController.maybeStopTrackingJobLocked(job, null); + } + + setProcessState(ActivityManager.PROCESS_STATE_RECEIVER); + synchronized (mQuotaController.mLock) { + assertEquals(mQcConstants.EJ_LIMIT_RARE_MS, + mQuotaController.getMaxJobExecutionTimeMsLocked(job)); + } + } + /** * Test getTimeUntilQuotaConsumedLocked when allowed time equals the bucket window size. */ @Test - public void testGetTimeUntilQuotaConsumedLocked_AllowedEqualsWindow() { + @DisableFlags(Flags.FLAG_ADJUST_QUOTA_DEFAULT_CONSTANTS) + public void testGetTimeUntilQuotaConsumedLocked_AllowedEqualsWindow_LegacyDefaultBucketWindowSizes() { final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, createTimingSession(now - (8 * HOUR_IN_MILLIS), 20 * MINUTE_IN_MILLIS, 5), false); @@ -1558,27 +1984,56 @@ public class QuotaControllerTest { } } + @Test + @EnableFlags(Flags.FLAG_ADJUST_QUOTA_DEFAULT_CONSTANTS) + public void testGetTimeUntilQuotaConsumedLocked_AllowedEqualsWindow_NewDefaultBucketWindowSizes() { + final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession(now - (8 * HOUR_IN_MILLIS), 20 * MINUTE_IN_MILLIS, 5), false); + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession(now - (10 * MINUTE_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5), + false); + + setDeviceConfigLong(QcConstants.KEY_ALLOWED_TIME_PER_PERIOD_EXEMPTED_MS, + 20 * MINUTE_IN_MILLIS); + setDeviceConfigLong(QcConstants.KEY_WINDOW_SIZE_EXEMPTED_MS, 20 * MINUTE_IN_MILLIS); + // window size = allowed time, so jobs can essentially run non-stop until they reach the + // max execution time. + setStandbyBucket(EXEMPTED_INDEX); + synchronized (mQuotaController.mLock) { + assertEquals(10 * MINUTE_IN_MILLIS, + mQuotaController.getRemainingExecutionTimeLocked( + SOURCE_USER_ID, SOURCE_PACKAGE)); + assertEquals(mQcConstants.MAX_EXECUTION_TIME_MS - 30 * MINUTE_IN_MILLIS, + mQuotaController.getTimeUntilQuotaConsumedLocked( + SOURCE_USER_ID, SOURCE_PACKAGE)); + } + } + /** * Test getTimeUntilQuotaConsumedLocked when the determination is based within the bucket * window. */ @Test - public void testGetTimeUntilQuotaConsumedLocked_BucketWindow() { + @DisableFlags(Flags.FLAG_ADJUST_QUOTA_DEFAULT_CONSTANTS) + public void testGetTimeUntilQuotaConsumedLocked_BucketWindow_LegacyDefaultBucketWindowSizes() { final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); // Close to RARE boundary. mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, - createTimingSession(now - (24 * HOUR_IN_MILLIS - 30 * SECOND_IN_MILLIS), + createTimingSession(now - (mQcConstants.WINDOW_SIZE_RARE_MS - 30 * SECOND_IN_MILLIS), 30 * SECOND_IN_MILLIS, 5), false); // Far away from FREQUENT boundary. mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, - createTimingSession(now - (7 * HOUR_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5), false); + createTimingSession(now - (mQcConstants.WINDOW_SIZE_FREQUENT_MS - HOUR_IN_MILLIS), + 3 * MINUTE_IN_MILLIS, 5), false); // Overlap WORKING_SET boundary. mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, - createTimingSession(now - (2 * HOUR_IN_MILLIS + MINUTE_IN_MILLIS), + createTimingSession(now - (mQcConstants.WINDOW_SIZE_WORKING_MS + MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5), false); // Close to ACTIVE boundary. mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, - createTimingSession(now - (9 * MINUTE_IN_MILLIS), 3 * MINUTE_IN_MILLIS, 5), false); + createTimingSession(now - (mQcConstants.WINDOW_SIZE_ACTIVE_MS - MINUTE_IN_MILLIS), + 3 * MINUTE_IN_MILLIS, 5), false); setStandbyBucket(RARE_INDEX); synchronized (mQuotaController.mLock) { @@ -1623,6 +2078,69 @@ public class QuotaControllerTest { } } + @Test + @EnableFlags(Flags.FLAG_ADJUST_QUOTA_DEFAULT_CONSTANTS) + public void testGetTimeUntilQuotaConsumedLocked_BucketWindow_NewDefaultBucketWindowSizes() { + final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); + // Close to RARE boundary. + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession(now - (mQcConstants.WINDOW_SIZE_RARE_MS - 30 * SECOND_IN_MILLIS), + 30 * SECOND_IN_MILLIS, 5), false); + // Far away from FREQUENT boundary. + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession(now - (mQcConstants.WINDOW_SIZE_FREQUENT_MS - HOUR_IN_MILLIS), + 3 * MINUTE_IN_MILLIS, 5), false); + // Overlap WORKING_SET boundary. + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession(now - (mQcConstants.WINDOW_SIZE_WORKING_MS + MINUTE_IN_MILLIS), + 3 * MINUTE_IN_MILLIS, 5), false); + // Close to ACTIVE boundary. + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession(now - (mQcConstants.WINDOW_SIZE_ACTIVE_MS - MINUTE_IN_MILLIS), + 3 * MINUTE_IN_MILLIS, 5), false); + + setStandbyBucket(RARE_INDEX); + synchronized (mQuotaController.mLock) { + assertEquals(30 * SECOND_IN_MILLIS, + mQuotaController.getRemainingExecutionTimeLocked( + SOURCE_USER_ID, SOURCE_PACKAGE)); + assertEquals(MINUTE_IN_MILLIS, + mQuotaController.getTimeUntilQuotaConsumedLocked( + SOURCE_USER_ID, SOURCE_PACKAGE)); + } + + setStandbyBucket(FREQUENT_INDEX); + synchronized (mQuotaController.mLock) { + assertEquals(MINUTE_IN_MILLIS, + mQuotaController.getRemainingExecutionTimeLocked( + SOURCE_USER_ID, SOURCE_PACKAGE)); + assertEquals(MINUTE_IN_MILLIS, + mQuotaController.getTimeUntilQuotaConsumedLocked( + SOURCE_USER_ID, SOURCE_PACKAGE)); + } + + setStandbyBucket(WORKING_INDEX); + synchronized (mQuotaController.mLock) { + assertEquals(5 * MINUTE_IN_MILLIS, + mQuotaController.getRemainingExecutionTimeLocked( + SOURCE_USER_ID, SOURCE_PACKAGE)); + assertEquals(7 * MINUTE_IN_MILLIS, + mQuotaController.getTimeUntilQuotaConsumedLocked( + SOURCE_USER_ID, SOURCE_PACKAGE)); + } + + // ACTIVE window != allowed time. + setStandbyBucket(ACTIVE_INDEX); + synchronized (mQuotaController.mLock) { + assertEquals(7 * MINUTE_IN_MILLIS, + mQuotaController.getRemainingExecutionTimeLocked( + SOURCE_USER_ID, SOURCE_PACKAGE)); + assertEquals(10 * MINUTE_IN_MILLIS, + mQuotaController.getTimeUntilQuotaConsumedLocked( + SOURCE_USER_ID, SOURCE_PACKAGE)); + } + } + /** * Test getTimeUntilQuotaConsumedLocked when the app is close to the max execution limit. */ @@ -1747,7 +2265,8 @@ public class QuotaControllerTest { * Test getTimeUntilQuotaConsumedLocked when allowed time equals the bucket window size. */ @Test - public void testGetTimeUntilQuotaConsumedLocked_EdgeOfWindow_AllowedEqualsWindow() { + @DisableFlags(Flags.FLAG_ADJUST_QUOTA_DEFAULT_CONSTANTS) + public void testGetTimeUntilQuotaConsumedLocked_EdgeOfWindow_AllowedEqualsWindow_LegacyDefaultBucketWindowSizes() { final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, createTimingSession(now - (24 * HOUR_IN_MILLIS), @@ -1773,55 +2292,84 @@ public class QuotaControllerTest { } } + @Test + @EnableFlags(Flags.FLAG_ADJUST_QUOTA_DEFAULT_CONSTANTS) + public void testGetTimeUntilQuotaConsumedLocked_EdgeOfWindow_AllowedEqualsWindow_NewDefaultBucketWindowSizes() { + final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession(now - (24 * HOUR_IN_MILLIS), + mQcConstants.MAX_EXECUTION_TIME_MS - 20 * MINUTE_IN_MILLIS, 5), + false); + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession(now - (20 * MINUTE_IN_MILLIS), 20 * MINUTE_IN_MILLIS, 5), + false); + + setDeviceConfigLong(QcConstants.KEY_ALLOWED_TIME_PER_PERIOD_EXEMPTED_MS, + 20 * MINUTE_IN_MILLIS); + setDeviceConfigLong(QcConstants.KEY_WINDOW_SIZE_EXEMPTED_MS, 20 * MINUTE_IN_MILLIS); + // window size != allowed time. + setStandbyBucket(EXEMPTED_INDEX); + synchronized (mQuotaController.mLock) { + assertEquals(0, + mQuotaController.getRemainingExecutionTimeLocked( + SOURCE_USER_ID, SOURCE_PACKAGE)); + assertEquals(mQcConstants.MAX_EXECUTION_TIME_MS - 20 * MINUTE_IN_MILLIS, + mQuotaController.getTimeUntilQuotaConsumedLocked( + SOURCE_USER_ID, SOURCE_PACKAGE)); + } + } + /** * Test getTimeUntilQuotaConsumedLocked when the determination is based within the bucket * window and the session is rolling out of the window. */ @Test - public void testGetTimeUntilQuotaConsumedLocked_EdgeOfWindow_BucketWindow() { + @DisableFlags(Flags.FLAG_ADJUST_QUOTA_DEFAULT_CONSTANTS) + public void testGetTimeUntilQuotaConsumedLocked_EdgeOfWindow_BucketWindow_LegacyDefaultBucketWindowSizes() { final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, - createTimingSession(now - (24 * HOUR_IN_MILLIS), - 10 * MINUTE_IN_MILLIS, 5), false); + createTimingSession(now - mQcConstants.WINDOW_SIZE_RARE_MS, + mQcConstants.ALLOWED_TIME_PER_PERIOD_RARE_MS, 5), false); setStandbyBucket(RARE_INDEX); synchronized (mQuotaController.mLock) { assertEquals(0, mQuotaController.getRemainingExecutionTimeLocked( SOURCE_USER_ID, SOURCE_PACKAGE)); - assertEquals(10 * MINUTE_IN_MILLIS, + assertEquals(mQcConstants.ALLOWED_TIME_PER_PERIOD_RARE_MS, mQuotaController.getTimeUntilQuotaConsumedLocked( SOURCE_USER_ID, SOURCE_PACKAGE)); } mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, - createTimingSession(now - (8 * HOUR_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5), false); + createTimingSession(now - mQcConstants.WINDOW_SIZE_FREQUENT_MS, + mQcConstants.ALLOWED_TIME_PER_PERIOD_FREQUENT_MS, 5), false); setStandbyBucket(FREQUENT_INDEX); synchronized (mQuotaController.mLock) { assertEquals(0, mQuotaController.getRemainingExecutionTimeLocked( SOURCE_USER_ID, SOURCE_PACKAGE)); - assertEquals(10 * MINUTE_IN_MILLIS, + assertEquals(mQcConstants.ALLOWED_TIME_PER_PERIOD_FREQUENT_MS, mQuotaController.getTimeUntilQuotaConsumedLocked( SOURCE_USER_ID, SOURCE_PACKAGE)); } mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, - createTimingSession(now - (2 * HOUR_IN_MILLIS), - 10 * MINUTE_IN_MILLIS, 5), false); + createTimingSession(now - mQcConstants.WINDOW_SIZE_WORKING_MS, + mQcConstants.ALLOWED_TIME_PER_PERIOD_WORKING_MS, 5), false); setStandbyBucket(WORKING_INDEX); synchronized (mQuotaController.mLock) { assertEquals(0, mQuotaController.getRemainingExecutionTimeLocked( SOURCE_USER_ID, SOURCE_PACKAGE)); - assertEquals(10 * MINUTE_IN_MILLIS, + assertEquals(mQcConstants.ALLOWED_TIME_PER_PERIOD_WORKING_MS, mQuotaController.getTimeUntilQuotaConsumedLocked( SOURCE_USER_ID, SOURCE_PACKAGE)); } mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, - createTimingSession(now - (10 * MINUTE_IN_MILLIS), 10 * MINUTE_IN_MILLIS, 5), - false); + createTimingSession(now - mQcConstants.WINDOW_SIZE_ACTIVE_MS, + mQcConstants.ALLOWED_TIME_PER_PERIOD_ACTIVE_MS, 5), false); // ACTIVE window = allowed time, so jobs can essentially run non-stop until they reach the // max execution time. setStandbyBucket(ACTIVE_INDEX); @@ -1829,7 +2377,85 @@ public class QuotaControllerTest { assertEquals(0, mQuotaController.getRemainingExecutionTimeLocked( SOURCE_USER_ID, SOURCE_PACKAGE)); - assertEquals(mQcConstants.MAX_EXECUTION_TIME_MS - 30 * MINUTE_IN_MILLIS, + assertEquals(mQcConstants.MAX_EXECUTION_TIME_MS + - (mQcConstants.ALLOWED_TIME_PER_PERIOD_RARE_MS + + mQcConstants.ALLOWED_TIME_PER_PERIOD_FREQUENT_MS + + mQcConstants.ALLOWED_TIME_PER_PERIOD_WORKING_MS), + mQuotaController.getTimeUntilQuotaConsumedLocked( + SOURCE_USER_ID, SOURCE_PACKAGE)); + } + } + + @Test + @EnableFlags(Flags.FLAG_ADJUST_QUOTA_DEFAULT_CONSTANTS) + public void testGetTimeUntilQuotaConsumedLocked_EdgeOfWindow_BucketWindow_NewDefaultBucketWindowSizes() { + final long now = JobSchedulerService.sElapsedRealtimeClock.millis(); + + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession(now - mQcConstants.WINDOW_SIZE_RARE_MS, + mQcConstants.ALLOWED_TIME_PER_PERIOD_RARE_MS, 5), false); + setStandbyBucket(RARE_INDEX); + synchronized (mQuotaController.mLock) { + assertEquals(0, + mQuotaController.getRemainingExecutionTimeLocked( + SOURCE_USER_ID, SOURCE_PACKAGE)); + assertEquals(mQcConstants.ALLOWED_TIME_PER_PERIOD_RARE_MS, + mQuotaController.getTimeUntilQuotaConsumedLocked( + SOURCE_USER_ID, SOURCE_PACKAGE)); + } + + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession(now - mQcConstants.WINDOW_SIZE_FREQUENT_MS, + mQcConstants.ALLOWED_TIME_PER_PERIOD_FREQUENT_MS, 5), false); + setStandbyBucket(FREQUENT_INDEX); + synchronized (mQuotaController.mLock) { + assertEquals(0, + mQuotaController.getRemainingExecutionTimeLocked( + SOURCE_USER_ID, SOURCE_PACKAGE)); + assertEquals(mQcConstants.ALLOWED_TIME_PER_PERIOD_FREQUENT_MS, + mQuotaController.getTimeUntilQuotaConsumedLocked( + SOURCE_USER_ID, SOURCE_PACKAGE)); + } + + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession(now - mQcConstants.WINDOW_SIZE_WORKING_MS, + mQcConstants.ALLOWED_TIME_PER_PERIOD_WORKING_MS, 5), false); + setStandbyBucket(WORKING_INDEX); + synchronized (mQuotaController.mLock) { + assertEquals(0, + mQuotaController.getRemainingExecutionTimeLocked( + SOURCE_USER_ID, SOURCE_PACKAGE)); + assertEquals(mQcConstants.ALLOWED_TIME_PER_PERIOD_WORKING_MS, + mQuotaController.getTimeUntilQuotaConsumedLocked( + SOURCE_USER_ID, SOURCE_PACKAGE)); + } + + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession(now - mQcConstants.WINDOW_SIZE_ACTIVE_MS, + mQcConstants.ALLOWED_TIME_PER_PERIOD_ACTIVE_MS, 5), + false); + // ACTIVE window != allowed time. + setStandbyBucket(ACTIVE_INDEX); + synchronized (mQuotaController.mLock) { + assertEquals(0, + mQuotaController.getRemainingExecutionTimeLocked( + SOURCE_USER_ID, SOURCE_PACKAGE)); + assertEquals(mQcConstants.ALLOWED_TIME_PER_PERIOD_ACTIVE_MS, + mQuotaController.getTimeUntilQuotaConsumedLocked( + SOURCE_USER_ID, SOURCE_PACKAGE)); + } + + mQuotaController.saveTimingSession(SOURCE_USER_ID, SOURCE_PACKAGE, + createTimingSession(now - mQcConstants.WINDOW_SIZE_EXEMPTED_MS, + mQcConstants.ALLOWED_TIME_PER_PERIOD_EXEMPTED_MS, 5), + false); + // EXEMPTED window != allowed time + setStandbyBucket(EXEMPTED_INDEX); + synchronized (mQuotaController.mLock) { + assertEquals(0, + mQuotaController.getRemainingExecutionTimeLocked( + SOURCE_USER_ID, SOURCE_PACKAGE)); + assertEquals(mQcConstants.ALLOWED_TIME_PER_PERIOD_EXEMPTED_MS, mQuotaController.getTimeUntilQuotaConsumedLocked( SOURCE_USER_ID, SOURCE_PACKAGE)); } @@ -3733,7 +4359,7 @@ public class QuotaControllerTest { /** Tests that Timers count FOREGROUND_SERVICE jobs. */ @Test - @RequiresFlagsEnabled(FLAG_ENFORCE_QUOTA_POLICY_TO_FGS_JOBS) + @EnableFlags(Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_FGS_JOBS) public void testTimerTracking_Fgs() { setDischarging(); diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/DistractingPackageHelperTest.kt b/services/tests/mockingservicestests/src/com/android/server/pm/DistractingPackageHelperTest.kt index e131a98b52d0..de029e0d770f 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/DistractingPackageHelperTest.kt +++ b/services/tests/mockingservicestests/src/com/android/server/pm/DistractingPackageHelperTest.kt @@ -185,7 +185,8 @@ class DistractingPackageHelperTest : PackageHelperTestBase() { verify(pms, never()).scheduleWritePackageRestrictions(eq(TEST_USER_ID)) verify(broadcastHelper, never()).sendPackageBroadcast(eq( Intent.ACTION_DISTRACTING_PACKAGES_CHANGED), nullable(), nullable(), anyInt(), - nullable(), nullable(), any(), nullable(), nullable(), nullable(), nullable()) + nullable(), nullable(), any(), nullable(), nullable(), nullable(), nullable(), + nullable()) distractingPackageHelper.removeDistractingPackageRestrictions(pms.snapshotComputer(), arrayOfNulls(0), TEST_USER_ID) diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java index 43a8aa957fa5..124c41ed449f 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/StagingManagerTest.java @@ -741,7 +741,8 @@ public class StagingManagerTest { /* stagedSessionErrorCode */ PackageManager.INSTALL_UNKNOWN, /* stagedSessionErrorMessage */ "no error", /* preVerifiedDomains */ null, - /* verifierController */ null); + /* verifierController */ null, + /* verificationPolicy */ PackageInstaller.VERIFICATION_POLICY_BLOCK_FAIL_CLOSED); StagingManager.StagedSession stagedSession = spy(session.mStagedSession); doReturn(packageName).when(stagedSession).getPackageName(); diff --git a/services/tests/security/forensic/Android.bp b/services/tests/security/forensic/Android.bp new file mode 100644 index 000000000000..adc49040a004 --- /dev/null +++ b/services/tests/security/forensic/Android.bp @@ -0,0 +1,41 @@ +package { + default_team: "trendy_team_platform_security", + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +android_test { + name: "ForensicServiceTests", + srcs: [ + "src/**/*.java", + ], + + static_libs: [ + "androidx.test.core", + "androidx.test.rules", + "androidx.test.runner", + "compatibility-device-util-axt", + "frameworks-base-testutils", + "junit", + "platform-test-annotations", + "services.core", + "truth", + ], + + platform_apis: true, + + test_suites: [ + "device-tests", + "automotive-tests", + ], + + certificate: "platform", + dxflags: ["--multi-dex"], + optimize: { + enabled: false, + }, +} diff --git a/services/tests/security/forensic/AndroidManifest.xml b/services/tests/security/forensic/AndroidManifest.xml new file mode 100644 index 000000000000..40feb19aecba --- /dev/null +++ b/services/tests/security/forensic/AndroidManifest.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2024 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. +--> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.server.security.forensic.tests"> + + <application android:testOnly="true"> + <uses-library android:name="android.test.runner"/> + </application> + + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.server.security.forensic.tests" + android:label="Frameworks Forensic Services Tests"/> +</manifest> diff --git a/services/tests/security/forensic/AndroidTest.xml b/services/tests/security/forensic/AndroidTest.xml new file mode 100644 index 000000000000..bbe2e9c303ce --- /dev/null +++ b/services/tests/security/forensic/AndroidTest.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2024 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. +--> +<configuration description="Runs Frameworks Forensic Service tests."> + <option name="test-suite-tag" value="apct" /> + <option name="test-suite-tag" value="apct-instrumentation" /> + + <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller"> + <option name="cleanup-apks" value="true"/> + <option name="test-file-name" value="ForensicServiceTests.apk"/> + <option name="install-arg" value="-t" /> + </target_preparer> + + <option name="test-tag" value="ForensicServiceTests" /> + <test class="com.android.tradefed.testtype.InstrumentationTest" > + <option name="package" value="com.android.server.security.forensic.tests" /> + <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" /> + <option name="hidden-api-checks" value="false"/> + </test> +</configuration> diff --git a/services/tests/security/forensic/TEST_MAPPING b/services/tests/security/forensic/TEST_MAPPING new file mode 100644 index 000000000000..bd8b2ab7c41f --- /dev/null +++ b/services/tests/security/forensic/TEST_MAPPING @@ -0,0 +1,7 @@ +{ + "postsubmit": [ + { + "name": "ForensicServiceTests" + } + ] +} diff --git a/services/tests/security/forensic/src/com/android/server/security/forensic/ForensicServiceTest.java b/services/tests/security/forensic/src/com/android/server/security/forensic/ForensicServiceTest.java new file mode 100644 index 000000000000..7aa2e0f609a7 --- /dev/null +++ b/services/tests/security/forensic/src/com/android/server/security/forensic/ForensicServiceTest.java @@ -0,0 +1,387 @@ +/* + * Copyright (C) 2024 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.server.security.forensic; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.os.Looper; +import android.os.RemoteException; +import android.os.test.TestLooper; +import android.security.forensic.IForensicServiceCommandCallback; +import android.security.forensic.IForensicServiceStateCallback; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class ForensicServiceTest { + private static final int STATE_UNKNOWN = IForensicServiceStateCallback.State.UNKNOWN; + private static final int STATE_INVISIBLE = IForensicServiceStateCallback.State.INVISIBLE; + private static final int STATE_VISIBLE = IForensicServiceStateCallback.State.VISIBLE; + private static final int STATE_ENABLED = IForensicServiceStateCallback.State.ENABLED; + + private static final int ERROR_UNKNOWN = IForensicServiceCommandCallback.ErrorCode.UNKNOWN; + private static final int ERROR_PERMISSION_DENIED = + IForensicServiceCommandCallback.ErrorCode.PERMISSION_DENIED; + private static final int ERROR_INVALID_STATE_TRANSITION = + IForensicServiceCommandCallback.ErrorCode.INVALID_STATE_TRANSITION; + private static final int ERROR_BACKUP_TRANSPORT_UNAVAILABLE = + IForensicServiceCommandCallback.ErrorCode.BACKUP_TRANSPORT_UNAVAILABLE; + private static final int ERROR_DATA_SOURCE_UNAVAILABLE = + IForensicServiceCommandCallback.ErrorCode.DATA_SOURCE_UNAVAILABLE; + + @Mock + private Context mContextSpy; + + private ForensicService mForensicService; + private TestLooper mTestLooper; + private Looper mLooper; + + @SuppressLint("VisibleForTests") + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + mTestLooper = new TestLooper(); + mLooper = mTestLooper.getLooper(); + mForensicService = new ForensicService(new MockInjector(mContextSpy)); + mForensicService.onStart(); + } + + @Test + public void testMonitorState_Invisible() throws RemoteException { + StateCallback scb = new StateCallback(); + assertEquals(STATE_UNKNOWN, scb.mState); + mForensicService.getBinderService().monitorState(scb); + mTestLooper.dispatchAll(); + assertEquals(STATE_INVISIBLE, scb.mState); + } + + @Test + public void testMonitorState_Invisible_TwoMonitors() throws RemoteException { + StateCallback scb1 = new StateCallback(); + assertEquals(STATE_UNKNOWN, scb1.mState); + mForensicService.getBinderService().monitorState(scb1); + mTestLooper.dispatchAll(); + assertEquals(STATE_INVISIBLE, scb1.mState); + + StateCallback scb2 = new StateCallback(); + assertEquals(STATE_UNKNOWN, scb2.mState); + mForensicService.getBinderService().monitorState(scb2); + mTestLooper.dispatchAll(); + assertEquals(STATE_INVISIBLE, scb2.mState); + } + + @Test + public void testMakeVisible_FromInvisible() throws RemoteException { + StateCallback scb = new StateCallback(); + assertEquals(STATE_UNKNOWN, scb.mState); + mForensicService.getBinderService().monitorState(scb); + mTestLooper.dispatchAll(); + assertEquals(STATE_INVISIBLE, scb.mState); + + CommandCallback ccb = new CommandCallback(); + mForensicService.getBinderService().makeVisible(ccb); + mTestLooper.dispatchAll(); + assertEquals(STATE_VISIBLE, scb.mState); + assertNull(ccb.mErrorCode); + } + + @Test + public void testMakeVisible_FromInvisible_TwoMonitors() throws RemoteException { + mForensicService.setState(STATE_INVISIBLE); + StateCallback scb1 = new StateCallback(); + StateCallback scb2 = new StateCallback(); + mForensicService.getBinderService().monitorState(scb1); + mForensicService.getBinderService().monitorState(scb2); + mTestLooper.dispatchAll(); + assertEquals(STATE_INVISIBLE, scb1.mState); + assertEquals(STATE_INVISIBLE, scb2.mState); + + CommandCallback ccb = new CommandCallback(); + mForensicService.getBinderService().makeVisible(ccb); + mTestLooper.dispatchAll(); + assertEquals(STATE_VISIBLE, scb1.mState); + assertEquals(STATE_VISIBLE, scb2.mState); + assertNull(ccb.mErrorCode); + } + + @Test + public void testMakeVisible_FromVisible_TwoMonitors() throws RemoteException { + mForensicService.setState(STATE_VISIBLE); + StateCallback scb1 = new StateCallback(); + StateCallback scb2 = new StateCallback(); + mForensicService.getBinderService().monitorState(scb1); + mForensicService.getBinderService().monitorState(scb2); + mTestLooper.dispatchAll(); + assertEquals(STATE_VISIBLE, scb1.mState); + assertEquals(STATE_VISIBLE, scb2.mState); + + CommandCallback ccb = new CommandCallback(); + mForensicService.getBinderService().makeVisible(ccb); + mTestLooper.dispatchAll(); + assertEquals(STATE_VISIBLE, scb1.mState); + assertEquals(STATE_VISIBLE, scb2.mState); + assertNull(ccb.mErrorCode); + } + + @Test + public void testMakeVisible_FromEnabled_TwoMonitors() throws RemoteException { + mForensicService.setState(STATE_ENABLED); + StateCallback scb1 = new StateCallback(); + StateCallback scb2 = new StateCallback(); + mForensicService.getBinderService().monitorState(scb1); + mForensicService.getBinderService().monitorState(scb2); + mTestLooper.dispatchAll(); + assertEquals(STATE_ENABLED, scb1.mState); + assertEquals(STATE_ENABLED, scb2.mState); + + CommandCallback ccb = new CommandCallback(); + mForensicService.getBinderService().makeVisible(ccb); + mTestLooper.dispatchAll(); + assertEquals(STATE_ENABLED, scb1.mState); + assertEquals(STATE_ENABLED, scb2.mState); + assertNotNull(ccb.mErrorCode); + assertEquals(ERROR_INVALID_STATE_TRANSITION, ccb.mErrorCode.intValue()); + } + + @Test + public void testMakeInvisible_FromInvisible_TwoMonitors() throws RemoteException { + mForensicService.setState(STATE_INVISIBLE); + StateCallback scb1 = new StateCallback(); + StateCallback scb2 = new StateCallback(); + mForensicService.getBinderService().monitorState(scb1); + mForensicService.getBinderService().monitorState(scb2); + mTestLooper.dispatchAll(); + assertEquals(STATE_INVISIBLE, scb1.mState); + assertEquals(STATE_INVISIBLE, scb2.mState); + + CommandCallback ccb = new CommandCallback(); + mForensicService.getBinderService().makeInvisible(ccb); + mTestLooper.dispatchAll(); + assertEquals(STATE_INVISIBLE, scb1.mState); + assertEquals(STATE_INVISIBLE, scb2.mState); + assertNull(ccb.mErrorCode); + } + + @Test + public void testMakeInvisible_FromVisible_TwoMonitors() throws RemoteException { + mForensicService.setState(STATE_VISIBLE); + StateCallback scb1 = new StateCallback(); + StateCallback scb2 = new StateCallback(); + mForensicService.getBinderService().monitorState(scb1); + mForensicService.getBinderService().monitorState(scb2); + mTestLooper.dispatchAll(); + assertEquals(STATE_VISIBLE, scb1.mState); + assertEquals(STATE_VISIBLE, scb2.mState); + + CommandCallback ccb = new CommandCallback(); + mForensicService.getBinderService().makeInvisible(ccb); + mTestLooper.dispatchAll(); + assertEquals(STATE_INVISIBLE, scb1.mState); + assertEquals(STATE_INVISIBLE, scb2.mState); + assertNull(ccb.mErrorCode); + } + + @Test + public void testMakeInvisible_FromEnabled_TwoMonitors() throws RemoteException { + mForensicService.setState(STATE_ENABLED); + StateCallback scb1 = new StateCallback(); + StateCallback scb2 = new StateCallback(); + mForensicService.getBinderService().monitorState(scb1); + mForensicService.getBinderService().monitorState(scb2); + mTestLooper.dispatchAll(); + assertEquals(STATE_ENABLED, scb1.mState); + assertEquals(STATE_ENABLED, scb2.mState); + + CommandCallback ccb = new CommandCallback(); + mForensicService.getBinderService().makeInvisible(ccb); + mTestLooper.dispatchAll(); + assertEquals(STATE_INVISIBLE, scb1.mState); + assertEquals(STATE_INVISIBLE, scb2.mState); + assertNull(ccb.mErrorCode); + } + + + @Test + public void testEnable_FromInvisible_TwoMonitors() throws RemoteException { + mForensicService.setState(STATE_INVISIBLE); + StateCallback scb1 = new StateCallback(); + StateCallback scb2 = new StateCallback(); + mForensicService.getBinderService().monitorState(scb1); + mForensicService.getBinderService().monitorState(scb2); + mTestLooper.dispatchAll(); + assertEquals(STATE_INVISIBLE, scb1.mState); + assertEquals(STATE_INVISIBLE, scb2.mState); + + CommandCallback ccb = new CommandCallback(); + mForensicService.getBinderService().enable(ccb); + mTestLooper.dispatchAll(); + assertEquals(STATE_INVISIBLE, scb1.mState); + assertEquals(STATE_INVISIBLE, scb2.mState); + assertNotNull(ccb.mErrorCode); + assertEquals(ERROR_INVALID_STATE_TRANSITION, ccb.mErrorCode.intValue()); + } + + @Test + public void testEnable_FromVisible_TwoMonitors() throws RemoteException { + mForensicService.setState(STATE_VISIBLE); + StateCallback scb1 = new StateCallback(); + StateCallback scb2 = new StateCallback(); + mForensicService.getBinderService().monitorState(scb1); + mForensicService.getBinderService().monitorState(scb2); + mTestLooper.dispatchAll(); + assertEquals(STATE_VISIBLE, scb1.mState); + assertEquals(STATE_VISIBLE, scb2.mState); + + CommandCallback ccb = new CommandCallback(); + mForensicService.getBinderService().enable(ccb); + mTestLooper.dispatchAll(); + assertEquals(STATE_ENABLED, scb1.mState); + assertEquals(STATE_ENABLED, scb2.mState); + assertNull(ccb.mErrorCode); + } + + @Test + public void testEnable_FromEnabled_TwoMonitors() throws RemoteException { + mForensicService.setState(STATE_ENABLED); + StateCallback scb1 = new StateCallback(); + StateCallback scb2 = new StateCallback(); + mForensicService.getBinderService().monitorState(scb1); + mForensicService.getBinderService().monitorState(scb2); + mTestLooper.dispatchAll(); + assertEquals(STATE_ENABLED, scb1.mState); + assertEquals(STATE_ENABLED, scb2.mState); + + CommandCallback ccb = new CommandCallback(); + mForensicService.getBinderService().enable(ccb); + mTestLooper.dispatchAll(); + assertEquals(STATE_ENABLED, scb1.mState); + assertEquals(STATE_ENABLED, scb2.mState); + assertNull(ccb.mErrorCode); + } + + @Test + public void testDisable_FromInvisible_TwoMonitors() throws RemoteException { + mForensicService.setState(STATE_INVISIBLE); + StateCallback scb1 = new StateCallback(); + StateCallback scb2 = new StateCallback(); + mForensicService.getBinderService().monitorState(scb1); + mForensicService.getBinderService().monitorState(scb2); + mTestLooper.dispatchAll(); + assertEquals(STATE_INVISIBLE, scb1.mState); + assertEquals(STATE_INVISIBLE, scb2.mState); + + CommandCallback ccb = new CommandCallback(); + mForensicService.getBinderService().disable(ccb); + mTestLooper.dispatchAll(); + assertEquals(STATE_INVISIBLE, scb1.mState); + assertEquals(STATE_INVISIBLE, scb2.mState); + assertNotNull(ccb.mErrorCode); + assertEquals(ERROR_INVALID_STATE_TRANSITION, ccb.mErrorCode.intValue()); + } + + @Test + public void testDisable_FromVisible_TwoMonitors() throws RemoteException { + mForensicService.setState(STATE_VISIBLE); + StateCallback scb1 = new StateCallback(); + StateCallback scb2 = new StateCallback(); + mForensicService.getBinderService().monitorState(scb1); + mForensicService.getBinderService().monitorState(scb2); + mTestLooper.dispatchAll(); + assertEquals(STATE_VISIBLE, scb1.mState); + assertEquals(STATE_VISIBLE, scb2.mState); + + CommandCallback ccb = new CommandCallback(); + mForensicService.getBinderService().disable(ccb); + mTestLooper.dispatchAll(); + assertEquals(STATE_VISIBLE, scb1.mState); + assertEquals(STATE_VISIBLE, scb2.mState); + assertNull(ccb.mErrorCode); + } + + @Test + public void testDisable_FromEnabled_TwoMonitors() throws RemoteException { + mForensicService.setState(STATE_ENABLED); + StateCallback scb1 = new StateCallback(); + StateCallback scb2 = new StateCallback(); + mForensicService.getBinderService().monitorState(scb1); + mForensicService.getBinderService().monitorState(scb2); + mTestLooper.dispatchAll(); + assertEquals(STATE_ENABLED, scb1.mState); + assertEquals(STATE_ENABLED, scb2.mState); + + CommandCallback ccb = new CommandCallback(); + mForensicService.getBinderService().disable(ccb); + mTestLooper.dispatchAll(); + assertEquals(STATE_VISIBLE, scb1.mState); + assertEquals(STATE_VISIBLE, scb2.mState); + assertNull(ccb.mErrorCode); + } + + private class MockInjector implements ForensicService.Injector { + private final Context mContext; + + MockInjector(Context context) { + mContext = context; + } + + @Override + public Context getContext() { + return mContext; + } + + + @Override + public Looper getLooper() { + return mLooper; + } + + } + + private static class StateCallback extends IForensicServiceStateCallback.Stub { + int mState = STATE_UNKNOWN; + + @Override + public void onStateChange(int state) throws RemoteException { + mState = state; + } + } + + private static class CommandCallback extends IForensicServiceCommandCallback.Stub { + Integer mErrorCode = null; + + public void reset() { + mErrorCode = null; + } + + @Override + public void onSuccess() throws RemoteException { + + } + + @Override + public void onFailure(int errorCode) throws RemoteException { + mErrorCode = errorCode; + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java index b745e6a7d4a5..e5831b326de5 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/FullScreenMagnificationGestureHandlerTest.java @@ -1417,7 +1417,7 @@ public class FullScreenMagnificationGestureHandlerTest { } @Test - @RequiresFlagsDisabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE) + @RequiresFlagsDisabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_BUGFIX) public void testMouseMoveEventsDoNotMoveMagnifierViewport() { runMoveEventsDoNotMoveMagnifierViewport(InputDevice.SOURCE_MOUSE); } @@ -1471,55 +1471,55 @@ public class FullScreenMagnificationGestureHandlerTest { } @Test - @RequiresFlagsDisabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE) + @RequiresFlagsDisabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_BUGFIX) public void testMouseHoverMoveEventsDoNotMoveMagnifierViewport() { runHoverMoveEventsDoNotMoveMagnifierViewport(InputDevice.SOURCE_MOUSE); } @Test - @RequiresFlagsDisabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE) + @RequiresFlagsDisabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_BUGFIX) public void testStylusHoverMoveEventsDoNotMoveMagnifierViewport() { runHoverMoveEventsDoNotMoveMagnifierViewport(InputDevice.SOURCE_STYLUS); } @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE) + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_BUGFIX) public void testMouseHoverMoveEventsMoveMagnifierViewport() { runHoverMovesViewportTest(InputDevice.SOURCE_MOUSE); } @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE) + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_BUGFIX) public void testStylusHoverMoveEventsMoveMagnifierViewport() { runHoverMovesViewportTest(InputDevice.SOURCE_STYLUS); } @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE) + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_BUGFIX) public void testMouseDownEventsDoNotMoveMagnifierViewport() { runDownDoesNotMoveViewportTest(InputDevice.SOURCE_MOUSE); } @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE) + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_BUGFIX) public void testStylusDownEventsDoNotMoveMagnifierViewport() { runDownDoesNotMoveViewportTest(InputDevice.SOURCE_STYLUS); } @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE) + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_BUGFIX) public void testMouseUpEventsDoNotMoveMagnifierViewport() { runUpDoesNotMoveViewportTest(InputDevice.SOURCE_MOUSE); } @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE) + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_BUGFIX) public void testStylusUpEventsDoNotMoveMagnifierViewport() { runUpDoesNotMoveViewportTest(InputDevice.SOURCE_STYLUS); } @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE) + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_BUGFIX) public void testMouseMoveEventsMoveMagnifierViewport() { final EventCaptor eventCaptor = new EventCaptor(); mMgh.setNext(eventCaptor); diff --git a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationGestureHandlerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationGestureHandlerTest.java index d80a1f056e94..45c157d1c1a0 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationGestureHandlerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/magnification/MagnificationGestureHandlerTest.java @@ -93,7 +93,7 @@ public class MagnificationGestureHandlerTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE) + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_BUGFIX) public void onMotionEvent_isFromMouse_handleMouseOrStylusEvent() { final MotionEvent mouseEvent = MotionEvent.obtain(0, 0, ACTION_HOVER_MOVE, 0, 0, 0); mouseEvent.setSource(InputDevice.SOURCE_MOUSE); @@ -108,7 +108,7 @@ public class MagnificationGestureHandlerTest { } @Test - @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE) + @RequiresFlagsEnabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_BUGFIX) public void onMotionEvent_isFromStylus_handleMouseOrStylusEvent() { final MotionEvent stylusEvent = MotionEvent.obtain(0, 0, ACTION_HOVER_MOVE, 0, 0, 0); stylusEvent.setSource(InputDevice.SOURCE_STYLUS); @@ -123,7 +123,7 @@ public class MagnificationGestureHandlerTest { } @Test - @RequiresFlagsDisabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE) + @RequiresFlagsDisabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_BUGFIX) public void onMotionEvent_isFromMouse_handleMouseOrStylusEventNotCalled() { final MotionEvent mouseEvent = MotionEvent.obtain(0, 0, ACTION_HOVER_MOVE, 0, 0, 0); mouseEvent.setSource(InputDevice.SOURCE_MOUSE); @@ -138,7 +138,7 @@ public class MagnificationGestureHandlerTest { } @Test - @RequiresFlagsDisabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE) + @RequiresFlagsDisabled(Flags.FLAG_ENABLE_MAGNIFICATION_FOLLOWS_MOUSE_BUGFIX) public void onMotionEvent_isFromStylus_handleMouseOrStylusEventNotCalled() { final MotionEvent stylusEvent = MotionEvent.obtain(0, 0, ACTION_HOVER_MOVE, 0, 0, 0); stylusEvent.setSource(InputDevice.SOURCE_STYLUS); diff --git a/services/tests/servicestests/src/com/android/server/integrity/AppIntegrityManagerServiceImplTest.java b/services/tests/servicestests/src/com/android/server/integrity/AppIntegrityManagerServiceImplTest.java index d1f6c2f9f1f0..9c6412b81b34 100644 --- a/services/tests/servicestests/src/com/android/server/integrity/AppIntegrityManagerServiceImplTest.java +++ b/services/tests/servicestests/src/com/android/server/integrity/AppIntegrityManagerServiceImplTest.java @@ -67,10 +67,8 @@ import android.provider.Settings; import androidx.test.InstrumentationRegistry; import com.android.internal.R; -import com.android.internal.pm.parsing.PackageParser2; import com.android.server.compat.PlatformCompat; import com.android.server.integrity.model.IntegrityCheckResult; -import com.android.server.pm.parsing.TestPackageParser2; import com.android.server.testutils.TestUtils; import org.junit.After; @@ -140,8 +138,6 @@ public class AppIntegrityManagerServiceImplTest { @Mock IntegrityFileManager mIntegrityFileManager; @Mock Handler mHandler; - private Supplier<PackageParser2> mParserSupplier = TestPackageParser2::new; - private final Context mRealContext = InstrumentationRegistry.getTargetContext(); private PackageManager mSpyPackageManager; @@ -173,7 +169,6 @@ public class AppIntegrityManagerServiceImplTest { new AppIntegrityManagerServiceImpl( mMockContext, mPackageManagerInternal, - mParserSupplier, mIntegrityFileManager, mHandler); diff --git a/services/tests/servicestests/src/com/android/server/people/data/CallLogQueryHelperTest.java b/services/tests/servicestests/src/com/android/server/people/data/CallLogQueryHelperTest.java index a54501029712..f45eddcf4480 100644 --- a/services/tests/servicestests/src/com/android/server/people/data/CallLogQueryHelperTest.java +++ b/services/tests/servicestests/src/com/android/server/people/data/CallLogQueryHelperTest.java @@ -24,6 +24,7 @@ import static org.mockito.Mockito.when; import android.database.Cursor; import android.database.MatrixCursor; +import android.database.sqlite.SQLiteException; import android.net.Uri; import android.provider.CallLog.Calls; import android.test.mock.MockContentProvider; @@ -58,6 +59,7 @@ public final class CallLogQueryHelperTest { private MatrixCursor mCursor; private EventConsumer mEventConsumer; private CallLogQueryHelper mHelper; + private CallLogContentProvider mCallLogContentProvider; @Before public void setUp() { @@ -66,7 +68,8 @@ public final class CallLogQueryHelperTest { mCursor = new MatrixCursor(CALL_LOG_COLUMNS); MockContentResolver contentResolver = new MockContentResolver(); - contentResolver.addProvider(CALL_LOG_AUTHORITY, new CallLogContentProvider()); + mCallLogContentProvider = new CallLogContentProvider(); + contentResolver.addProvider(CALL_LOG_AUTHORITY, mCallLogContentProvider); when(mContext.getContentResolver()).thenReturn(contentResolver); mEventConsumer = new EventConsumer(); @@ -80,6 +83,12 @@ public final class CallLogQueryHelperTest { } @Test + public void testQueryWithSQLiteException() { + mCallLogContentProvider.setThrowSQLiteException(true); + assertFalse(mHelper.querySince(50L)); + } + + @Test public void testQueryIncomingCall() { mCursor.addRow(new Object[] { NORMALIZED_PHONE_NUMBER, /* date= */ 100L, /* duration= */ 30L, @@ -159,11 +168,20 @@ public final class CallLogQueryHelperTest { } private class CallLogContentProvider extends MockContentProvider { + private boolean mThrowSQLiteException = false; @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + if (mThrowSQLiteException) { + throw new SQLiteException(); + } + return mCursor; } + + public void setThrowSQLiteException(boolean throwException) { + this.mThrowSQLiteException = throwException; + } } } diff --git a/services/tests/servicestests/src/com/android/server/people/data/ContactsQueryHelperTest.java b/services/tests/servicestests/src/com/android/server/people/data/ContactsQueryHelperTest.java index 16a02b678511..1daee39ce9de 100644 --- a/services/tests/servicestests/src/com/android/server/people/data/ContactsQueryHelperTest.java +++ b/services/tests/servicestests/src/com/android/server/people/data/ContactsQueryHelperTest.java @@ -99,6 +99,14 @@ public final class ContactsQueryHelperTest { } @Test + public void testQueryOtherException_returnsFalse() { + contentProvider.setThrowOtherException(true); + + Uri contactUri = Uri.withAppendedPath(Contacts.CONTENT_LOOKUP_URI, CONTACT_LOOKUP_KEY); + assertFalse(mHelper.query(contactUri.toString())); + } + + @Test public void testQueryIllegalArgumentException_returnsFalse() { contentProvider.setThrowIllegalArgumentException(true); @@ -152,6 +160,13 @@ public final class ContactsQueryHelperTest { } @Test + public void testQueryWithPhoneNumber_otherExceptionReturnsFalse() { + contentProvider.setThrowOtherException(true); + String contactUri = "tel:" + PHONE_NUMBER; + assertFalse(mHelper.query(contactUri)); + } + + @Test public void testQueryWithEmail() { mContactsLookupCursor.addRow(new Object[] { /* id= */ 11, CONTACT_LOOKUP_KEY, /* starred= */ 1, /* hasPhoneNumber= */ 0 }); @@ -188,6 +203,7 @@ public final class ContactsQueryHelperTest { private Map<Uri, Cursor> mUriPrefixToCursorMap = new ArrayMap<>(); private boolean mThrowSQLiteException = false; private boolean mThrowIllegalArgumentException = false; + private boolean mThrowOtherException = false; @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, @@ -198,6 +214,9 @@ public final class ContactsQueryHelperTest { if (mThrowIllegalArgumentException) { throw new IllegalArgumentException(); } + if (mThrowOtherException) { + throw new ArrayIndexOutOfBoundsException(); + } for (Uri prefixUri : mUriPrefixToCursorMap.keySet()) { if (uri.isPathPrefixMatch(prefixUri)) { @@ -215,6 +234,10 @@ public final class ContactsQueryHelperTest { this.mThrowIllegalArgumentException = throwException; } + public void setThrowOtherException(boolean throwException) { + this.mThrowOtherException = throwException; + } + private void registerCursor(Uri uriPrefix, Cursor cursor) { mUriPrefixToCursorMap.put(uriPrefix, cursor); } diff --git a/services/tests/servicestests/src/com/android/server/people/data/MmsQueryHelperTest.java b/services/tests/servicestests/src/com/android/server/people/data/MmsQueryHelperTest.java index 7730890e1486..9f4a43df6de8 100644 --- a/services/tests/servicestests/src/com/android/server/people/data/MmsQueryHelperTest.java +++ b/services/tests/servicestests/src/com/android/server/people/data/MmsQueryHelperTest.java @@ -23,6 +23,7 @@ import static org.mockito.Mockito.when; import android.database.Cursor; import android.database.MatrixCursor; +import android.database.sqlite.SQLiteException; import android.net.Uri; import android.provider.Telephony.BaseMmsColumns; import android.provider.Telephony.Mms; @@ -63,6 +64,7 @@ public final class MmsQueryHelperTest { private final List<MatrixCursor> mAddrCursors = new ArrayList<>(); private EventConsumer mEventConsumer; private MmsQueryHelper mHelper; + private MmsContentProvider mMmsContentProvider; @Before public void setUp() { @@ -73,7 +75,8 @@ public final class MmsQueryHelperTest { mAddrCursors.add(new MatrixCursor(ADDR_COLUMNS)); MockContentResolver contentResolver = new MockContentResolver(); - contentResolver.addProvider(MMS_AUTHORITY, new MmsContentProvider()); + mMmsContentProvider = new MmsContentProvider(); + contentResolver.addProvider(MMS_AUTHORITY, mMmsContentProvider); when(mContext.getContentResolver()).thenReturn(contentResolver); mEventConsumer = new EventConsumer(); @@ -87,6 +90,12 @@ public final class MmsQueryHelperTest { } @Test + public void testQueryWithSQLiteException() { + mMmsContentProvider.setThrowSQLiteException(true); + assertFalse(mHelper.querySince(50_000L)); + } + + @Test public void testQueryIncomingMessage() { mMmsCursor.addRow(new Object[] { /* id= */ 0, /* date= */ 100L, /* msgBox= */ BaseMmsColumns.MESSAGE_BOX_INBOX }); @@ -159,10 +168,15 @@ public final class MmsQueryHelperTest { } private class MmsContentProvider extends MockContentProvider { + private boolean mThrowSQLiteException = false; @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + if (mThrowSQLiteException) { + throw new SQLiteException(); + } + List<String> segments = uri.getPathSegments(); if (segments.size() == 2 && "addr".equals(segments.get(1))) { int messageId = Integer.valueOf(segments.get(0)); @@ -170,5 +184,9 @@ public final class MmsQueryHelperTest { } return mMmsCursor; } + + public void setThrowSQLiteException(boolean throwException) { + this.mThrowSQLiteException = throwException; + } } } diff --git a/services/tests/servicestests/src/com/android/server/people/data/SmsQueryHelperTest.java b/services/tests/servicestests/src/com/android/server/people/data/SmsQueryHelperTest.java index 5cb8cb4fe9f1..09a0dff77eb0 100644 --- a/services/tests/servicestests/src/com/android/server/people/data/SmsQueryHelperTest.java +++ b/services/tests/servicestests/src/com/android/server/people/data/SmsQueryHelperTest.java @@ -23,6 +23,7 @@ import static org.mockito.Mockito.when; import android.database.Cursor; import android.database.MatrixCursor; +import android.database.sqlite.SQLiteException; import android.net.Uri; import android.provider.Telephony.Sms; import android.provider.Telephony.TextBasedSmsColumns; @@ -59,6 +60,7 @@ public final class SmsQueryHelperTest { private MatrixCursor mSmsCursor; private EventConsumer mEventConsumer; private SmsQueryHelper mHelper; + private SmsContentProvider mSmsContentProvider; @Before public void setUp() { @@ -67,7 +69,8 @@ public final class SmsQueryHelperTest { mSmsCursor = new MatrixCursor(SMS_COLUMNS); MockContentResolver contentResolver = new MockContentResolver(); - contentResolver.addProvider(SMS_AUTHORITY, new SmsContentProvider()); + mSmsContentProvider = new SmsContentProvider(); + contentResolver.addProvider(SMS_AUTHORITY, mSmsContentProvider); when(mContext.getContentResolver()).thenReturn(contentResolver); mEventConsumer = new EventConsumer(); @@ -130,6 +133,12 @@ public final class SmsQueryHelperTest { assertEquals(110L, events.get(1).getTimestamp()); } + @Test + public void testQueryWithSQLiteException() { + mSmsContentProvider.setThrowSQLiteException(true); + assertFalse(mHelper.querySince(50L)); + } + private class EventConsumer implements BiConsumer<String, Event> { private final Map<String, List<Event>> mEventMap = new ArrayMap<>(); @@ -141,11 +150,19 @@ public final class SmsQueryHelperTest { } private class SmsContentProvider extends MockContentProvider { + private boolean mThrowSQLiteException = false; @Override public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) { + if (mThrowSQLiteException) { + throw new SQLiteException(); + } return mSmsCursor; } + + public void setThrowSQLiteException(boolean throwException) { + this.mThrowSQLiteException = throwException; + } } } diff --git a/services/tests/wmtests/src/com/android/server/wm/InsetsSourceProviderTest.java b/services/tests/wmtests/src/com/android/server/wm/InsetsSourceProviderTest.java index 457058849fca..79967b861ea5 100644 --- a/services/tests/wmtests/src/com/android/server/wm/InsetsSourceProviderTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/InsetsSourceProviderTest.java @@ -30,6 +30,7 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import android.graphics.Insets; +import android.graphics.Point; import android.graphics.Rect; import android.platform.test.annotations.Presubmit; import android.view.InsetsSource; @@ -260,6 +261,27 @@ public class InsetsSourceProviderTest extends WindowTestsBase { } @Test + public void testUpdateInsetsControlPosition() { + final WindowState target = createWindow(null, TYPE_APPLICATION, "target"); + + final WindowState ime1 = createWindow(null, TYPE_INPUT_METHOD, "ime1"); + ime1.getFrame().set(new Rect(0, 0, 0, 0)); + mImeProvider.setWindowContainer(ime1, null, null); + mImeProvider.updateControlForTarget(target, false /* force */, null /* statsToken */); + ime1.getFrame().set(new Rect(0, 400, 500, 500)); + mImeProvider.updateInsetsControlPosition(ime1); + assertEquals(new Point(0, 400), mImeProvider.getControl(target).getSurfacePosition()); + + final WindowState ime2 = createWindow(null, TYPE_INPUT_METHOD, "ime2"); + ime2.getFrame().set(new Rect(0, 0, 0, 0)); + mImeProvider.setWindowContainer(ime2, null, null); + mImeProvider.updateControlForTarget(target, false /* force */, null /* statsToken */); + ime2.getFrame().set(new Rect(0, 400, 500, 500)); + mImeProvider.updateInsetsControlPosition(ime2); + assertEquals(new Point(0, 400), mImeProvider.getControl(target).getSurfacePosition()); + } + + @Test public void testSetRequestedVisibleTypes() { final WindowState statusBar = createWindow(null, TYPE_APPLICATION, "statusBar"); final WindowState target = createWindow(null, TYPE_APPLICATION, "target"); |