diff options
447 files changed, 12121 insertions, 2363 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index 98b62b3e2046..2c78f935c00d 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -60,6 +60,7 @@ aconfig_srcjars = [ ":android.webkit.flags-aconfig-java{.generated_srcjars}", ":android.widget.flags-aconfig-java{.generated_srcjars}", ":audio-framework-aconfig", + ":backup_flags_lib{.generated_srcjars}", ":camera_platform_flags_core_java_lib{.generated_srcjars}", ":com.android.hardware.input-aconfig-java{.generated_srcjars}", ":com.android.input.flags-aconfig-java{.generated_srcjars}", @@ -1092,4 +1093,11 @@ java_aconfig_library { name: "android.crashrecovery.flags-aconfig-java", aconfig_declarations: "android.crashrecovery.flags-aconfig", defaults: ["framework-minus-apex-aconfig-java-defaults"], -}
\ No newline at end of file +} + +// Backup +java_aconfig_library { + name: "backup_flags_lib", + aconfig_declarations: "backup_flags", + defaults: ["framework-minus-apex-aconfig-java-defaults"], +} diff --git a/apct-tests/perftests/core/src/android/input/MotionPredictorBenchmark.kt b/apct-tests/perftests/core/src/android/input/MotionPredictorBenchmark.kt index aadbc2319a62..add0a086201b 100644 --- a/apct-tests/perftests/core/src/android/input/MotionPredictorBenchmark.kt +++ b/apct-tests/perftests/core/src/android/input/MotionPredictorBenchmark.kt @@ -16,8 +16,6 @@ package android.input -import android.content.Context -import android.content.res.Resources import android.os.SystemProperties import android.perftests.utils.PerfStatusReporter import android.view.InputDevice @@ -38,8 +36,6 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mockito.mock -import org.mockito.Mockito.`when` import java.time.Duration @@ -68,18 +64,6 @@ private fun getStylusMotionEvent( InputDevice.SOURCE_STYLUS, /*flags=*/0) } -private fun getPredictionContext(offset: Duration, enablePrediction: Boolean): Context { - val context = mock(Context::class.java) - val resources: Resources = mock(Resources::class.java) - `when`(context.getResources()).thenReturn(resources) - `when`(resources.getInteger( - com.android.internal.R.integer.config_motionPredictionOffsetNanos)).thenReturn( - offset.toNanos().toInt()) - `when`(resources.getBoolean( - com.android.internal.R.bool.config_enableMotionPrediction)).thenReturn(enablePrediction) - return context -} - @RunWith(AndroidJUnit4::class) @LargeTest class MotionPredictorBenchmark { @@ -115,7 +99,7 @@ class MotionPredictorBenchmark { var eventPosition = 0f val positionInterval = 10f - val predictor = MotionPredictor(getPredictionContext(offset, /*enablePrediction=*/true)) + val predictor = MotionPredictor(/*isPredictionEnabled=*/true, offset.toNanos().toInt()) // ACTION_DOWN t=0 x=0 y=0 predictor.record(getStylusMotionEvent( eventTime, ACTION_DOWN, /*x=*/eventPosition, /*y=*/eventPosition)) @@ -141,12 +125,11 @@ class MotionPredictorBenchmark { */ @Test fun timeCreatePredictor() { - val context = getPredictionContext( - /*offset=*/Duration.ofMillis(20), /*enablePrediction=*/true) + val offsetNanos = Duration.ofMillis(20).toNanos().toInt() val state = perfStatusReporter.getBenchmarkState() while (state.keepRunning()) { - MotionPredictor(context) + MotionPredictor(/*isPredictionEnabled=*/true, offsetNanos) } } } diff --git a/apex/jobscheduler/framework/java/com/android/server/job/JobSchedulerInternal.java b/apex/jobscheduler/framework/java/com/android/server/job/JobSchedulerInternal.java index 6c8af39015f5..ae98fe14fbe6 100644 --- a/apex/jobscheduler/framework/java/com/android/server/job/JobSchedulerInternal.java +++ b/apex/jobscheduler/framework/java/com/android/server/job/JobSchedulerInternal.java @@ -77,6 +77,12 @@ public interface JobSchedulerInternal { @NonNull String notificationChannel, int userId, @NonNull String packageName); /** + * @return {@code true} if the given package holds the + * {@link android.Manifest.permission.RUN_BACKUP_JOBS} permission. + */ + boolean hasRunBackupJobsPermission(@NonNull String packageName, int packageUid); + + /** * Report a snapshot of sync-related jobs back to the sync manager */ JobStorePersistStats getPersistStats(); diff --git a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java index fc193d8147b5..57467e3cc83d 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java +++ b/apex/jobscheduler/service/java/com/android/server/job/JobSchedulerService.java @@ -4197,6 +4197,11 @@ public class JobSchedulerService extends com.android.server.SystemService } @Override + public boolean hasRunBackupJobsPermission(@NonNull String packageName, int packageUid) { + return JobSchedulerService.this.hasRunBackupJobsPermission(packageName, packageUid); + } + + @Override public JobStorePersistStats getPersistStats() { synchronized (mLock) { return new JobStorePersistStats(mJobs.getPersistStats()); @@ -4359,6 +4364,22 @@ public class JobSchedulerService extends com.android.server.SystemService } /** + * Returns whether the app holds the {@link Manifest.permission.RUN_BACKUP_JOBS} permission. + */ + private boolean hasRunBackupJobsPermission(@NonNull String packageName, int packageUid) { + if (packageName == null) { + Slog.wtfStack(TAG, + "Expected a non-null package name when calling hasRunBackupJobsPermission"); + return false; + } + + return PermissionChecker.checkPermissionForPreflight(getTestableContext(), + android.Manifest.permission.RUN_BACKUP_JOBS, + PermissionChecker.PID_UNKNOWN, packageUid, packageName) + == PermissionChecker.PERMISSION_GRANTED; + } + + /** * Binder stub trampoline implementation */ final class JobSchedulerStub extends IJobScheduler.Stub { diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java index a4df5d829281..2ea980d40287 100644 --- a/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java +++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/JobStatus.java @@ -1222,21 +1222,25 @@ public final class JobStatus { return ACTIVE_INDEX; } - final int bucketWithMediaExemption; - if (actualBucket != RESTRICTED_INDEX && actualBucket != NEVER_INDEX - && mHasMediaBackupExemption) { + final boolean isEligibleAsBackupJob = job.getTriggerContentUris() != null + && job.getRequiredNetwork() != null + && !job.hasLateConstraint() + && mJobSchedulerInternal.hasRunBackupJobsPermission(sourcePackageName, sourceUid); + final boolean isBackupExempt = mHasMediaBackupExemption || isEligibleAsBackupJob; + final int bucketWithBackupExemption; + if (actualBucket != RESTRICTED_INDEX && actualBucket != NEVER_INDEX && isBackupExempt) { // Treat it as if it's at most WORKING_INDEX (lower index grants higher quota) since // media backup jobs are important to the user, and the source package may not have // been used directly in a while. - bucketWithMediaExemption = Math.min(WORKING_INDEX, actualBucket); + bucketWithBackupExemption = Math.min(WORKING_INDEX, actualBucket); } else { - bucketWithMediaExemption = actualBucket; + bucketWithBackupExemption = actualBucket; } // If the app is considered buggy, but hasn't yet been put in the RESTRICTED bucket // (potentially because it's used frequently by the user), limit its effective bucket // so that it doesn't get to run as much as a normal ACTIVE app. - if (isBuggy && bucketWithMediaExemption < WORKING_INDEX) { + if (isBuggy && bucketWithBackupExemption < WORKING_INDEX) { if (!mIsDowngradedDueToBuggyApp) { // Safety check to avoid logging multiple times for the same job. Counter.logIncrementWithUid( @@ -1246,7 +1250,7 @@ public final class JobStatus { } return WORKING_INDEX; } - return bucketWithMediaExemption; + return bucketWithBackupExemption; } /** Returns the real standby bucket of the job. */ diff --git a/core/TEST_MAPPING b/core/TEST_MAPPING index 24ba5c4e1281..f1e4d0ee4906 100644 --- a/core/TEST_MAPPING +++ b/core/TEST_MAPPING @@ -23,7 +23,7 @@ ], "postsubmit": [ { - "name": "ContactKeysManagerTest", + "name": "CtsContactKeysManagerTestCases", "options": [ { "include-filter": "android.provider.cts.contactkeys." diff --git a/core/api/current.txt b/core/api/current.txt index cb8db9ea69a0..4efb3d324d2d 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -147,6 +147,7 @@ package android { field public static final String MANAGE_DEVICE_POLICY_CAMERA = "android.permission.MANAGE_DEVICE_POLICY_CAMERA"; field public static final String MANAGE_DEVICE_POLICY_CERTIFICATES = "android.permission.MANAGE_DEVICE_POLICY_CERTIFICATES"; field public static final String MANAGE_DEVICE_POLICY_COMMON_CRITERIA_MODE = "android.permission.MANAGE_DEVICE_POLICY_COMMON_CRITERIA_MODE"; + field @FlaggedApi("android.view.contentprotection.flags.manage_device_policy_enabled") public static final String MANAGE_DEVICE_POLICY_CONTENT_PROTECTION = "android.permission.MANAGE_DEVICE_POLICY_CONTENT_PROTECTION"; field public static final String MANAGE_DEVICE_POLICY_DEBUGGING_FEATURES = "android.permission.MANAGE_DEVICE_POLICY_DEBUGGING_FEATURES"; field public static final String MANAGE_DEVICE_POLICY_DEFAULT_SMS = "android.permission.MANAGE_DEVICE_POLICY_DEFAULT_SMS"; field public static final String MANAGE_DEVICE_POLICY_DEVICE_IDENTIFIERS = "android.permission.MANAGE_DEVICE_POLICY_DEVICE_IDENTIFIERS"; @@ -8194,6 +8195,9 @@ package android.app.admin { field public static final String ACTION_SET_NEW_PASSWORD = "android.app.action.SET_NEW_PASSWORD"; field public static final String ACTION_START_ENCRYPTION = "android.app.action.START_ENCRYPTION"; field public static final String ACTION_SYSTEM_UPDATE_POLICY_CHANGED = "android.app.action.SYSTEM_UPDATE_POLICY_CHANGED"; + field @FlaggedApi("android.view.contentprotection.flags.manage_device_policy_enabled") public static final int CONTENT_PROTECTION_DISABLED = 1; // 0x1 + field @FlaggedApi("android.view.contentprotection.flags.manage_device_policy_enabled") public static final int CONTENT_PROTECTION_ENABLED = 2; // 0x2 + field @FlaggedApi("android.view.contentprotection.flags.manage_device_policy_enabled") public static final int CONTENT_PROTECTION_NOT_CONTROLLED_BY_POLICY = 0; // 0x0 field public static final String DELEGATION_APP_RESTRICTIONS = "delegation-app-restrictions"; field public static final String DELEGATION_BLOCK_UNINSTALL = "delegation-block-uninstall"; field public static final String DELEGATION_CERT_INSTALL = "delegation-cert-install"; @@ -12387,6 +12391,7 @@ package android.content.pm { method public void registerCallback(android.content.pm.LauncherApps.Callback, android.os.Handler); method public void registerPackageInstallerSessionCallback(@NonNull java.util.concurrent.Executor, @NonNull android.content.pm.PackageInstaller.SessionCallback); method public android.content.pm.LauncherActivityInfo resolveActivity(android.content.Intent, android.os.UserHandle); + method @FlaggedApi("android.content.pm.archiving") public void setArchiveCompatibilityOptions(boolean, boolean); method public boolean shouldHideFromSuggestions(@NonNull String, @NonNull android.os.UserHandle); method public void startAppDetailsActivity(android.content.ComponentName, android.os.UserHandle, android.graphics.Rect, android.os.Bundle); method public void startMainActivity(android.content.ComponentName, android.os.UserHandle, android.graphics.Rect, android.os.Bundle); @@ -25785,6 +25790,7 @@ package android.media.metrics { field public static final int FINAL_STATE_CANCELED = 2; // 0x2 field public static final int FINAL_STATE_ERROR = 3; // 0x3 field public static final int FINAL_STATE_SUCCEEDED = 1; // 0x1 + field public static final int TIME_SINCE_CREATED_UNKNOWN = -1; // 0xffffffff } @FlaggedApi("com.android.media.editing.flags.add_media_metrics_editing") public static final class EditingEndedEvent.Builder { @@ -25792,7 +25798,7 @@ package android.media.metrics { method @NonNull public android.media.metrics.EditingEndedEvent build(); method @NonNull public android.media.metrics.EditingEndedEvent.Builder setErrorCode(int); method @NonNull public android.media.metrics.EditingEndedEvent.Builder setMetricsBundle(@NonNull android.os.Bundle); - method @NonNull public android.media.metrics.EditingEndedEvent.Builder setTimeSinceCreatedMillis(@IntRange(from=0xffffffff) long); + method @NonNull public android.media.metrics.EditingEndedEvent.Builder setTimeSinceCreatedMillis(@IntRange(from=android.media.metrics.EditingEndedEvent.TIME_SINCE_CREATED_UNKNOWN) long); } public final class EditingSession implements java.lang.AutoCloseable { @@ -33140,7 +33146,7 @@ package android.os { method public static long getStartRequestedElapsedRealtime(); method public static long getStartRequestedUptimeMillis(); method public static long getStartUptimeMillis(); - method public static final int getThreadPriority(int) throws java.lang.IllegalArgumentException; + method @IntRange(from=0xffffffec, to=android.os.Process.THREAD_PRIORITY_LOWEST) public static final int getThreadPriority(int) throws java.lang.IllegalArgumentException; method public static final int getUidForName(String); method public static final boolean is64Bit(); method public static boolean isApplicationUid(int); @@ -33155,8 +33161,8 @@ package android.os { method public static final int myUid(); method public static android.os.UserHandle myUserHandle(); method public static final void sendSignal(int, int); - method public static final void setThreadPriority(int, int) throws java.lang.IllegalArgumentException, java.lang.SecurityException; - method public static final void setThreadPriority(int) throws java.lang.IllegalArgumentException, java.lang.SecurityException; + method public static final void setThreadPriority(int, @IntRange(from=0xffffffec, to=android.os.Process.THREAD_PRIORITY_LOWEST) int) throws java.lang.IllegalArgumentException, java.lang.SecurityException; + method public static final void setThreadPriority(@IntRange(from=0xffffffec, to=android.os.Process.THREAD_PRIORITY_LOWEST) int) throws java.lang.IllegalArgumentException, java.lang.SecurityException; method @Deprecated public static final boolean supportsProcesses(); field public static final int BLUETOOTH_UID = 1002; // 0x3ea field public static final int FIRST_APPLICATION_UID = 10000; // 0x2710 @@ -50644,6 +50650,7 @@ package android.view { field public static final int KEYCODE_DVR = 173; // 0xad field public static final int KEYCODE_E = 33; // 0x21 field public static final int KEYCODE_EISU = 212; // 0xd4 + field @FlaggedApi("com.android.hardware.input.emoji_and_screenshot_keycodes_available") public static final int KEYCODE_EMOJI_PICKER = 317; // 0x13d field public static final int KEYCODE_ENDCALL = 6; // 0x6 field public static final int KEYCODE_ENTER = 66; // 0x42 field public static final int KEYCODE_ENVELOPE = 65; // 0x41 @@ -50776,6 +50783,7 @@ package android.view { field public static final int KEYCODE_RIGHT_BRACKET = 72; // 0x48 field public static final int KEYCODE_RO = 217; // 0xd9 field public static final int KEYCODE_S = 47; // 0x2f + field @FlaggedApi("com.android.hardware.input.emoji_and_screenshot_keycodes_available") public static final int KEYCODE_SCREENSHOT = 318; // 0x13e field public static final int KEYCODE_SCROLL_LOCK = 116; // 0x74 field public static final int KEYCODE_SEARCH = 84; // 0x54 field public static final int KEYCODE_SEMICOLON = 74; // 0x4a diff --git a/core/api/module-lib-current.txt b/core/api/module-lib-current.txt index 24b923326baa..55ed1f559f51 100644 --- a/core/api/module-lib-current.txt +++ b/core/api/module-lib-current.txt @@ -66,6 +66,11 @@ package android.app { method @RequiresPermission(android.Manifest.permission.STATUS_BAR) public void setExpansionDisabledForSimNetworkLock(boolean); } + public final class SystemServiceRegistry { + method @FlaggedApi("android.webkit.update_service_ipc_wrapper") @Nullable public static Object getSystemServiceWithNoContext(@NonNull String); + method @FlaggedApi("android.webkit.update_service_ipc_wrapper") public static <TServiceClass> void registerForeverStaticService(@NonNull String, @NonNull Class<TServiceClass>, @NonNull android.app.SystemServiceRegistry.StaticServiceProducerWithBinder<TServiceClass>); + } + } package android.app.admin { diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 94cde98ca762..f1011616c3ab 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -561,7 +561,7 @@ package android.app { public class ActivityManager { method @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) public void addOnUidImportanceListener(android.app.ActivityManager.OnUidImportanceListener, int); - method @FlaggedApi("android.app.uid_importance_listener_for_uids") @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) public void addOnUidImportanceListener(@NonNull android.app.ActivityManager.OnUidImportanceListener, int, @Nullable int[]); + method @FlaggedApi("android.app.uid_importance_listener_for_uids") @RequiresPermission(android.Manifest.permission.PACKAGE_USAGE_STATS) public void addOnUidImportanceListener(@NonNull android.app.ActivityManager.OnUidImportanceListener, int, @NonNull int[]); method @RequiresPermission(android.Manifest.permission.FORCE_STOP_PACKAGES) public void forceStopPackage(String); method @FlaggedApi("android.app.get_binding_uid_importance") @RequiresPermission(android.Manifest.permission.GET_BINDING_UID_IMPORTANCE) public int getBindingUidImportance(int); method @RequiresPermission(anyOf={"android.permission.INTERACT_ACROSS_USERS", "android.permission.INTERACT_ACROSS_USERS_FULL"}) public static int getCurrentUser(); @@ -6165,6 +6165,7 @@ package android.hardware.radio { method public boolean containsKey(String); method public int describeContents(); method @Deprecated public android.graphics.Bitmap getBitmap(String); + method @FlaggedApi("android.hardware.radio.hd_radio_improved") public int getBitmapId(@NonNull String); method public android.hardware.radio.RadioMetadata.Clock getClock(String); method public int getInt(String); method public String getString(String); @@ -6228,6 +6229,7 @@ package android.hardware.radio { method @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract void close(); method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract int getConfiguration(android.hardware.radio.RadioManager.BandConfig[]); method @Nullable @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public android.hardware.radio.ProgramList getDynamicProgramList(@Nullable android.hardware.radio.ProgramList.Filter); + method @FlaggedApi("android.hardware.radio.hd_radio_improved") @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public android.graphics.Bitmap getMetadataImage(int); method @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract boolean getMute(); method @NonNull @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public java.util.Map<java.lang.String,java.lang.String> getParameters(@NonNull java.util.List<java.lang.String>); method @Deprecated @RequiresPermission(android.Manifest.permission.ACCESS_BROADCAST_RADIO) public abstract int getProgramInformation(android.hardware.radio.RadioManager.ProgramInfo[]); @@ -14846,11 +14848,11 @@ package android.telephony { method @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setDataEnabled(int, boolean); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setDataRoamingEnabled(boolean); method @FlaggedApi("com.android.internal.telephony.flags.enable_identifier_disclosure_transparency") @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setEnableCellularIdentifierDisclosureNotifications(boolean); - method @FlaggedApi("com.android.internal.telephony.flags.enable_modem_cipher_transparency") @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setEnableNullCipherNotifications(boolean); method @NonNull @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public android.telephony.PinResult setIccLockEnabled(boolean, @NonNull String); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setMobileDataPolicyEnabled(int, boolean); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setMultiSimCarrierRestriction(boolean); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public int setNrDualConnectivityState(int); + method @FlaggedApi("com.android.internal.telephony.flags.enable_modem_cipher_transparency") @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public void setNullCipherNotificationsEnabled(boolean); method @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean setOpportunisticNetworkState(boolean); method @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean setPreferredNetworkTypeBitmask(long); method @Deprecated @RequiresPermission(android.Manifest.permission.MODIFY_PHONE_STATE) public boolean setRadio(boolean); diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 42cf08ff9a66..77add41f6805 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -195,8 +195,11 @@ package android.app { method public void setTaskOverlay(boolean, boolean); } - public static final class ActivityOptions.LaunchCookie { + public static final class ActivityOptions.LaunchCookie implements android.os.Parcelable { ctor public ActivityOptions.LaunchCookie(); + method public int describeContents(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.app.ActivityOptions.LaunchCookie> CREATOR; } public static interface ActivityOptions.OnAnimationFinishedListener { @@ -2108,6 +2111,14 @@ package android.media.metrics { } +package android.media.projection { + + public final class MediaProjectionManager { + method @NonNull public android.content.Intent createScreenCaptureIntent(@Nullable android.app.ActivityOptions.LaunchCookie); + } + +} + package android.media.soundtrigger { public final class SoundTriggerInstrumentation { @@ -2643,7 +2654,6 @@ package android.os.storage { method @NonNull public static String convert(@NonNull java.util.UUID); method @Nullable public String getCloudMediaProvider(); method public boolean isAppIoBlocked(@NonNull java.util.UUID, int, int, int); - method public static boolean isUserKeyUnlocked(int); field public static final String CACHE_RESERVE_PERCENT_HIGH_KEY = "cache_reserve_percent_high"; field public static final String CACHE_RESERVE_PERCENT_LOW_KEY = "cache_reserve_percent_low"; field public static final String STORAGE_THRESHOLD_PERCENT_HIGH_KEY = "storage_threshold_percent_high"; @@ -3606,7 +3616,7 @@ package android.view { method public final int getDisplayId(); method public final void setDisplayId(int); field public static final int FLAG_IS_ACCESSIBILITY_EVENT = 2048; // 0x800 - field public static final int LAST_KEYCODE = 316; // 0x13c + field public static final int LAST_KEYCODE = 318; // 0x13e } public final class KeyboardShortcutGroup implements android.os.Parcelable { diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java index 9d20f3c47bb5..6285eb3b2096 100644 --- a/core/java/android/app/ActivityManager.java +++ b/core/java/android/app/ActivityManager.java @@ -107,6 +107,7 @@ import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.Objects; import java.util.concurrent.Executor; import java.util.function.Consumer; @@ -4440,8 +4441,7 @@ public class ActivityManager { * is used here, you will receive a call each time a uids importance transitions between * being <= {@link RunningAppProcessInfo#IMPORTANCE_PERCEPTIBLE} and * > {@link RunningAppProcessInfo#IMPORTANCE_PERCEPTIBLE}. - * @param uids The UIDs that this listener is interested with. A {@code null} value means - * all UIDs will be monitored by this listener, this will be equivalent to the + * @param uids The UIDs that this listener is interested with. * {@link #addOnUidImportanceListener(OnUidImportanceListener, int)} in this case. * * <p>Calling this API with the same instance of {@code listener} without @@ -4456,7 +4456,9 @@ public class ActivityManager { @SuppressLint("SamShouldBeLast") @RequiresPermission(Manifest.permission.PACKAGE_USAGE_STATS) public void addOnUidImportanceListener(@NonNull OnUidImportanceListener listener, - @RunningAppProcessInfo.Importance int importanceCutpoint, @Nullable int[] uids) { + @RunningAppProcessInfo.Importance int importanceCutpoint, @NonNull int[] uids) { + Objects.requireNonNull(listener); + Objects.requireNonNull(uids); addOnUidImportanceListenerInternal(listener, importanceCutpoint, uids); } diff --git a/core/java/android/app/ActivityOptions.aidl b/core/java/android/app/ActivityOptions.aidl index bd5cd88959a3..2d4a85f4f6f4 100644 --- a/core/java/android/app/ActivityOptions.aidl +++ b/core/java/android/app/ActivityOptions.aidl @@ -17,4 +17,7 @@ package android.app; /** @hide */ -parcelable ActivityOptions.SceneTransitionInfo;
\ No newline at end of file +parcelable ActivityOptions.SceneTransitionInfo; + +/** @hide */ +parcelable ActivityOptions.LaunchCookie;
\ No newline at end of file diff --git a/core/java/android/app/ActivityOptions.java b/core/java/android/app/ActivityOptions.java index 4a566db3afb3..111895e3053b 100644 --- a/core/java/android/app/ActivityOptions.java +++ b/core/java/android/app/ActivityOptions.java @@ -1958,14 +1958,87 @@ public class ActivityOptions extends ComponentOptions { */ @SuppressLint("UnflaggedApi") @TestApi - public static final class LaunchCookie { + public static final class LaunchCookie implements Parcelable { /** @hide */ - public final IBinder binder = new Binder(); + public final IBinder binder; /** @hide */ @SuppressLint("UnflaggedApi") @TestApi - public LaunchCookie() {} + public LaunchCookie() { + binder = new Binder(); + } + + /** @hide */ + public LaunchCookie(@Nullable String descriptor) { + binder = new Binder(descriptor); + } + + private LaunchCookie(Parcel in) { + this.binder = in.readStrongBinder(); + } + + /** @hide */ + @SuppressLint("UnflaggedApi") + @TestApi + @Override + public int describeContents() { + return 0; + } + + /** @hide */ + @SuppressLint("UnflaggedApi") + @TestApi + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeStrongBinder(binder); + } + + /** @hide */ + public static LaunchCookie readFromParcel(@NonNull Parcel in) { + return new LaunchCookie(in); + } + + /** @hide */ + public static void writeToParcel(@Nullable LaunchCookie launchCookie, Parcel out) { + if (launchCookie != null) { + launchCookie.writeToParcel(out, 0); + } else { + out.writeStrongBinder(null); + } + } + + /** @hide */ + @SuppressLint("UnflaggedApi") + @TestApi + @NonNull + public static final Parcelable.Creator<LaunchCookie> CREATOR = + new Parcelable.Creator<LaunchCookie>() { + + @Override + public LaunchCookie createFromParcel(Parcel source) { + return new LaunchCookie(source); + } + + @Override + public LaunchCookie[] newArray(int size) { + return new LaunchCookie[size]; + } + }; + + @Override + public boolean equals(@Nullable Object obj) { + if (obj instanceof LaunchCookie) { + LaunchCookie other = (LaunchCookie) obj; + return binder == other.binder; + } + return false; + } + + @Override + public int hashCode() { + return binder.hashCode(); + } } /** diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java index 00c4b0f6515f..bb666e65d6fb 100644 --- a/core/java/android/app/AppOpsManager.java +++ b/core/java/android/app/AppOpsManager.java @@ -1555,9 +1555,24 @@ public class AppOpsManager { */ public static final int OP_RUN_BACKUP_JOBS = AppProtoEnums.APP_OP_RUN_BACKUP_JOBS; + /** + * Whether the app has enabled to receive the icon overlay for fetching archived apps. + * + * @hide + */ + public static final int OP_ARCHIVE_ICON_OVERLAY = AppProtoEnums.APP_OP_ARCHIVE_ICON_OVERLAY; + + /** + * Whether the app has enabled compatibility support for unarchival. + * + * @hide + */ + public static final int OP_UNARCHIVAL_CONFIRMATION = + AppProtoEnums.APP_OP_UNARCHIVAL_CONFIRMATION; + /** @hide */ @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - public static final int _NUM_OP = 145; + public static final int _NUM_OP = 147; /** * All app ops represented as strings. @@ -1708,6 +1723,8 @@ public class AppOpsManager { OPSTR_RESERVED_FOR_TESTING, OPSTR_RAPID_CLEAR_NOTIFICATIONS_BY_LISTENER, OPSTR_RUN_BACKUP_JOBS, + OPSTR_ARCHIVE_ICON_OVERLAY, + OPSTR_UNARCHIVAL_CONFIRMATION, }) public @interface AppOpString {} @@ -2048,6 +2065,20 @@ public class AppOpsManager { public static final String OPSTR_MEDIA_ROUTING_CONTROL = "android:media_routing_control"; /** + * Whether the app has enabled to receive the icon overlay for fetching archived apps. + * + * @hide + */ + public static final String OPSTR_ARCHIVE_ICON_OVERLAY = "android:archive_icon_overlay"; + + /** + * Whether the app has enabled compatibility support for unarchival. + * + * @hide + */ + public static final String OPSTR_UNARCHIVAL_CONFIRMATION = "android:unarchival_support"; + + /** * AppOp granted to apps that we are started via {@code am instrument -e --no-isolated-storage} * * <p>MediaProvider is the only component (outside of system server) that should care about this @@ -2520,6 +2551,8 @@ public class AppOpsManager { OP_MEDIA_ROUTING_CONTROL, OP_READ_SYSTEM_GRAMMATICAL_GENDER, OP_RUN_BACKUP_JOBS, + OP_ARCHIVE_ICON_OVERLAY, + OP_UNARCHIVAL_CONFIRMATION, }; static final AppOpInfo[] sAppOpInfos = new AppOpInfo[]{ @@ -2979,6 +3012,12 @@ public class AppOpsManager { .build(), new AppOpInfo.Builder(OP_RUN_BACKUP_JOBS, OPSTR_RUN_BACKUP_JOBS, "RUN_BACKUP_JOBS") .setPermission(Manifest.permission.RUN_BACKUP_JOBS).build(), + new AppOpInfo.Builder(OP_ARCHIVE_ICON_OVERLAY, OPSTR_ARCHIVE_ICON_OVERLAY, + "ARCHIVE_ICON_OVERLAY") + .setDefaultMode(MODE_ALLOWED).build(), + new AppOpInfo.Builder(OP_UNARCHIVAL_CONFIRMATION, OPSTR_UNARCHIVAL_CONFIRMATION, + "UNARCHIVAL_CONFIRMATION") + .setDefaultMode(MODE_ALLOWED).build(), }; // The number of longs needed to form a full bitmask of app ops @@ -3113,7 +3152,7 @@ public class AppOpsManager { /** * Retrieve the permission associated with an operation, or null if there is not one. - * + * @param op The operation name. * * @hide diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java index 34c44f9489d5..4f1db7d3784a 100644 --- a/core/java/android/app/ApplicationPackageManager.java +++ b/core/java/android/app/ApplicationPackageManager.java @@ -4032,7 +4032,8 @@ public class ApplicationPackageManager extends PackageManager { private Drawable getArchivedAppIcon(String packageName) { try { return new BitmapDrawable(null, - mPM.getArchivedAppIcon(packageName, new UserHandle(getUserId()))); + mPM.getArchivedAppIcon(packageName, new UserHandle(getUserId()), + mContext.getPackageName())); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java index 2162e3a77f15..68512b8bd771 100644 --- a/core/java/android/app/Instrumentation.java +++ b/core/java/android/app/Instrumentation.java @@ -79,6 +79,7 @@ import java.util.concurrent.TimeoutException; * implementation is described to the system through an AndroidManifest.xml's * <instrumentation> tag. */ +@android.ravenwood.annotation.RavenwoodKeepPartialClass public class Instrumentation { /** @@ -132,6 +133,7 @@ public class Instrumentation { private UiAutomation mUiAutomation; private final Object mAnimationCompleteLock = new Object(); + @android.ravenwood.annotation.RavenwoodKeep public Instrumentation() { } @@ -142,6 +144,7 @@ public class Instrumentation { * reflection, but it will serve as noticeable discouragement from * doing such a thing. */ + @android.ravenwood.annotation.RavenwoodReplace private void checkInstrumenting(String method) { // Check if we have an instrumentation context, as init should only get called by // the system in startup processes that are being instrumented. @@ -151,6 +154,11 @@ public class Instrumentation { } } + private void checkInstrumenting$ravenwood(String method) { + // At the moment, Ravenwood doesn't attach a Context, but we're only ever + // running code as part of tests, so we continue quietly + } + /** * Returns if it is being called in an instrumentation environment. * @@ -2504,6 +2512,7 @@ public class Instrumentation { * Takes control of the execution of messages on the specified looper until * {@link TestLooperManager#release} is called. */ + @android.ravenwood.annotation.RavenwoodKeep public TestLooperManager acquireLooperManager(Looper looper) { checkInstrumenting("acquireLooperManager"); return new TestLooperManager(looper); diff --git a/core/java/android/app/OWNERS b/core/java/android/app/OWNERS index 3b5bba20a10b..729f92a73e5b 100644 --- a/core/java/android/app/OWNERS +++ b/core/java/android/app/OWNERS @@ -111,5 +111,9 @@ per-file Window* = file:/services/core/java/com/android/server/wm/OWNERS per-file ConfigurationController.java = file:/services/core/java/com/android/server/wm/OWNERS per-file *ScreenCapture* = file:/services/core/java/com/android/server/wm/OWNERS +# Multitasking +per-file multitasking.aconfig = file:/services/core/java/com/android/server/wm/OWNERS +per-file multitasking.aconfig = file:/libs/WindowManager/Shell/OWNERS + # Zygote per-file *Zygote* = file:/ZYGOTE_OWNERS diff --git a/core/java/android/app/PendingIntent.java b/core/java/android/app/PendingIntent.java index 0261f0a02174..1ac08ac4cd24 100644 --- a/core/java/android/app/PendingIntent.java +++ b/core/java/android/app/PendingIntent.java @@ -179,6 +179,14 @@ public final class PendingIntent implements Parcelable { @Overridable public static final long BLOCK_MUTABLE_IMPLICIT_PENDING_INTENT = 236704164L; + /** + * Validate options passed in as bundle. + * @hide + */ + @ChangeId + @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + public static final long PENDING_INTENT_OPTIONS_CHECK = 320664730L; + /** @hide */ @IntDef(flag = true, value = { diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java index 397b63fe6a30..b21b0f3bcca8 100644 --- a/core/java/android/app/SystemServiceRegistry.java +++ b/core/java/android/app/SystemServiceRegistry.java @@ -19,6 +19,7 @@ package android.app; import android.accounts.AccountManager; import android.accounts.IAccountManager; import android.adservices.AdServicesFrameworkInitializer; +import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; @@ -1668,11 +1669,7 @@ public final class SystemServiceRegistry { return new Object[sServiceCacheSize]; } - /** - * Gets a system service from a given context. - * @hide - */ - public static Object getSystemService(ContextImpl ctx, String name) { + private static ServiceFetcher<?> getSystemServiceFetcher(String name) { if (name == null) { return null; } @@ -1683,6 +1680,18 @@ public final class SystemServiceRegistry { } return null; } + return fetcher; + } + + /** + * Gets a system service from a given context. + * @hide + */ + public static Object getSystemService(@NonNull ContextImpl ctx, String name) { + final ServiceFetcher<?> fetcher = getSystemServiceFetcher(name); + if (fetcher == null) { + return null; + } final Object ret = fetcher.getService(ctx); if (sEnableServiceNotFoundWtf && ret == null) { @@ -1710,6 +1719,26 @@ public final class SystemServiceRegistry { } /** + * Gets a system service which has opted-in to being fetched without a context. + * @hide + */ + @FlaggedApi(android.webkit.Flags.FLAG_UPDATE_SERVICE_IPC_WRAPPER) + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + public static @Nullable Object getSystemServiceWithNoContext(@NonNull String name) { + final ServiceFetcher<?> fetcher = getSystemServiceFetcher(name); + if (fetcher == null) { + return null; + } + + if (!fetcher.supportsFetchWithoutContext()) { + throw new IllegalArgumentException( + "Manager cannot be fetched without a context: " + name); + } + + return fetcher.getService(null); + } + + /** * Gets the name of the system-level service that is represented by the specified class. * @hide */ @@ -1863,6 +1892,50 @@ public final class SystemServiceRegistry { } /** + * Used by apex modules to register a "service wrapper" that is not tied to any {@link Context} + * and will never require a context in the future. + * + * Services registered in this way can be fetched via + * {@link #getSystemServiceWithNoContext(String)}, so cannot require a context in future without + * a breaking change. + * + * <p>This can only be called from the methods called by the static initializer of + * {@link SystemServiceRegistry}. (Otherwise it throws a {@link IllegalStateException}.) + * + * @param serviceName the name of the binder object, such as + * {@link Context#JOB_SCHEDULER_SERVICE}. + * @param serviceWrapperClass the wrapper class, such as the class of + * {@link android.app.job.JobScheduler}. + * @param serviceProducer Callback that takes the service binder object with the name + * {@code serviceName} and returns an actual service wrapper instance. + * + * @hide + */ + @FlaggedApi(android.webkit.Flags.FLAG_UPDATE_SERVICE_IPC_WRAPPER) + @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES) + public static <TServiceClass> void registerForeverStaticService( + @NonNull String serviceName, @NonNull Class<TServiceClass> serviceWrapperClass, + @NonNull StaticServiceProducerWithBinder<TServiceClass> serviceProducer) { + ensureInitializing("registerStaticService"); + Preconditions.checkStringNotEmpty(serviceName); + Objects.requireNonNull(serviceWrapperClass); + Objects.requireNonNull(serviceProducer); + + registerService(serviceName, serviceWrapperClass, + new StaticServiceFetcher<TServiceClass>() { + @Override + public TServiceClass createService() throws ServiceNotFoundException { + return serviceProducer.createService( + ServiceManager.getServiceOrThrow(serviceName)); + } + + @Override + public boolean supportsFetchWithoutContext() { + return true; + }}); + } + + /** * Similar to {@link #registerStaticService(String, Class, StaticServiceProducerWithBinder)}, * but used for a "service wrapper" that doesn't take a service binder in its constructor. * @@ -1952,6 +2025,18 @@ public final class SystemServiceRegistry { */ static abstract interface ServiceFetcher<T> { T getService(ContextImpl ctx); + + /** + * Should this service fetcher support being fetched via {@link #getSystemService(String)}, + * without a Context? + * + * This means that the service cannot depend on a Context in future! + * + * @return true if this is supported for this service. + */ + default boolean supportsFetchWithoutContext() { + return false; + } } /** @@ -2059,6 +2144,11 @@ public final class SystemServiceRegistry { } public abstract T createService(ContextImpl ctx) throws ServiceNotFoundException; + + // Services that explicitly use a Context can never be fetched without one. + public final boolean supportsFetchWithoutContext() { + return false; + } } /** @@ -2083,6 +2173,13 @@ public final class SystemServiceRegistry { } public abstract T createService() throws ServiceNotFoundException; + + // Services that do not need a Context can potentially be fetched without one, but the + // default is false, so that the service can require one in future without this being a + // breaking change. + public boolean supportsFetchWithoutContext() { + return false; + } } /** @hide */ diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java index 5c42b0ed975a..86d0125fd7a2 100644 --- a/core/java/android/app/admin/DevicePolicyManager.java +++ b/core/java/android/app/admin/DevicePolicyManager.java @@ -53,6 +53,7 @@ import static android.app.admin.flags.Flags.onboardingBugreportV2Enabled; import static android.content.Intent.LOCAL_FLAG_FROM_SYSTEM; import static android.net.NetworkCapabilities.NET_ENTERPRISE_ID_1; import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; +import static android.view.contentprotection.flags.Flags.FLAG_MANAGE_DEVICE_POLICY_ENABLED; import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; @@ -61,6 +62,7 @@ import android.accounts.Account; import android.annotation.BroadcastBehavior; import android.annotation.CallbackExecutor; import android.annotation.ColorInt; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -4092,6 +4094,29 @@ public class DevicePolicyManager { return MTE_NOT_CONTROLLED_BY_POLICY; } + /** Indicates that content protection is not controlled by policy, allowing user to choose. */ + @FlaggedApi(FLAG_MANAGE_DEVICE_POLICY_ENABLED) + public static final int CONTENT_PROTECTION_NOT_CONTROLLED_BY_POLICY = 0; + + /** Indicates that content protection is controlled and disabled by a policy. */ + @FlaggedApi(FLAG_MANAGE_DEVICE_POLICY_ENABLED) + public static final int CONTENT_PROTECTION_DISABLED = 1; + + /** Indicates that content protection is controlled and enabled by a policy. */ + @FlaggedApi(FLAG_MANAGE_DEVICE_POLICY_ENABLED) + public static final int CONTENT_PROTECTION_ENABLED = 2; + + /** @hide */ + @IntDef( + prefix = {"CONTENT_PROTECTION_"}, + value = { + CONTENT_PROTECTION_NOT_CONTROLLED_BY_POLICY, + CONTENT_PROTECTION_DISABLED, + CONTENT_PROTECTION_ENABLED, + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ContentProtectionPolicy {} + /** * This object is a single place to tack on invalidation and disable calls. All * binder caches in this class derive from this Config, so all can be invalidated or diff --git a/core/java/android/app/background_install_control_manager.aconfig b/core/java/android/app/background_install_control_manager.aconfig index 029b93ab4534..4473b9523f1b 100644 --- a/core/java/android/app/background_install_control_manager.aconfig +++ b/core/java/android/app/background_install_control_manager.aconfig @@ -1,7 +1,7 @@ package: "android.app" flag { - namespace: "background_install_control" + namespace: "preload_safety" name: "bic_client" description: "System API for background install control." is_fixed_read_only: true diff --git a/core/java/android/app/backup/BackupAgent.java b/core/java/android/app/backup/BackupAgent.java index 6b558d07c059..ffbd80ce5824 100644 --- a/core/java/android/app/backup/BackupAgent.java +++ b/core/java/android/app/backup/BackupAgent.java @@ -43,12 +43,14 @@ import android.util.Log; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.infra.AndroidFuture; +import com.android.server.backup.Flags; import libcore.io.IoUtils; import org.xmlpull.v1.XmlPullParserException; import java.io.File; +import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.lang.annotation.Retention; @@ -1283,6 +1285,14 @@ public abstract class BackupAgent extends ContextWrapper { // And bring live SharedPreferences instances up to date reloadSharedPreferences(); + // It's possible that onRestoreFile was overridden and that the agent did not + // consume all the data for this file from the pipe. We need to clear the pipe, + // otherwise the framework can get stuck trying to write to a full pipe or + // onRestoreFile could be called with the previous file's data left in the pipe. + if (Flags.enableClearPipeAfterRestoreFile()) { + clearUnconsumedDataFromPipe(data, size); + } + Binder.restoreCallingIdentity(ident); try { callbackBinder.opCompleteForUser(getBackupUserId(), token, 0); @@ -1296,6 +1306,16 @@ public abstract class BackupAgent extends ContextWrapper { } } + private static void clearUnconsumedDataFromPipe(ParcelFileDescriptor data, long size) { + try (FileInputStream in = new FileInputStream(data.getFileDescriptor())) { + if (in.available() > 0) { + in.skip(size); + } + } catch (IOException e) { + Log.w(TAG, "Failed to clear unconsumed data from pipe.", e); + } + } + @Override public void doRestoreFinished(int token, IBackupManager callbackBinder) { final long ident = Binder.clearCallingIdentity(); diff --git a/core/java/android/app/multitasking.aconfig b/core/java/android/app/multitasking.aconfig new file mode 100644 index 000000000000..ab00891b9b31 --- /dev/null +++ b/core/java/android/app/multitasking.aconfig @@ -0,0 +1,8 @@ +package: "android.app" + +flag { + name: "enable_pip_ui_state_callback_on_entering" + namespace: "multitasking" + description: "Enables PiP UI state callback on entering" + bug: "303718131" +} diff --git a/core/java/android/appwidget/AppWidgetHostView.java b/core/java/android/appwidget/AppWidgetHostView.java index ec181dac6b36..1f19f817a0b3 100644 --- a/core/java/android/appwidget/AppWidgetHostView.java +++ b/core/java/android/appwidget/AppWidgetHostView.java @@ -907,7 +907,10 @@ public class AppWidgetHostView extends FrameLayout implements AppWidgetHost.AppW private InteractionHandler getHandler(InteractionHandler handler) { return (view, pendingIntent, response) -> { - AppWidgetManager.getInstance(mContext).noteAppWidgetTapped(mAppWidgetId); + AppWidgetManager manager = AppWidgetManager.getInstance(mContext); + if (manager != null) { + manager.noteAppWidgetTapped(mAppWidgetId); + } if (handler != null) { return handler.onInteraction(view, pendingIntent, response); } else { diff --git a/core/java/android/content/Context.java b/core/java/android/content/Context.java index b1173a25e95f..b8d754348211 100644 --- a/core/java/android/content/Context.java +++ b/core/java/android/content/Context.java @@ -3657,8 +3657,8 @@ public abstract class Context { * On Android {@link android.os.Build.VERSION_CODES#S} and later, * if the application is in a state where the service * can not be started (such as not in the foreground in a state when services are allowed), - * {@link android.app.BackgroundServiceStartNotAllowedException} is thrown - * This excemption extends {@link IllegalStateException}, so apps can + * {@link android.app.BackgroundServiceStartNotAllowedException} is thrown. + * This exception extends {@link IllegalStateException}, so apps can * use {@code catch (IllegalStateException)} to catch both. * * @see #startForegroundService(Intent) diff --git a/core/java/android/content/pm/ILauncherApps.aidl b/core/java/android/content/pm/ILauncherApps.aidl index a97de6368b8c..62db65f15df3 100644 --- a/core/java/android/content/pm/ILauncherApps.aidl +++ b/core/java/android/content/pm/ILauncherApps.aidl @@ -128,4 +128,6 @@ interface ILauncherApps { /** Unregister a callback, so that it won't be called when LauncherApps dumps. */ void unRegisterDumpCallback(IDumpCallback cb); + + void setArchiveCompatibilityOptions(boolean enableIconOverlay, boolean enableUnarchivalConfirmation); } diff --git a/core/java/android/content/pm/IPackageManager.aidl b/core/java/android/content/pm/IPackageManager.aidl index 6dc8d4738c87..380de965b143 100644 --- a/core/java/android/content/pm/IPackageManager.aidl +++ b/core/java/android/content/pm/IPackageManager.aidl @@ -840,7 +840,7 @@ interface IPackageManager { ArchivedPackageParcel getArchivedPackage(in String packageName, int userId); - Bitmap getArchivedAppIcon(String packageName, in UserHandle user); + Bitmap getArchivedAppIcon(String packageName, in UserHandle user, String callingPackageName); boolean isAppArchivable(String packageName, in UserHandle user); } diff --git a/core/java/android/content/pm/LauncherApps.java b/core/java/android/content/pm/LauncherApps.java index 1d2b1aff46bc..50be983ec938 100644 --- a/core/java/android/content/pm/LauncherApps.java +++ b/core/java/android/content/pm/LauncherApps.java @@ -1801,6 +1801,31 @@ public class LauncherApps { } } + /** + * Enable or disable different archive compatibility options of the launcher. + * + * @param enableIconOverlay Provides a cloud overlay for archived apps to ensure users are aware + * that a certain app is archived. True by default. + * Launchers might want to disable this operation if they want to provide custom user experience + * to differentiate archived apps. + * @param enableUnarchivalConfirmation If true, the user is shown a confirmation dialog when + * they click an archived app, which explains that the app will be downloaded and restored in + * the background. True by default. + * Launchers might want to disable this operation if they provide sufficient, alternative user + * guidance to highlight that an unarchival is starting and ongoing once an archived app is + * tapped. E.g., this could be achieved by showing the unarchival progress around the icon. + */ + @FlaggedApi(android.content.pm.Flags.FLAG_ARCHIVING) + public void setArchiveCompatibilityOptions(boolean enableIconOverlay, + boolean enableUnarchivalConfirmation) { + try { + mService.setArchiveCompatibilityOptions(enableIconOverlay, + enableUnarchivalConfirmation); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } + /** @return position in mCallbacks for callback or -1 if not present. */ private int findCallbackLocked(Callback callback) { if (callback == null) { diff --git a/core/java/android/content/res/Element.java b/core/java/android/content/res/Element.java index 89f4985461b7..6ff96f42e433 100644 --- a/core/java/android/content/res/Element.java +++ b/core/java/android/content/res/Element.java @@ -93,6 +93,7 @@ public class Element { protected static final String TAG_SUPPORTS_GL_TEXTURE = "supports-gl-texture"; protected static final String TAG_SUPPORTS_INPUT = "supports-input"; protected static final String TAG_SUPPORTS_SCREENS = "supports-screens"; + protected static final String TAG_URI_RELATIVE_FILTER_GROUP = "uri-relative-filter-group"; protected static final String TAG_USES_CONFIGURATION = "uses-configuration"; protected static final String TAG_USES_FEATURE = "uses-feature"; protected static final String TAG_USES_GL_TEXTURE = "uses-gl-texture"; @@ -106,6 +107,11 @@ public class Element { protected static final String TAG_ATTR_BACKUP_AGENT = "backupAgent"; protected static final String TAG_ATTR_CATEGORY = "category"; + protected static final String TAG_ATTR_FRAGMENT = "fragment"; + protected static final String TAG_ATTR_FRAGMENT_ADVANCED_PATTERN = "fragmentAdvancedPattern"; + protected static final String TAG_ATTR_FRAGMENT_PATTERN = "fragmentPattern"; + protected static final String TAG_ATTR_FRAGMENT_PREFIX = "fragmentPrefix"; + protected static final String TAG_ATTR_FRAGMENT_SUFFIX = "fragmentSuffix"; protected static final String TAG_ATTR_HOST = "host"; protected static final String TAG_ATTR_MANAGE_SPACE_ACTIVITY = "manageSpaceActivity"; protected static final String TAG_ATTR_MIMETYPE = "mimeType"; @@ -122,6 +128,11 @@ public class Element { protected static final String TAG_ATTR_PERMISSION_GROUP = "permissionGroup"; protected static final String TAG_ATTR_PORT = "port"; protected static final String TAG_ATTR_PROCESS = "process"; + protected static final String TAG_ATTR_QUERY = "query"; + protected static final String TAG_ATTR_QUERY_ADVANCED_PATTERN = "queryAdvancedPattern"; + protected static final String TAG_ATTR_QUERY_PATTERN = "queryPattern"; + protected static final String TAG_ATTR_QUERY_PREFIX = "queryPrefix"; + protected static final String TAG_ATTR_QUERY_SUFFIX = "querySuffix"; protected static final String TAG_ATTR_READ_PERMISSION = "readPermission"; protected static final String TAG_ATTR_REQUIRED_ACCOUNT_TYPE = "requiredAccountType"; protected static final String TAG_ATTR_REQUIRED_SYSTEM_PROPERTY_NAME = @@ -143,7 +154,7 @@ public class Element { // The length of mTagCounters corresponds to the number of tags defined in getCounterIdx. If new // tags are added then the size here should be increased to match. - private final TagCounter[] mTagCounters = new TagCounter[34]; + private final TagCounter[] mTagCounters = new TagCounter[35]; String mTag; @@ -238,9 +249,11 @@ public class Element { return 31; case TAG_INTENT: return 32; + case TAG_URI_RELATIVE_FILTER_GROUP: + return 33; default: // The size of the mTagCounters array should be equal to this value+1 - return 33; + return 34; } } @@ -276,6 +289,7 @@ public class Element { case TAG_SERVICE: case TAG_SUPPORTS_GL_TEXTURE: case TAG_SUPPORTS_SCREENS: + case TAG_URI_RELATIVE_FILTER_GROUP: case TAG_USES_CONFIGURATION: case TAG_USES_FEATURE: case TAG_USES_LIBRARY: @@ -322,6 +336,7 @@ public class Element { break; case TAG_INTENT: case TAG_INTENT_FILTER: + initializeCounter(TAG_URI_RELATIVE_FILTER_GROUP, 100); initializeCounter(TAG_ACTION, 20000); initializeCounter(TAG_CATEGORY, 40000); initializeCounter(TAG_DATA, 40000); @@ -354,6 +369,9 @@ public class Element { initializeCounter(TAG_INTENT, 2000); initializeCounter(TAG_PROVIDER, 8000); break; + case TAG_URI_RELATIVE_FILTER_GROUP: + initializeCounter(TAG_DATA, 100); + break; } } @@ -391,11 +409,21 @@ public class Element { case TAG_ATTR_VERSION_NAME: case TAG_ATTR_ZYGOTE_PRELOAD_NAME: return MAX_ATTR_LEN_NAME; + case TAG_ATTR_FRAGMENT: + case TAG_ATTR_FRAGMENT_ADVANCED_PATTERN: + case TAG_ATTR_FRAGMENT_PATTERN: + case TAG_ATTR_FRAGMENT_PREFIX: + case TAG_ATTR_FRAGMENT_SUFFIX: case TAG_ATTR_PATH: case TAG_ATTR_PATH_ADVANCED_PATTERN: case TAG_ATTR_PATH_PATTERN: case TAG_ATTR_PATH_PREFIX: case TAG_ATTR_PATH_SUFFIX: + case TAG_ATTR_QUERY: + case TAG_ATTR_QUERY_ADVANCED_PATTERN: + case TAG_ATTR_QUERY_PATTERN: + case TAG_ATTR_QUERY_PREFIX: + case TAG_ATTR_QUERY_SUFFIX: return MAX_ATTR_LEN_PATH; case TAG_ATTR_VALUE: return MAX_ATTR_LEN_VALUE; @@ -535,6 +563,16 @@ public class Element { case R.styleable.AndroidManifestData_pathPrefix: case R.styleable.AndroidManifestData_pathSuffix: case R.styleable.AndroidManifestData_pathAdvancedPattern: + case R.styleable.AndroidManifestData_query: + case R.styleable.AndroidManifestData_queryPattern: + case R.styleable.AndroidManifestData_queryPrefix: + case R.styleable.AndroidManifestData_querySuffix: + case R.styleable.AndroidManifestData_queryAdvancedPattern: + case R.styleable.AndroidManifestData_fragment: + case R.styleable.AndroidManifestData_fragmentPattern: + case R.styleable.AndroidManifestData_fragmentPrefix: + case R.styleable.AndroidManifestData_fragmentSuffix: + case R.styleable.AndroidManifestData_fragmentAdvancedPattern: return MAX_ATTR_LEN_PATH; default: return DEFAULT_MAX_STRING_ATTR_LENGTH; diff --git a/core/java/android/hardware/camera2/extension/CameraOutputConfig.aidl b/core/java/android/hardware/camera2/extension/CameraOutputConfig.aidl index 7c54a9b01dde..509bcb8e3d23 100644 --- a/core/java/android/hardware/camera2/extension/CameraOutputConfig.aidl +++ b/core/java/android/hardware/camera2/extension/CameraOutputConfig.aidl @@ -26,6 +26,7 @@ parcelable CameraOutputConfig Surface surface; int imageFormat; int capacity; + long usage; const int TYPE_SURFACE = 0; const int TYPE_IMAGEREADER = 1; diff --git a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java index 98bc31161591..f6c8f36a1b01 100644 --- a/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java +++ b/core/java/android/hardware/camera2/impl/CameraAdvancedExtensionSessionImpl.java @@ -1182,7 +1182,8 @@ public final class CameraAdvancedExtensionSessionImpl extends CameraExtensionSes return null; } ImageReader reader = ImageReader.newInstance(output.size.width, - output.size.height, output.imageFormat, output.capacity); + output.size.height, output.imageFormat, output.capacity, + output.usage); mReaderMap.put(output.outputId.id, reader); return reader.getSurface(); case CameraOutputConfig.TYPE_MULTIRES_IMAGEREADER: diff --git a/core/java/android/hardware/input/InputSettings.java b/core/java/android/hardware/input/InputSettings.java index fdbd3197fb79..89fa5fb47a07 100644 --- a/core/java/android/hardware/input/InputSettings.java +++ b/core/java/android/hardware/input/InputSettings.java @@ -19,6 +19,7 @@ package android.hardware.input; import static com.android.hardware.input.Flags.keyboardA11yBounceKeysFlag; import static com.android.hardware.input.Flags.keyboardA11ySlowKeysFlag; import static com.android.hardware.input.Flags.keyboardA11yStickyKeysFlag; +import static com.android.hardware.input.Flags.touchpadTapDragging; import static com.android.input.flags.Flags.enableInputFilterRustImpl; import android.Manifest; @@ -303,6 +304,53 @@ public class InputSettings { } /** + * Returns true if the feature flag for touchpad tap dragging is enabled. + * + * @hide + */ + public static boolean isTouchpadTapDraggingFeatureFlagEnabled() { + return touchpadTapDragging(); + } + + /** + * Returns true if the touchpad should allow tap dragging. + * + * The returned value only applies to gesture-compatible touchpads. + * + * @param context The application context. + * @return Whether the touchpad should allow tap dragging. + * + * @hide + */ + public static boolean useTouchpadTapDragging(@NonNull Context context) { + if (!isTouchpadTapDraggingFeatureFlagEnabled()) { + return false; + } + return Settings.System.getIntForUser(context.getContentResolver(), + Settings.System.TOUCHPAD_TAP_DRAGGING, 0, UserHandle.USER_CURRENT) == 1; + } + + /** + * Sets the tap dragging behavior for the touchpad. + * + * The new behavior is only applied to gesture-compatible touchpads. + * + * @param context The application context. + * @param enabled Will enable tap dragging if true, disable it if false + * + * @hide + */ + @RequiresPermission(Manifest.permission.WRITE_SETTINGS) + public static void setTouchpadTapDragging(@NonNull Context context, boolean enabled) { + if (!isTouchpadTapDraggingFeatureFlagEnabled()) { + return; + } + Settings.System.putIntForUser(context.getContentResolver(), + Settings.System.TOUCHPAD_TAP_DRAGGING, enabled ? 1 : 0, + UserHandle.USER_CURRENT); + } + + /** * Returns true if the touchpad should use the right click zone. * * The returned value only applies to gesture-compatible touchpads. diff --git a/core/java/android/hardware/input/input_framework.aconfig b/core/java/android/hardware/input/input_framework.aconfig index 0ed6569afd2a..e070fe570907 100644 --- a/core/java/android/hardware/input/input_framework.aconfig +++ b/core/java/android/hardware/input/input_framework.aconfig @@ -33,7 +33,21 @@ flag { flag { namespace: "input_native" + name: "emoji_and_screenshot_keycodes_available" + description: "Add new KeyEvent keycodes for opening Emoji Picker and Taking Screenshots" + bug: "315307777" +} + +flag { + namespace: "input_native" name: "keyboard_a11y_slow_keys_flag" description: "Controls if the slow keys accessibility feature for physical keyboard is available to the user" bug: "294546335" +} + +flag { + namespace: "input_native" + name: "touchpad_tap_dragging" + description: "Offers a setting to enable touchpad tap dragging" + bug: "321978150" }
\ No newline at end of file diff --git a/core/java/android/hardware/radio/RadioMetadata.java b/core/java/android/hardware/radio/RadioMetadata.java index db14c08b3698..da6b9c25e2ba 100644 --- a/core/java/android/hardware/radio/RadioMetadata.java +++ b/core/java/android/hardware/radio/RadioMetadata.java @@ -507,10 +507,16 @@ public final class RadioMetadata implements Parcelable { * * @param key The key the value is stored under. * @return a bitmap identifier or 0 if it's missing. - * @hide This API is not thoroughly elaborated yet + * @throws NullPointerException if metadata key is {@code null} + * @throws IllegalArgumentException if the metadata with the key is not found in + * metadata or the key is not of bitmap-key type */ + @FlaggedApi(Flags.FLAG_HD_RADIO_IMPROVED) public int getBitmapId(@NonNull String key) { - if (!METADATA_KEY_ICON.equals(key) && !METADATA_KEY_ART.equals(key)) return 0; + Objects.requireNonNull(key, "Metadata key can not be null"); + if (!METADATA_KEY_ICON.equals(key) && !METADATA_KEY_ART.equals(key)) { + throw new IllegalArgumentException("Failed to retrieve key " + key + " as bitmap key"); + } return getInt(key); } diff --git a/core/java/android/hardware/radio/RadioTuner.java b/core/java/android/hardware/radio/RadioTuner.java index 9b2bcdea5f30..7c5c00369e93 100644 --- a/core/java/android/hardware/radio/RadioTuner.java +++ b/core/java/android/hardware/radio/RadioTuner.java @@ -17,6 +17,7 @@ package android.hardware.radio; import android.Manifest; +import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -332,29 +333,30 @@ public abstract class RadioTuner { public abstract int getProgramInformation(RadioManager.ProgramInfo[] info); /** - * Retrieves a {@link Bitmap} for the given image ID or null, + * Retrieves a {@link Bitmap} for the given image ID or throw {@link IllegalArgumentException}, * if the image was missing from the tuner. * * <p>This involves doing a call to the tuner, so the bitmap should be cached * on the application side. * - * <p>If the method returns null for non-zero ID, it means the image was - * updated on the tuner side. There is a race conditon between fetching - * image for an old ID and tuner updating the image (and cleaning up the + * <p>If the method throws {@link IllegalArgumentException} for non-zero ID, it + * means the image was updated on the tuner side. There is a race condition between + * fetching image for an old ID and tuner updating the image (and cleaning up the * old image). In such case, a new ProgramInfo with updated image id will * be sent with a {@link Callback#onProgramInfoChanged(RadioManager.ProgramInfo)} * callback. * * @param id The image identifier, retrieved with * {@link RadioMetadata#getBitmapId(String)}. - * @return A {@link Bitmap} or null. - * @throws IllegalArgumentException if id==0 - * @hide This API is not thoroughly elaborated yet + * @return A {@link Bitmap} for the given image ID. + * @throws IllegalArgumentException if id is 0 or the referenced image id no longer exists. */ - @SuppressWarnings("HiddenAbstractMethod") + @FlaggedApi(Flags.FLAG_HD_RADIO_IMPROVED) @RequiresPermission(Manifest.permission.ACCESS_BROADCAST_RADIO) - public abstract @Nullable Bitmap getMetadataImage(int id); - + public @NonNull Bitmap getMetadataImage(int id) { + throw new UnsupportedOperationException( + "Getting metadata image must be implemented in child classes"); + } /** * Initiates a background scan to update internally cached program list. * diff --git a/core/java/android/hardware/radio/TunerAdapter.java b/core/java/android/hardware/radio/TunerAdapter.java index ba31ca3627bf..63b2d4cdd1fb 100644 --- a/core/java/android/hardware/radio/TunerAdapter.java +++ b/core/java/android/hardware/radio/TunerAdapter.java @@ -16,6 +16,7 @@ package android.hardware.radio; +import android.annotation.NonNull; import android.annotation.Nullable; import android.graphics.Bitmap; import android.os.RemoteException; @@ -251,10 +252,18 @@ final class TunerAdapter extends RadioTuner { } @Override - @Nullable + @NonNull public Bitmap getMetadataImage(int id) { + if (id == 0) { + throw new IllegalArgumentException("Invalid metadata image id 0"); + } try { - return mTuner.getImage(id); + Bitmap bitmap = mTuner.getImage(id); + if (bitmap == null) { + throw new IllegalArgumentException("Metadata image with id " + id + + " is not available"); + } + return bitmap; } catch (RemoteException e) { throw new RuntimeException("Service died", e); } diff --git a/core/java/android/os/Binder.java b/core/java/android/os/Binder.java index 05b7827f586f..b7556dfb51af 100644 --- a/core/java/android/os/Binder.java +++ b/core/java/android/os/Binder.java @@ -292,7 +292,7 @@ public class Binder implements IBinder { sWarnOnBlockingOnCurrentThread.set(sWarnOnBlocking); } - private static ThreadLocal<SomeArgs> sIdentity$ravenwood; + private static volatile ThreadLocal<SomeArgs> sIdentity$ravenwood; @android.ravenwood.annotation.RavenwoodKeepWholeClass private static class IdentitySupplier implements Supplier<SomeArgs> { diff --git a/core/java/android/os/HandlerThread.java b/core/java/android/os/HandlerThread.java index fcd57313a28d..36730cb07344 100644 --- a/core/java/android/os/HandlerThread.java +++ b/core/java/android/os/HandlerThread.java @@ -35,6 +35,7 @@ public class HandlerThread extends Thread { public HandlerThread(String name) { super(name); mPriority = Process.THREAD_PRIORITY_DEFAULT; + onCreated(); } /** @@ -46,8 +47,21 @@ public class HandlerThread extends Thread { public HandlerThread(String name, int priority) { super(name); mPriority = priority; + onCreated(); } - + + /** @hide */ + @android.ravenwood.annotation.RavenwoodReplace + protected void onCreated() { + } + + /** @hide */ + protected void onCreated$ravenwood() { + // Mark ourselves as daemon to enable tests to terminate quickly when finished, despite + // any HandlerThread instances that may be lingering around + setDaemon(true); + } + /** * Call back method that can be explicitly overridden if needed to execute some * setup before Looper loops. diff --git a/core/java/android/os/Process.java b/core/java/android/os/Process.java index 1f3a1620a9f2..7020a38ed08a 100644 --- a/core/java/android/os/Process.java +++ b/core/java/android/os/Process.java @@ -20,6 +20,7 @@ import static android.annotation.SystemApi.Client.MODULE_LIBRARIES; import android.annotation.ElapsedRealtimeLong; import android.annotation.FlaggedApi; +import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SuppressLint; @@ -844,7 +845,7 @@ public class Process { return "amd64".equals(System.getProperty("os.arch")); } - private static ThreadLocal<SomeArgs> sIdentity$ravenwood; + private static volatile ThreadLocal<SomeArgs> sIdentity$ravenwood; /** @hide */ @android.ravenwood.annotation.RavenwoodKeep @@ -1122,7 +1123,8 @@ public class Process { * priority. */ @android.ravenwood.annotation.RavenwoodReplace - public static final native void setThreadPriority(int tid, int priority) + public static final native void setThreadPriority(int tid, + @IntRange(from = -20, to = THREAD_PRIORITY_LOWEST) int priority) throws IllegalArgumentException, SecurityException; /** @hide */ @@ -1288,7 +1290,8 @@ public class Process { * @see #setThreadPriority(int, int) */ @android.ravenwood.annotation.RavenwoodReplace - public static final native void setThreadPriority(int priority) + public static final native void setThreadPriority( + @IntRange(from = -20, to = THREAD_PRIORITY_LOWEST) int priority) throws IllegalArgumentException, SecurityException; /** @hide */ @@ -1310,6 +1313,7 @@ public class Process { * <var>tid</var> does not exist. */ @android.ravenwood.annotation.RavenwoodReplace + @IntRange(from = -20, to = THREAD_PRIORITY_LOWEST) public static final native int getThreadPriority(int tid) throws IllegalArgumentException; diff --git a/core/java/android/os/TestLooperManager.java b/core/java/android/os/TestLooperManager.java index 5e7549fa67d8..4b16c1dce463 100644 --- a/core/java/android/os/TestLooperManager.java +++ b/core/java/android/os/TestLooperManager.java @@ -28,6 +28,7 @@ import java.util.concurrent.LinkedBlockingQueue; * The test code may use {@link #next()} to acquire messages that have been queued to this * {@link Looper}/{@link MessageQueue} and then {@link #execute} to run any that desires. */ +@android.ravenwood.annotation.RavenwoodKeepWholeClass public class TestLooperManager { private static final ArraySet<Looper> sHeldLoopers = new ArraySet<>(); diff --git a/core/java/android/os/storage/StorageManager.java b/core/java/android/os/storage/StorageManager.java index 9587db13ea87..3a57e84ee404 100644 --- a/core/java/android/os/storage/StorageManager.java +++ b/core/java/android/os/storage/StorageManager.java @@ -1669,12 +1669,6 @@ public class StorageManager { } } - /** {@hide} */ - @TestApi - public static boolean isUserKeyUnlocked(int userId) { - return isCeStorageUnlocked(userId); - } - /** * Returns true if the user's credential-encrypted (CE) storage is unlocked. * diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index e2380800bdb8..524b7336718f 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -6049,6 +6049,13 @@ public final class Settings { public static final String TOUCHPAD_TAP_TO_CLICK = "touchpad_tap_to_click"; /** + * Whether to enable tap dragging on touchpads. + * + * @hide + */ + public static final String TOUCHPAD_TAP_DRAGGING = "touchpad_tap_dragging"; + + /** * Whether to enable a right-click zone on touchpads. * * When set to 1, pressing to click in a section on the right-hand side of the touchpad will @@ -6270,6 +6277,7 @@ public final class Settings { PRIVATE_SETTINGS.add(TOUCHPAD_POINTER_SPEED); PRIVATE_SETTINGS.add(TOUCHPAD_NATURAL_SCROLLING); PRIVATE_SETTINGS.add(TOUCHPAD_TAP_TO_CLICK); + PRIVATE_SETTINGS.add(TOUCHPAD_TAP_DRAGGING); PRIVATE_SETTINGS.add(TOUCHPAD_RIGHT_CLICK_ZONE); PRIVATE_SETTINGS.add(CAMERA_FLASH_NOTIFICATION); PRIVATE_SETTINGS.add(SCREEN_FLASH_NOTIFICATION); diff --git a/core/java/android/provider/Telephony.java b/core/java/android/provider/Telephony.java index d47ff2e2cd99..2841dc05b164 100644 --- a/core/java/android/provider/Telephony.java +++ b/core/java/android/provider/Telephony.java @@ -3202,7 +3202,7 @@ public final class Telephony { /** * The infrastructure bitmask which the APN can be used on. For example, some APNs can only * be used when the device is on cellular, on satellite, or both. The default value is - * 1 (INFRASTRUCTURE_CELLULAR). + * 3 (INFRASTRUCTURE_CELLULAR | INFRASTRUCTURE_SATELLITE). * * <P>Type: INTEGER</P> * @hide @@ -4927,7 +4927,7 @@ public final class Telephony { /** * TelephonyProvider column name for satellite attach enabled for carrier. The value of this * column is set based on user settings. - * By default, it's disabled. + * By default, it's enabled. * * @hide */ diff --git a/core/java/android/service/notification/flags.aconfig b/core/java/android/service/notification/flags.aconfig index 3008b8d45252..446fe3de6482 100644 --- a/core/java/android/service/notification/flags.aconfig +++ b/core/java/android/service/notification/flags.aconfig @@ -8,14 +8,6 @@ flag { } flag { - name: "notification_lifetime_extension_refactor" - namespace: "systemui" - description: "Enables moving notification lifetime extension management from SystemUI to " - "Notification Manager Service" - bug: "299448097" -} - -flag { name: "redact_sensitive_notifications_from_untrusted_listeners" namespace: "systemui" description: "This flag controls the redacting of sensitive notifications from untrusted NotificationListenerServices" diff --git a/core/java/android/view/KeyEvent.java b/core/java/android/view/KeyEvent.java index c6601e8d3085..1ee9509b116a 100644 --- a/core/java/android/view/KeyEvent.java +++ b/core/java/android/view/KeyEvent.java @@ -19,6 +19,7 @@ package android.view; import static android.os.IInputConstants.INPUT_EVENT_FLAG_IS_ACCESSIBILITY_EVENT; import static android.view.Display.INVALID_DISPLAY; +import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.TestApi; @@ -31,6 +32,8 @@ import android.util.Log; import android.util.SparseIntArray; import android.view.KeyCharacterMap.KeyData; +import com.android.hardware.input.Flags; + import java.util.concurrent.TimeUnit; /** @@ -384,7 +387,13 @@ public class KeyEvent extends InputEvent implements Parcelable { public static final int KEYCODE_META_RIGHT = 118; /** Key code constant: Function modifier key. */ public static final int KEYCODE_FUNCTION = 119; - /** Key code constant: System Request / Print Screen key. */ + /** + * Key code constant: System Request / Print Screen key. + * + * This key is sent to the app first and only if the app doesn't handle it, the framework + * handles it (to take a screenshot), unlike {@code KEYCODE_TAKE_SCREENSHOT} which is + * fully handled by the framework. + */ public static final int KEYCODE_SYSRQ = 120; /** Key code constant: Break / Pause key. */ public static final int KEYCODE_BREAK = 121; @@ -921,14 +930,25 @@ public class KeyEvent extends InputEvent implements Parcelable { * User customizable key #4. */ public static final int KEYCODE_MACRO_4 = 316; - + /** Key code constant: To open emoji picker */ + @FlaggedApi(Flags.FLAG_EMOJI_AND_SCREENSHOT_KEYCODES_AVAILABLE) + public static final int KEYCODE_EMOJI_PICKER = 317; + /** + * Key code constant: To take a screenshot + * + * This key is fully handled by the framework and will not be sent to the foreground app, + * unlike {@code KEYCODE_SYSRQ} which is sent to the app first and only if the app + * doesn't handle it, the framework handles it (to take a screenshot). + */ + @FlaggedApi(Flags.FLAG_EMOJI_AND_SCREENSHOT_KEYCODES_AVAILABLE) + public static final int KEYCODE_SCREENSHOT = 318; /** * Integer value of the last KEYCODE. Increases as new keycodes are added to KeyEvent. * @hide */ @TestApi - public static final int LAST_KEYCODE = KEYCODE_MACRO_4; + public static final int LAST_KEYCODE = KEYCODE_SCREENSHOT; // NOTE: If you add a new keycode here you must also add it to: // isSystem() diff --git a/core/java/android/view/MotionPredictor.java b/core/java/android/view/MotionPredictor.java index 27af3000fc8b..db2efaa81ef3 100644 --- a/core/java/android/view/MotionPredictor.java +++ b/core/java/android/view/MotionPredictor.java @@ -20,6 +20,8 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; +import com.android.internal.annotations.VisibleForTesting; + import libcore.util.NativeAllocationRegistry; /** @@ -57,11 +59,21 @@ public final class MotionPredictor { * @param context The context for the predictions */ public MotionPredictor(@NonNull Context context) { - mIsPredictionEnabled = context.getResources().getBoolean( - com.android.internal.R.bool.config_enableMotionPrediction); - final int offsetNanos = context.getResources().getInteger( - com.android.internal.R.integer.config_motionPredictionOffsetNanos); - mPtr = nativeInitialize(offsetNanos); + this( + context.getResources().getBoolean( + com.android.internal.R.bool.config_enableMotionPrediction), + context.getResources().getInteger( + com.android.internal.R.integer.config_motionPredictionOffsetNanos)); + } + + /** + * Internal constructor for testing. + * @hide + */ + @VisibleForTesting + public MotionPredictor(boolean isPredictionEnabled, int motionPredictionOffsetNanos) { + mIsPredictionEnabled = isPredictionEnabled; + mPtr = nativeInitialize(motionPredictionOffsetNanos); RegistryHolder.REGISTRY.registerNativeAllocation(this, mPtr); } diff --git a/core/java/android/view/SurfaceControlViewHost.java b/core/java/android/view/SurfaceControlViewHost.java index 4840f003da3e..5249fd5253f2 100644 --- a/core/java/android/view/SurfaceControlViewHost.java +++ b/core/java/android/view/SurfaceControlViewHost.java @@ -538,8 +538,8 @@ public class SurfaceControlViewHost { } private void addWindowToken(WindowManager.LayoutParams attrs) { - final WindowManagerImpl wm = - (WindowManagerImpl) mViewRoot.mContext.getSystemService(Context.WINDOW_SERVICE); + final WindowManager wm = + (WindowManager) mViewRoot.mContext.getSystemService(Context.WINDOW_SERVICE); attrs.token = wm.getDefaultToken(); } diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 7bc832ef9e3f..f9abc8afe824 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -399,6 +399,8 @@ public final class ViewRootImpl implements ViewParent, @Nullable private ContentObserver mForceInvertObserver; + private static final int INVALID_VALUE = Integer.MIN_VALUE; + private int mForceInvertEnabled = INVALID_VALUE; /** * Callback for notifying about global configuration changes. */ @@ -1604,6 +1606,23 @@ public final class ViewRootImpl implements ViewParent, } } + private boolean isForceInvertEnabled() { + if (mForceInvertEnabled == INVALID_VALUE) { + reloadForceInvertEnabled(); + } + return mForceInvertEnabled == 1; + } + + private void reloadForceInvertEnabled() { + if (forceInvertColor()) { + mForceInvertEnabled = Settings.Secure.getIntForUser( + mContext.getContentResolver(), + Settings.Secure.ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED, + /* def= */ 0, + UserHandle.myUserId()); + } + } + /** * Register any kind of listeners if setView was success. */ @@ -1630,6 +1649,7 @@ public final class ViewRootImpl implements ViewParent, mForceInvertObserver = new ContentObserver(mHandler) { @Override public void onChange(boolean selfChange) { + reloadForceInvertEnabled(); updateForceDarkMode(); } }; @@ -1850,16 +1870,11 @@ public final class ViewRootImpl implements ViewParent, @VisibleForTesting public @ForceDarkType.ForceDarkTypeDef int determineForceDarkType() { if (forceInvertColor()) { - boolean isForceInvertEnabled = Settings.Secure.getIntForUser( - mContext.getContentResolver(), - Settings.Secure.ACCESSIBILITY_FORCE_INVERT_COLOR_ENABLED, - /* def= */ 0, - UserHandle.myUserId()) == 1; // Force invert ignores all developer opt-outs. // We also ignore dark theme, since the app developer can override the user's preference // for dark mode in configuration.uiMode. Instead, we assume that the force invert // setting will be enabled at the same time dark theme is in the Settings app. - if (isForceInvertEnabled) { + if (isForceInvertEnabled()) { return ForceDarkType.FORCE_INVERT_COLOR_DARK; } } diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index 427d053f754e..c78826116426 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -6109,4 +6109,12 @@ public interface WindowManager extends ViewManager { throw new UnsupportedOperationException( "getSurfaceControlInputClientToken is not implemented"); } + + /** + * @hide + */ + default @NonNull IBinder getDefaultToken() { + throw new UnsupportedOperationException( + "getDefaultToken is not implemented"); + } } diff --git a/core/java/android/view/WindowManagerImpl.java b/core/java/android/view/WindowManagerImpl.java index 41d181c1b10c..5072ad755cba 100644 --- a/core/java/android/view/WindowManagerImpl.java +++ b/core/java/android/view/WindowManagerImpl.java @@ -458,7 +458,9 @@ public final class WindowManagerImpl implements WindowManager { return null; } - IBinder getDefaultToken() { + @Override + @NonNull + public IBinder getDefaultToken() { return mDefaultToken; } diff --git a/core/java/android/view/accessibility/AccessibilityManager.java b/core/java/android/view/accessibility/AccessibilityManager.java index 7782fd7e6c2a..ef1bf5a5c548 100644 --- a/core/java/android/view/accessibility/AccessibilityManager.java +++ b/core/java/android/view/accessibility/AccessibilityManager.java @@ -62,6 +62,7 @@ import android.util.ArrayMap; import android.util.Log; import android.util.SparseArray; import android.view.IWindow; +import android.view.SurfaceControl; import android.view.View; import android.view.accessibility.AccessibilityEvent.EventType; @@ -2404,4 +2405,29 @@ public final class AccessibilityManager { throw re.rethrowFromSystemServer(); } } + + + /** + * Attaches a {@link android.view.SurfaceControl} containing an accessibility overlay to the + * specified display. + * + * @hide + */ + @RequiresPermission(android.Manifest.permission.INTERNAL_SYSTEM_WINDOW) + public void attachAccessibilityOverlayToDisplay( + int displayId, @NonNull SurfaceControl surfaceControl) { + final IAccessibilityManager service; + synchronized (mLock) { + service = getServiceLocked(); + if (service == null) { + return; + } + } + try { + service.attachAccessibilityOverlayToDisplay_enforcePermission( + displayId, surfaceControl); + } catch (RemoteException re) { + throw re.rethrowFromSystemServer(); + } + } } diff --git a/core/java/android/view/accessibility/IAccessibilityManager.aidl b/core/java/android/view/accessibility/IAccessibilityManager.aidl index 9c04c27d189a..1c5d29e0ff1e 100644 --- a/core/java/android/view/accessibility/IAccessibilityManager.aidl +++ b/core/java/android/view/accessibility/IAccessibilityManager.aidl @@ -31,6 +31,7 @@ import android.view.accessibility.IMagnificationConnection; import android.view.InputEvent; import android.view.IWindow; import android.view.MagnificationSpec; +import android.view.SurfaceControl; /** * Interface implemented by the AccessibilityManagerService called by @@ -136,4 +137,7 @@ interface IAccessibilityManager { MagnificationSpec magnificationSpec; } WindowTransformationSpec getWindowTransformationSpec(int windowId); + + @JavaPassthrough(annotation="@android.annotation.RequiresPermission(android.Manifest.permission.INTERNAL_SYSTEM_WINDOW)") + void attachAccessibilityOverlayToDisplay_enforcePermission(int displayId, in SurfaceControl surfaceControl); } diff --git a/core/java/android/view/contentprotection/flags/content_protection_flags.aconfig b/core/java/android/view/contentprotection/flags/content_protection_flags.aconfig index 2a3008a53635..5d3153c00e8a 100644 --- a/core/java/android/view/contentprotection/flags/content_protection_flags.aconfig +++ b/core/java/android/view/contentprotection/flags/content_protection_flags.aconfig @@ -34,3 +34,10 @@ flag { description: "If true, an appop is logged when a notification is rapidly cleared by a notification listener." bug: "289080543" } + +flag { + name: "manage_device_policy_enabled" + namespace: "content_protection" + description: "If true, the APIs to manage content protection device policy will be enabled." + bug: "319477846" +} diff --git a/core/java/android/webkit/flags.aconfig b/core/java/android/webkit/flags.aconfig new file mode 100644 index 000000000000..6938b29e78e9 --- /dev/null +++ b/core/java/android/webkit/flags.aconfig @@ -0,0 +1,9 @@ +package: "android.webkit" + +flag { + name: "update_service_ipc_wrapper" + namespace: "webview" + description: "New API: proper wrapper for IWebViewUpdateService" + bug: "319292658" + is_fixed_read_only: true +} diff --git a/core/java/com/android/internal/os/BatteryStatsHistory.java b/core/java/com/android/internal/os/BatteryStatsHistory.java index 7a79e0f7cfea..aa60cc9e672c 100644 --- a/core/java/com/android/internal/os/BatteryStatsHistory.java +++ b/core/java/com/android/internal/os/BatteryStatsHistory.java @@ -490,7 +490,7 @@ public class BatteryStatsHistory { * Returns true if this instance only supports reading history. */ public boolean isReadOnly() { - return mActiveFile == null || mHistoryDir == null; + return !mMutable || mActiveFile == null || mHistoryDir == null; } /** @@ -508,6 +508,13 @@ public class BatteryStatsHistory { * create next history file. */ public void startNextFile(long elapsedRealtimeMs) { + synchronized (this) { + startNextFileLocked(elapsedRealtimeMs); + } + } + + @GuardedBy("this") + private void startNextFileLocked(long elapsedRealtimeMs) { if (mMaxHistoryFiles == 0) { Slog.wtf(TAG, "mMaxHistoryFiles should not be zero when writing history"); return; @@ -548,10 +555,7 @@ public class BatteryStatsHistory { } mWrittenPowerStatsDescriptors.clear(); - - synchronized (this) { - cleanupLocked(); - } + cleanupLocked(); } @GuardedBy("this") @@ -599,27 +603,31 @@ public class BatteryStatsHistory { * number 0 again. */ public void reset() { - if (DEBUG) Slog.i(TAG, "********** CLEARING HISTORY!"); - for (BatteryHistoryFile file : mHistoryFiles) { - file.atomicFile.delete(); - } - mHistoryFiles.clear(); + synchronized (this) { + if (DEBUG) Slog.i(TAG, "********** CLEARING HISTORY!"); + for (BatteryHistoryFile file : mHistoryFiles) { + file.atomicFile.delete(); + } + mHistoryFiles.clear(); - BatteryHistoryFile name = makeBatteryHistoryFile(); - mHistoryFiles.add(name); - setActiveFile(name); + BatteryHistoryFile name = makeBatteryHistoryFile(); + mHistoryFiles.add(name); + setActiveFile(name); - initHistoryBuffer(); + initHistoryBuffer(); + } } /** * Returns the monotonic clock time when the available battery history collection started. */ public long getStartTime() { - if (!mHistoryFiles.isEmpty()) { - return mHistoryFiles.get(0).monotonicTimeMs; - } else { - return mHistoryBufferStartTime; + synchronized (this) { + if (!mHistoryFiles.isEmpty()) { + return mHistoryFiles.get(0).monotonicTimeMs; + } else { + return mHistoryBufferStartTime; + } } } @@ -633,11 +641,14 @@ public class BatteryStatsHistory { */ @NonNull public BatteryStatsHistoryIterator iterate(long startTimeMs, long endTimeMs) { + if (mMutable) { + return copy().iterate(startTimeMs, endTimeMs); + } + mCurrentFileIndex = 0; mCurrentParcel = null; mCurrentParcelEnd = 0; mParcelIndex = 0; - mMutable = false; if (mWritableHistory != null) { synchronized (mWritableHistory) { mWritableHistory.setCleanupEnabledLocked(false); @@ -650,14 +661,11 @@ public class BatteryStatsHistory { * Finish iterating history files and history buffer. */ void iteratorFinished() { - // setDataPosition so mHistoryBuffer Parcel can be written. mHistoryBuffer.setDataPosition(mHistoryBuffer.dataSize()); if (mWritableHistory != null) { synchronized (mWritableHistory) { mWritableHistory.setCleanupEnabledLocked(true); } - } else { - mMutable = true; } } @@ -671,6 +679,8 @@ public class BatteryStatsHistory { */ @Nullable public Parcel getNextParcel(long startTimeMs, long endTimeMs) { + checkImmutable(); + // First iterate through all records in current parcel. if (mCurrentParcel != null) { if (mCurrentParcel.dataPosition() < mCurrentParcelEnd) { @@ -754,6 +764,12 @@ public class BatteryStatsHistory { return mCurrentParcel; } + private void checkImmutable() { + if (mMutable) { + throw new IllegalStateException("Iterating over a mutable battery history"); + } + } + /** * Read history file into a parcel. * @@ -863,8 +879,10 @@ public class BatteryStatsHistory { * @param out the output parcel */ public void writeToParcel(Parcel out) { - writeHistoryBuffer(out); - writeToParcel(out, false /* useBlobs */); + synchronized (this) { + writeHistoryBuffer(out); + writeToParcel(out, false /* useBlobs */); + } } /** @@ -874,8 +892,10 @@ public class BatteryStatsHistory { * @param out the output parcel */ public void writeToBatteryUsageStatsParcel(Parcel out) { - out.writeBlob(mHistoryBuffer.marshall()); - writeToParcel(out, true /* useBlobs */); + synchronized (this) { + out.writeBlob(mHistoryBuffer.marshall()); + writeToParcel(out, true /* useBlobs */); + } } private void writeToParcel(Parcel out, boolean useBlobs) { @@ -1022,14 +1042,18 @@ public class BatteryStatsHistory { * Enables/disables recording of history. When disabled, all "record*" calls are a no-op. */ public void setHistoryRecordingEnabled(boolean enabled) { - mRecordingHistory = enabled; + synchronized (this) { + mRecordingHistory = enabled; + } } /** * Returns true if history recording is enabled. */ public boolean isRecordingHistory() { - return mRecordingHistory; + synchronized (this) { + return mRecordingHistory; + } } /** @@ -1037,8 +1061,10 @@ public class BatteryStatsHistory { */ @VisibleForTesting public void forceRecordAllHistory() { - mHaveBatteryLevel = true; - mRecordingHistory = true; + synchronized (this) { + mHaveBatteryLevel = true; + mRecordingHistory = true; + } } /** @@ -1046,37 +1072,43 @@ public class BatteryStatsHistory { */ public void startRecordingHistory(final long elapsedRealtimeMs, final long uptimeMs, boolean reset) { - mRecordingHistory = true; - mHistoryCur.currentTime = mClock.currentTimeMillis(); - writeHistoryItem(elapsedRealtimeMs, uptimeMs, mHistoryCur, - reset ? HistoryItem.CMD_RESET : HistoryItem.CMD_CURRENT_TIME); - mHistoryCur.currentTime = 0; + synchronized (this) { + mRecordingHistory = true; + mHistoryCur.currentTime = mClock.currentTimeMillis(); + writeHistoryItem(elapsedRealtimeMs, uptimeMs, mHistoryCur, + reset ? HistoryItem.CMD_RESET : HistoryItem.CMD_CURRENT_TIME); + mHistoryCur.currentTime = 0; + } } /** * Prepares to continue recording after restoring previous history from persistent storage. */ public void continueRecordingHistory() { - if (mHistoryBuffer.dataPosition() <= 0 && mHistoryFiles.size() <= 1) { - return; - } + synchronized (this) { + if (mHistoryBuffer.dataPosition() <= 0 && mHistoryFiles.size() <= 1) { + return; + } - mRecordingHistory = true; - final long elapsedRealtimeMs = mClock.elapsedRealtime(); - final long uptimeMs = mClock.uptimeMillis(); - writeHistoryItem(elapsedRealtimeMs, uptimeMs, mHistoryCur, HistoryItem.CMD_START); - startRecordingHistory(elapsedRealtimeMs, uptimeMs, false); + mRecordingHistory = true; + final long elapsedRealtimeMs = mClock.elapsedRealtime(); + final long uptimeMs = mClock.uptimeMillis(); + writeHistoryItem(elapsedRealtimeMs, uptimeMs, mHistoryCur, HistoryItem.CMD_START); + startRecordingHistory(elapsedRealtimeMs, uptimeMs, false); + } } /** * Notes the current battery state to be reflected in the next written history item. */ public void setBatteryState(boolean charging, int status, int level, int chargeUah) { - mHaveBatteryLevel = true; - setChargingState(charging); - mHistoryCur.batteryStatus = (byte) status; - mHistoryCur.batteryLevel = (byte) level; - mHistoryCur.batteryChargeUah = chargeUah; + synchronized (this) { + mHaveBatteryLevel = true; + setChargingState(charging); + mHistoryCur.batteryStatus = (byte) status; + mHistoryCur.batteryLevel = (byte) level; + mHistoryCur.batteryChargeUah = chargeUah; + } } /** @@ -1084,24 +1116,28 @@ public class BatteryStatsHistory { */ public void setBatteryState(int status, int level, int health, int plugType, int temperature, int voltageMv, int chargeUah) { - mHaveBatteryLevel = true; - mHistoryCur.batteryStatus = (byte) status; - mHistoryCur.batteryLevel = (byte) level; - mHistoryCur.batteryHealth = (byte) health; - mHistoryCur.batteryPlugType = (byte) plugType; - mHistoryCur.batteryTemperature = (short) temperature; - mHistoryCur.batteryVoltage = (char) voltageMv; - mHistoryCur.batteryChargeUah = chargeUah; + synchronized (this) { + mHaveBatteryLevel = true; + mHistoryCur.batteryStatus = (byte) status; + mHistoryCur.batteryLevel = (byte) level; + mHistoryCur.batteryHealth = (byte) health; + mHistoryCur.batteryPlugType = (byte) plugType; + mHistoryCur.batteryTemperature = (short) temperature; + mHistoryCur.batteryVoltage = (char) voltageMv; + mHistoryCur.batteryChargeUah = chargeUah; + } } /** * Notes the current power plugged-in state to be reflected in the next written history item. */ public void setPluggedInState(boolean pluggedIn) { - if (pluggedIn) { - mHistoryCur.states |= HistoryItem.STATE_BATTERY_PLUGGED_FLAG; - } else { - mHistoryCur.states &= ~HistoryItem.STATE_BATTERY_PLUGGED_FLAG; + synchronized (this) { + if (pluggedIn) { + mHistoryCur.states |= HistoryItem.STATE_BATTERY_PLUGGED_FLAG; + } else { + mHistoryCur.states &= ~HistoryItem.STATE_BATTERY_PLUGGED_FLAG; + } } } @@ -1109,10 +1145,12 @@ public class BatteryStatsHistory { * Notes the current battery charging state to be reflected in the next written history item. */ public void setChargingState(boolean charging) { - if (charging) { - mHistoryCur.states2 |= HistoryItem.STATE2_CHARGING_FLAG; - } else { - mHistoryCur.states2 &= ~HistoryItem.STATE2_CHARGING_FLAG; + synchronized (this) { + if (charging) { + mHistoryCur.states2 |= HistoryItem.STATE2_CHARGING_FLAG; + } else { + mHistoryCur.states2 &= ~HistoryItem.STATE2_CHARGING_FLAG; + } } } @@ -1121,38 +1159,44 @@ public class BatteryStatsHistory { */ public void recordEvent(long elapsedRealtimeMs, long uptimeMs, int code, String name, int uid) { - mHistoryCur.eventCode = code; - mHistoryCur.eventTag = mHistoryCur.localEventTag; - mHistoryCur.eventTag.string = name; - mHistoryCur.eventTag.uid = uid; - writeHistoryItem(elapsedRealtimeMs, uptimeMs); + synchronized (this) { + mHistoryCur.eventCode = code; + mHistoryCur.eventTag = mHistoryCur.localEventTag; + mHistoryCur.eventTag.string = name; + mHistoryCur.eventTag.uid = uid; + writeHistoryItem(elapsedRealtimeMs, uptimeMs); + } } /** * Records a time change event. */ public void recordCurrentTimeChange(long elapsedRealtimeMs, long uptimeMs, long currentTimeMs) { - if (!mRecordingHistory) { - return; - } + synchronized (this) { + if (!mRecordingHistory) { + return; + } - mHistoryCur.currentTime = currentTimeMs; - writeHistoryItem(elapsedRealtimeMs, uptimeMs, mHistoryCur, - HistoryItem.CMD_CURRENT_TIME); - mHistoryCur.currentTime = 0; + mHistoryCur.currentTime = currentTimeMs; + writeHistoryItem(elapsedRealtimeMs, uptimeMs, mHistoryCur, + HistoryItem.CMD_CURRENT_TIME); + mHistoryCur.currentTime = 0; + } } /** * Records a system shutdown event. */ public void recordShutdownEvent(long elapsedRealtimeMs, long uptimeMs, long currentTimeMs) { - if (!mRecordingHistory) { - return; - } + synchronized (this) { + if (!mRecordingHistory) { + return; + } - mHistoryCur.currentTime = currentTimeMs; - writeHistoryItem(elapsedRealtimeMs, uptimeMs, mHistoryCur, HistoryItem.CMD_SHUTDOWN); - mHistoryCur.currentTime = 0; + mHistoryCur.currentTime = currentTimeMs; + writeHistoryItem(elapsedRealtimeMs, uptimeMs, mHistoryCur, HistoryItem.CMD_SHUTDOWN); + mHistoryCur.currentTime = 0; + } } /** @@ -1160,13 +1204,15 @@ public class BatteryStatsHistory { */ public void recordBatteryState(long elapsedRealtimeMs, long uptimeMs, int batteryLevel, boolean isPlugged) { - mHistoryCur.batteryLevel = (byte) batteryLevel; - setPluggedInState(isPlugged); - if (DEBUG) { - Slog.v(TAG, "Battery unplugged to: " - + Integer.toHexString(mHistoryCur.states)); + synchronized (this) { + mHistoryCur.batteryLevel = (byte) batteryLevel; + setPluggedInState(isPlugged); + if (DEBUG) { + Slog.v(TAG, "Battery unplugged to: " + + Integer.toHexString(mHistoryCur.states)); + } + writeHistoryItem(elapsedRealtimeMs, uptimeMs); } - writeHistoryItem(elapsedRealtimeMs, uptimeMs); } /** @@ -1174,9 +1220,11 @@ public class BatteryStatsHistory { */ public void recordPowerStats(long elapsedRealtimeMs, long uptimeMs, PowerStats powerStats) { - mHistoryCur.powerStats = powerStats; - mHistoryCur.states2 |= HistoryItem.STATE2_EXTENSIONS_FLAG; - writeHistoryItem(elapsedRealtimeMs, uptimeMs); + synchronized (this) { + mHistoryCur.powerStats = powerStats; + mHistoryCur.states2 |= HistoryItem.STATE2_EXTENSIONS_FLAG; + writeHistoryItem(elapsedRealtimeMs, uptimeMs); + } } /** @@ -1184,11 +1232,13 @@ public class BatteryStatsHistory { */ public void recordProcessStateChange(long elapsedRealtimeMs, long uptimeMs, int uid, @BatteryConsumer.ProcessState int processState) { - mHistoryCur.processStateChange = mHistoryCur.localProcessStateChange; - mHistoryCur.processStateChange.uid = uid; - mHistoryCur.processStateChange.processState = processState; - mHistoryCur.states2 |= HistoryItem.STATE2_EXTENSIONS_FLAG; - writeHistoryItem(elapsedRealtimeMs, uptimeMs); + synchronized (this) { + mHistoryCur.processStateChange = mHistoryCur.localProcessStateChange; + mHistoryCur.processStateChange.uid = uid; + mHistoryCur.processStateChange.processState = processState; + mHistoryCur.states2 |= HistoryItem.STATE2_EXTENSIONS_FLAG; + writeHistoryItem(elapsedRealtimeMs, uptimeMs); + } } /** @@ -1197,8 +1247,10 @@ public class BatteryStatsHistory { */ public void recordWifiConsumedCharge(long elapsedRealtimeMs, long uptimeMs, double monitoredRailChargeMah) { - mHistoryCur.wifiRailChargeMah += monitoredRailChargeMah; - writeHistoryItem(elapsedRealtimeMs, uptimeMs); + synchronized (this) { + mHistoryCur.wifiRailChargeMah += monitoredRailChargeMah; + writeHistoryItem(elapsedRealtimeMs, uptimeMs); + } } /** @@ -1206,10 +1258,12 @@ public class BatteryStatsHistory { */ public void recordWakelockStartEvent(long elapsedRealtimeMs, long uptimeMs, String historyName, int uid) { - mHistoryCur.wakelockTag = mHistoryCur.localWakelockTag; - mHistoryCur.wakelockTag.string = historyName; - mHistoryCur.wakelockTag.uid = uid; - recordStateStartEvent(elapsedRealtimeMs, uptimeMs, HistoryItem.STATE_WAKE_LOCK_FLAG); + synchronized (this) { + mHistoryCur.wakelockTag = mHistoryCur.localWakelockTag; + mHistoryCur.wakelockTag.string = historyName; + mHistoryCur.wakelockTag.uid = uid; + recordStateStartEvent(elapsedRealtimeMs, uptimeMs, HistoryItem.STATE_WAKE_LOCK_FLAG); + } } /** @@ -1217,18 +1271,20 @@ public class BatteryStatsHistory { */ public boolean maybeUpdateWakelockTag(long elapsedRealtimeMs, long uptimeMs, String historyName, int uid) { - if (mHistoryLastWritten.cmd != HistoryItem.CMD_UPDATE) { - return false; - } - if (mHistoryLastWritten.wakelockTag != null) { - // We'll try to update the last tag. - mHistoryLastWritten.wakelockTag = null; - mHistoryCur.wakelockTag = mHistoryCur.localWakelockTag; - mHistoryCur.wakelockTag.string = historyName; - mHistoryCur.wakelockTag.uid = uid; - writeHistoryItem(elapsedRealtimeMs, uptimeMs); + synchronized (this) { + if (mHistoryLastWritten.cmd != HistoryItem.CMD_UPDATE) { + return false; + } + if (mHistoryLastWritten.wakelockTag != null) { + // We'll try to update the last tag. + mHistoryLastWritten.wakelockTag = null; + mHistoryCur.wakelockTag = mHistoryCur.localWakelockTag; + mHistoryCur.wakelockTag.string = historyName; + mHistoryCur.wakelockTag.uid = uid; + writeHistoryItem(elapsedRealtimeMs, uptimeMs); + } + return true; } - return true; } /** @@ -1236,26 +1292,32 @@ public class BatteryStatsHistory { */ public void recordWakelockStopEvent(long elapsedRealtimeMs, long uptimeMs, String historyName, int uid) { - mHistoryCur.wakelockTag = mHistoryCur.localWakelockTag; - mHistoryCur.wakelockTag.string = historyName != null ? historyName : ""; - mHistoryCur.wakelockTag.uid = uid; - recordStateStopEvent(elapsedRealtimeMs, uptimeMs, HistoryItem.STATE_WAKE_LOCK_FLAG); + synchronized (this) { + mHistoryCur.wakelockTag = mHistoryCur.localWakelockTag; + mHistoryCur.wakelockTag.string = historyName != null ? historyName : ""; + mHistoryCur.wakelockTag.uid = uid; + recordStateStopEvent(elapsedRealtimeMs, uptimeMs, HistoryItem.STATE_WAKE_LOCK_FLAG); + } } /** * Records an event when some state flag changes to true. */ public void recordStateStartEvent(long elapsedRealtimeMs, long uptimeMs, int stateFlags) { - mHistoryCur.states |= stateFlags; - writeHistoryItem(elapsedRealtimeMs, uptimeMs); + synchronized (this) { + mHistoryCur.states |= stateFlags; + writeHistoryItem(elapsedRealtimeMs, uptimeMs); + } } /** * Records an event when some state flag changes to false. */ public void recordStateStopEvent(long elapsedRealtimeMs, long uptimeMs, int stateFlags) { - mHistoryCur.states &= ~stateFlags; - writeHistoryItem(elapsedRealtimeMs, uptimeMs); + synchronized (this) { + mHistoryCur.states &= ~stateFlags; + writeHistoryItem(elapsedRealtimeMs, uptimeMs); + } } /** @@ -1263,34 +1325,42 @@ public class BatteryStatsHistory { */ public void recordStateChangeEvent(long elapsedRealtimeMs, long uptimeMs, int stateStartFlags, int stateStopFlags) { - mHistoryCur.states = (mHistoryCur.states | stateStartFlags) & ~stateStopFlags; - writeHistoryItem(elapsedRealtimeMs, uptimeMs); + synchronized (this) { + mHistoryCur.states = (mHistoryCur.states | stateStartFlags) & ~stateStopFlags; + writeHistoryItem(elapsedRealtimeMs, uptimeMs); + } } /** * Records an event when some state2 flag changes to true. */ public void recordState2StartEvent(long elapsedRealtimeMs, long uptimeMs, int stateFlags) { - mHistoryCur.states2 |= stateFlags; - writeHistoryItem(elapsedRealtimeMs, uptimeMs); + synchronized (this) { + mHistoryCur.states2 |= stateFlags; + writeHistoryItem(elapsedRealtimeMs, uptimeMs); + } } /** * Records an event when some state2 flag changes to false. */ public void recordState2StopEvent(long elapsedRealtimeMs, long uptimeMs, int stateFlags) { - mHistoryCur.states2 &= ~stateFlags; - writeHistoryItem(elapsedRealtimeMs, uptimeMs); + synchronized (this) { + mHistoryCur.states2 &= ~stateFlags; + writeHistoryItem(elapsedRealtimeMs, uptimeMs); + } } /** * Records an wakeup event. */ public void recordWakeupEvent(long elapsedRealtimeMs, long uptimeMs, String reason) { - mHistoryCur.wakeReasonTag = mHistoryCur.localWakeReasonTag; - mHistoryCur.wakeReasonTag.string = reason; - mHistoryCur.wakeReasonTag.uid = 0; - writeHistoryItem(elapsedRealtimeMs, uptimeMs); + synchronized (this) { + mHistoryCur.wakeReasonTag = mHistoryCur.localWakeReasonTag; + mHistoryCur.wakeReasonTag.string = reason; + mHistoryCur.wakeReasonTag.uid = 0; + writeHistoryItem(elapsedRealtimeMs, uptimeMs); + } } /** @@ -1298,10 +1368,12 @@ public class BatteryStatsHistory { */ public void recordScreenBrightnessEvent(long elapsedRealtimeMs, long uptimeMs, int brightnessBin) { - mHistoryCur.states = setBitField(mHistoryCur.states, brightnessBin, - HistoryItem.STATE_BRIGHTNESS_SHIFT, - HistoryItem.STATE_BRIGHTNESS_MASK); - writeHistoryItem(elapsedRealtimeMs, uptimeMs); + synchronized (this) { + mHistoryCur.states = setBitField(mHistoryCur.states, brightnessBin, + HistoryItem.STATE_BRIGHTNESS_SHIFT, + HistoryItem.STATE_BRIGHTNESS_MASK); + writeHistoryItem(elapsedRealtimeMs, uptimeMs); + } } /** @@ -1309,20 +1381,24 @@ public class BatteryStatsHistory { */ public void recordGpsSignalQualityEvent(long elapsedRealtimeMs, long uptimeMs, int signalLevel) { - mHistoryCur.states2 = setBitField(mHistoryCur.states2, signalLevel, - HistoryItem.STATE2_GPS_SIGNAL_QUALITY_SHIFT, - HistoryItem.STATE2_GPS_SIGNAL_QUALITY_MASK); - writeHistoryItem(elapsedRealtimeMs, uptimeMs); + synchronized (this) { + mHistoryCur.states2 = setBitField(mHistoryCur.states2, signalLevel, + HistoryItem.STATE2_GPS_SIGNAL_QUALITY_SHIFT, + HistoryItem.STATE2_GPS_SIGNAL_QUALITY_MASK); + writeHistoryItem(elapsedRealtimeMs, uptimeMs); + } } /** * Records a device idle mode change event. */ public void recordDeviceIdleEvent(long elapsedRealtimeMs, long uptimeMs, int mode) { - mHistoryCur.states2 = setBitField(mHistoryCur.states2, mode, - HistoryItem.STATE2_DEVICE_IDLE_SHIFT, - HistoryItem.STATE2_DEVICE_IDLE_MASK); - writeHistoryItem(elapsedRealtimeMs, uptimeMs); + synchronized (this) { + mHistoryCur.states2 = setBitField(mHistoryCur.states2, mode, + HistoryItem.STATE2_DEVICE_IDLE_SHIFT, + HistoryItem.STATE2_DEVICE_IDLE_MASK); + writeHistoryItem(elapsedRealtimeMs, uptimeMs); + } } /** @@ -1330,20 +1406,22 @@ public class BatteryStatsHistory { */ public void recordPhoneStateChangeEvent(long elapsedRealtimeMs, long uptimeMs, int addStateFlag, int removeStateFlag, int state, int signalStrength) { - mHistoryCur.states = (mHistoryCur.states | addStateFlag) & ~removeStateFlag; - if (state != -1) { - mHistoryCur.states = - setBitField(mHistoryCur.states, state, - HistoryItem.STATE_PHONE_STATE_SHIFT, - HistoryItem.STATE_PHONE_STATE_MASK); - } - if (signalStrength != -1) { - mHistoryCur.states = - setBitField(mHistoryCur.states, signalStrength, - HistoryItem.STATE_PHONE_SIGNAL_STRENGTH_SHIFT, - HistoryItem.STATE_PHONE_SIGNAL_STRENGTH_MASK); + synchronized (this) { + mHistoryCur.states = (mHistoryCur.states | addStateFlag) & ~removeStateFlag; + if (state != -1) { + mHistoryCur.states = + setBitField(mHistoryCur.states, state, + HistoryItem.STATE_PHONE_STATE_SHIFT, + HistoryItem.STATE_PHONE_STATE_MASK); + } + if (signalStrength != -1) { + mHistoryCur.states = + setBitField(mHistoryCur.states, signalStrength, + HistoryItem.STATE_PHONE_SIGNAL_STRENGTH_SHIFT, + HistoryItem.STATE_PHONE_SIGNAL_STRENGTH_MASK); + } + writeHistoryItem(elapsedRealtimeMs, uptimeMs); } - writeHistoryItem(elapsedRealtimeMs, uptimeMs); } /** @@ -1351,10 +1429,12 @@ public class BatteryStatsHistory { */ public void recordDataConnectionTypeChangeEvent(long elapsedRealtimeMs, long uptimeMs, int dataConnectionType) { - mHistoryCur.states = setBitField(mHistoryCur.states, dataConnectionType, - HistoryItem.STATE_DATA_CONNECTION_SHIFT, - HistoryItem.STATE_DATA_CONNECTION_MASK); - writeHistoryItem(elapsedRealtimeMs, uptimeMs); + synchronized (this) { + mHistoryCur.states = setBitField(mHistoryCur.states, dataConnectionType, + HistoryItem.STATE_DATA_CONNECTION_SHIFT, + HistoryItem.STATE_DATA_CONNECTION_MASK); + writeHistoryItem(elapsedRealtimeMs, uptimeMs); + } } /** @@ -1362,10 +1442,12 @@ public class BatteryStatsHistory { */ public void recordNrStateChangeEvent(long elapsedRealtimeMs, long uptimeMs, int nrState) { - mHistoryCur.states2 = setBitField(mHistoryCur.states2, nrState, - HistoryItem.STATE2_NR_STATE_SHIFT, - HistoryItem.STATE2_NR_STATE_MASK); - writeHistoryItem(elapsedRealtimeMs, uptimeMs); + synchronized (this) { + mHistoryCur.states2 = setBitField(mHistoryCur.states2, nrState, + HistoryItem.STATE2_NR_STATE_SHIFT, + HistoryItem.STATE2_NR_STATE_MASK); + writeHistoryItem(elapsedRealtimeMs, uptimeMs); + } } /** @@ -1373,11 +1455,13 @@ public class BatteryStatsHistory { */ public void recordWifiSupplicantStateChangeEvent(long elapsedRealtimeMs, long uptimeMs, int supplState) { - mHistoryCur.states2 = - setBitField(mHistoryCur.states2, supplState, - HistoryItem.STATE2_WIFI_SUPPL_STATE_SHIFT, - HistoryItem.STATE2_WIFI_SUPPL_STATE_MASK); - writeHistoryItem(elapsedRealtimeMs, uptimeMs); + synchronized (this) { + mHistoryCur.states2 = + setBitField(mHistoryCur.states2, supplState, + HistoryItem.STATE2_WIFI_SUPPL_STATE_SHIFT, + HistoryItem.STATE2_WIFI_SUPPL_STATE_MASK); + writeHistoryItem(elapsedRealtimeMs, uptimeMs); + } } /** @@ -1385,11 +1469,13 @@ public class BatteryStatsHistory { */ public void recordWifiSignalStrengthChangeEvent(long elapsedRealtimeMs, long uptimeMs, int strengthBin) { - mHistoryCur.states2 = - setBitField(mHistoryCur.states2, strengthBin, - HistoryItem.STATE2_WIFI_SIGNAL_STRENGTH_SHIFT, - HistoryItem.STATE2_WIFI_SIGNAL_STRENGTH_MASK); - writeHistoryItem(elapsedRealtimeMs, uptimeMs); + synchronized (this) { + mHistoryCur.states2 = + setBitField(mHistoryCur.states2, strengthBin, + HistoryItem.STATE2_WIFI_SIGNAL_STRENGTH_SHIFT, + HistoryItem.STATE2_WIFI_SIGNAL_STRENGTH_MASK); + writeHistoryItem(elapsedRealtimeMs, uptimeMs); + } } /** @@ -1446,25 +1532,30 @@ public class BatteryStatsHistory { * Writes the current history item to history. */ public void writeHistoryItem(long elapsedRealtimeMs, long uptimeMs) { - if (mTrackRunningHistoryElapsedRealtimeMs != 0) { - final long diffElapsedMs = elapsedRealtimeMs - mTrackRunningHistoryElapsedRealtimeMs; - final long diffUptimeMs = uptimeMs - mTrackRunningHistoryUptimeMs; - if (diffUptimeMs < (diffElapsedMs - 20)) { - final long wakeElapsedTimeMs = elapsedRealtimeMs - (diffElapsedMs - diffUptimeMs); - mHistoryAddTmp.setTo(mHistoryLastWritten); - mHistoryAddTmp.wakelockTag = null; - mHistoryAddTmp.wakeReasonTag = null; - mHistoryAddTmp.eventCode = HistoryItem.EVENT_NONE; - mHistoryAddTmp.states &= ~HistoryItem.STATE_CPU_RUNNING_FLAG; - writeHistoryItem(wakeElapsedTimeMs, uptimeMs, mHistoryAddTmp); + synchronized (this) { + if (mTrackRunningHistoryElapsedRealtimeMs != 0) { + final long diffElapsedMs = + elapsedRealtimeMs - mTrackRunningHistoryElapsedRealtimeMs; + final long diffUptimeMs = uptimeMs - mTrackRunningHistoryUptimeMs; + if (diffUptimeMs < (diffElapsedMs - 20)) { + final long wakeElapsedTimeMs = + elapsedRealtimeMs - (diffElapsedMs - diffUptimeMs); + mHistoryAddTmp.setTo(mHistoryLastWritten); + mHistoryAddTmp.wakelockTag = null; + mHistoryAddTmp.wakeReasonTag = null; + mHistoryAddTmp.eventCode = HistoryItem.EVENT_NONE; + mHistoryAddTmp.states &= ~HistoryItem.STATE_CPU_RUNNING_FLAG; + writeHistoryItem(wakeElapsedTimeMs, uptimeMs, mHistoryAddTmp); + } } + mHistoryCur.states |= HistoryItem.STATE_CPU_RUNNING_FLAG; + mTrackRunningHistoryElapsedRealtimeMs = elapsedRealtimeMs; + mTrackRunningHistoryUptimeMs = uptimeMs; + writeHistoryItem(elapsedRealtimeMs, uptimeMs, mHistoryCur); } - mHistoryCur.states |= HistoryItem.STATE_CPU_RUNNING_FLAG; - mTrackRunningHistoryElapsedRealtimeMs = elapsedRealtimeMs; - mTrackRunningHistoryUptimeMs = uptimeMs; - writeHistoryItem(elapsedRealtimeMs, uptimeMs, mHistoryCur); } + @GuardedBy("this") private void writeHistoryItem(long elapsedRealtimeMs, long uptimeMs, HistoryItem cur) { if (mTracer != null && mTracer.tracingEnabled()) { recordTraceEvents(cur.eventCode, cur.eventTag); @@ -1591,6 +1682,7 @@ public class BatteryStatsHistory { writeHistoryItem(elapsedRealtimeMs, uptimeMs, cur, HistoryItem.CMD_UPDATE); } + @GuardedBy("this") private void writeHistoryItem(long elapsedRealtimeMs, @SuppressWarnings("UnusedVariable") long uptimeMs, HistoryItem cur, byte cmd) { if (!mMutable) { @@ -1701,7 +1793,8 @@ public class BatteryStatsHistory { /** * Writes the delta between the previous and current history items into history buffer. */ - public void writeHistoryDelta(Parcel dest, HistoryItem cur, HistoryItem last) { + @GuardedBy("this") + private void writeHistoryDelta(Parcel dest, HistoryItem cur, HistoryItem last) { if (last == null || cur.cmd != HistoryItem.CMD_UPDATE) { dest.writeInt(BatteryStatsHistory.DELTA_TIME_ABS); cur.writeToParcel(dest, 0); @@ -1921,6 +2014,7 @@ public class BatteryStatsHistory { * while writing the current history buffer, the method returns * <code>(index | TAG_FIRST_OCCURRENCE_FLAG)</code> */ + @GuardedBy("this") private int writeHistoryTag(HistoryTag tag) { if (tag.string == null) { Slog.wtfStack(TAG, "writeHistoryTag called with null name"); @@ -1964,33 +2058,37 @@ public class BatteryStatsHistory { * Don't allow any more batching in to the current history event. */ public void commitCurrentHistoryBatchLocked() { - mHistoryLastWritten.cmd = HistoryItem.CMD_NULL; + synchronized (this) { + mHistoryLastWritten.cmd = HistoryItem.CMD_NULL; + } } /** * Saves the accumulated history buffer in the active file, see {@link #getActiveFile()} . */ public void writeHistory() { - if (isReadOnly()) { - Slog.w(TAG, "writeHistory: this instance instance is read-only"); - return; - } + synchronized (this) { + if (isReadOnly()) { + Slog.w(TAG, "writeHistory: this instance instance is read-only"); + return; + } - // Save the monotonic time first, so that even if the history write below fails, - // we still wouldn't end up with overlapping history timelines. - mMonotonicClock.write(); + // Save the monotonic time first, so that even if the history write below fails, + // we still wouldn't end up with overlapping history timelines. + mMonotonicClock.write(); - Parcel p = Parcel.obtain(); - try { - final long start = SystemClock.uptimeMillis(); - writeHistoryBuffer(p); - if (DEBUG) { - Slog.d(TAG, "writeHistoryBuffer duration ms:" - + (SystemClock.uptimeMillis() - start) + " bytes:" + p.dataSize()); + Parcel p = Parcel.obtain(); + try { + final long start = SystemClock.uptimeMillis(); + writeHistoryBuffer(p); + if (DEBUG) { + Slog.d(TAG, "writeHistoryBuffer duration ms:" + + (SystemClock.uptimeMillis() - start) + " bytes:" + p.dataSize()); + } + writeParcelToFileLocked(p, mActiveFile); + } finally { + p.recycle(); } - writeParcelToFileLocked(p, mActiveFile); - } finally { - p.recycle(); } } @@ -1998,35 +2096,38 @@ public class BatteryStatsHistory { * Reads history buffer from a persisted Parcel. */ public void readHistoryBuffer(Parcel in) throws ParcelFormatException { - final int version = in.readInt(); - if (version != BatteryStatsHistory.VERSION) { - Slog.w("BatteryStats", "readHistoryBuffer: version got " + version - + ", expected " + BatteryStatsHistory.VERSION + "; erasing old stats"); - return; - } - - mHistoryBufferStartTime = in.readLong(); - mHistoryBuffer.setDataSize(0); - mHistoryBuffer.setDataPosition(0); + synchronized (this) { + final int version = in.readInt(); + if (version != BatteryStatsHistory.VERSION) { + Slog.w("BatteryStats", "readHistoryBuffer: version got " + version + + ", expected " + BatteryStatsHistory.VERSION + "; erasing old stats"); + return; + } - int bufSize = in.readInt(); - int curPos = in.dataPosition(); - if (bufSize >= (mMaxHistoryBufferSize * 100)) { - throw new ParcelFormatException( - "File corrupt: history data buffer too large " + bufSize); - } else if ((bufSize & ~3) != bufSize) { - throw new ParcelFormatException( - "File corrupt: history data buffer not aligned " + bufSize); - } else { - if (DEBUG) { - Slog.i(TAG, "***************** READING NEW HISTORY: " + bufSize - + " bytes at " + curPos); + mHistoryBufferStartTime = in.readLong(); + mHistoryBuffer.setDataSize(0); + mHistoryBuffer.setDataPosition(0); + + int bufSize = in.readInt(); + int curPos = in.dataPosition(); + if (bufSize >= (mMaxHistoryBufferSize * 100)) { + throw new ParcelFormatException( + "File corrupt: history data buffer too large " + bufSize); + } else if ((bufSize & ~3) != bufSize) { + throw new ParcelFormatException( + "File corrupt: history data buffer not aligned " + bufSize); + } else { + if (DEBUG) { + Slog.i(TAG, "***************** READING NEW HISTORY: " + bufSize + + " bytes at " + curPos); + } + mHistoryBuffer.appendFrom(in, curPos, bufSize); + in.setDataPosition(curPos + bufSize); } - mHistoryBuffer.appendFrom(in, curPos, bufSize); - in.setDataPosition(curPos + bufSize); } } + @GuardedBy("this") private void writeHistoryBuffer(Parcel out) { out.writeInt(BatteryStatsHistory.VERSION); out.writeLong(mHistoryBufferStartTime); @@ -2038,6 +2139,7 @@ public class BatteryStatsHistory { out.appendFrom(mHistoryBuffer, 0, mHistoryBuffer.dataSize()); } + @GuardedBy("this") private void writeParcelToFileLocked(Parcel p, AtomicFile file) { FileOutputStream fos = null; mWriteLock.lock(); @@ -2066,34 +2168,43 @@ public class BatteryStatsHistory { * Returns the total number of history tags in the tag pool. */ public int getHistoryStringPoolSize() { - return mHistoryTagPool.size(); + synchronized (this) { + return mHistoryTagPool.size(); + } } /** * Returns the total number of bytes occupied by the history tag pool. */ public int getHistoryStringPoolBytes() { - return mNumHistoryTagChars; + synchronized (this) { + return mNumHistoryTagChars; + } } /** * Returns the string held by the requested history tag. */ public String getHistoryTagPoolString(int index) { - ensureHistoryTagArray(); - HistoryTag historyTag = mHistoryTags.get(index); - return historyTag != null ? historyTag.string : null; + synchronized (this) { + ensureHistoryTagArray(); + HistoryTag historyTag = mHistoryTags.get(index); + return historyTag != null ? historyTag.string : null; + } } /** * Returns the UID held by the requested history tag. */ public int getHistoryTagPoolUid(int index) { - ensureHistoryTagArray(); - HistoryTag historyTag = mHistoryTags.get(index); - return historyTag != null ? historyTag.uid : Process.INVALID_UID; + synchronized (this) { + ensureHistoryTagArray(); + HistoryTag historyTag = mHistoryTags.get(index); + return historyTag != null ? historyTag.uid : Process.INVALID_UID; + } } + @GuardedBy("this") private void ensureHistoryTagArray() { if (mHistoryTags != null) { return; diff --git a/core/java/com/android/internal/statusbar/IStatusBar.aidl b/core/java/com/android/internal/statusbar/IStatusBar.aidl index 5351c6dbf94f..ed43b8190f93 100644 --- a/core/java/com/android/internal/statusbar/IStatusBar.aidl +++ b/core/java/com/android/internal/statusbar/IStatusBar.aidl @@ -151,8 +151,17 @@ oneway interface IStatusBar * @param hidesStatusBar whether it is being hidden */ void setTopAppHidesStatusBar(boolean hidesStatusBar); - + /** + * Add a tile to the Quick Settings Panel to the first item in the QS Panel + * @param tile the ComponentName of the {@link android.service.quicksettings.TileService} + */ void addQsTile(in ComponentName tile); + /** + * Add a tile to the Quick Settings Panel + * @param tile the ComponentName of the {@link android.service.quicksettings.TileService} + * @param end if true, the tile will be added at the end. If false, at the beginning. + */ + void addQsTileToFrontOrEnd(in ComponentName tile, boolean end); void remQsTile(in ComponentName tile); void setQsTiles(in String[] tiles); void clickQsTile(in ComponentName tile); diff --git a/core/proto/android/providers/settings/system.proto b/core/proto/android/providers/settings/system.proto index 48243f216015..5fc2a59e3028 100644 --- a/core/proto/android/providers/settings/system.proto +++ b/core/proto/android/providers/settings/system.proto @@ -207,6 +207,7 @@ message SystemSettingsProto { optional SettingProto pointer_speed = 2 [ (android.privacy).dest = DEST_AUTOMATIC ]; optional SettingProto right_click_zone = 3 [ (android.privacy).dest = DEST_AUTOMATIC ]; optional SettingProto tap_to_click = 4 [ (android.privacy).dest = DEST_AUTOMATIC ]; + optional SettingProto tap_dragging = 5 [ (android.privacy).dest = DEST_AUTOMATIC ]; } optional Touchpad touchpad = 36; diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index c4b5d8187845..0171f584a838 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -3751,6 +3751,13 @@ <permission android:name="android.permission.MANAGE_DEVICE_POLICY_DEVICE_IDENTIFIERS" android:protectionLevel="internal|role" /> + <!-- Allows an application to manage policy related to content protection. + <p>Protection level: internal|role + @FlaggedApi("android.view.contentprotection.flags.manage_device_policy_enabled") + --> + <permission android:name="android.permission.MANAGE_DEVICE_POLICY_CONTENT_PROTECTION" + android:protectionLevel="internal|role" /> + <!-- Allows an application to set device policies outside the current user that are critical for securing data within the current user. <p>Holding this permission allows the use of other held MANAGE_DEVICE_POLICY_* diff --git a/core/res/res/drawable-nodpi/platlogo.xml b/core/res/res/drawable-nodpi/platlogo.xml index fe12f6ee7836..f3acab063bbf 100644 --- a/core/res/res/drawable-nodpi/platlogo.xml +++ b/core/res/res/drawable-nodpi/platlogo.xml @@ -1,5 +1,5 @@ <!-- -Copyright (C) 2024 The Android Open Source Project +Copyright (C) 2021 The Android Open Source Project Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -20,103 +20,179 @@ Copyright (C) 2024 The Android Open Source Project android:viewportWidth="512" android:viewportHeight="512"> <path - android:pathData="M256.22,437.7c-26.38,0 -43.81,-18.3 -61.2,-48.42 -17.39,-30.12 -103.26,-178.86 -120.65,-208.98s-24.52,-54.37 -11.33,-77.21c13.19,-22.84 37.75,-28.79 72.53,-28.79 34.78,0 206.53,0 241.31,0 34.78,0 59.35,5.95 72.53,28.79 13.19,22.84 6.06,47.09 -11.33,77.21s-103.26,178.86 -120.65,208.98c-17.39,30.12 -34.83,48.42 -61.2,48.42Z" - android:strokeWidth="0"> + android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"> <aapt:attr name="android:fillColor"> <gradient - android:startX="56.22" - android:startY="256" - android:endX="456.22" - android:endY="256" + android:startX="256" + android:startY="21.81" + android:endX="256" + android:endY="350.42" android:type="linear"> <item android:offset="0" android:color="#FF073042"/> <item android:offset="1" android:color="#FF073042"/> </gradient> </aapt:attr> </path> + <group> + <clip-path + android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> + <path + android:pathData="m195.27,187.64l2.25,-6.69c13.91,78.13 50.84,284.39 50.84,50.33 0,-0.97 0.72,-1.81 1.62,-1.81h12.69c0.9,0 1.62,0.83 1.62,1.8 -0.2,409.91 -69.03,-43.64 -69.03,-43.64Z" + android:fillColor="#3ddc84"/> + </group> + <group> + <clip-path + android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> + <path + android:pathData="m158.77,180.68l-33.17,57.45c-1.9,3.3 -0.77,7.52 2.53,9.42 3.3,1.9 7.52,0.77 9.42,-2.53l33.59,-58.17c54.27,24.33 116.34,24.33 170.61,0l33.59,58.17c1.97,3.26 6.21,4.3 9.47,2.33 3.17,-1.91 4.26,-5.99 2.47,-9.23l-33.16,-57.45c56.95,-30.97 95.91,-88.64 101.61,-156.76H57.17c5.7,68.12 44.65,125.79 101.61,156.76Z" + android:fillColor="#fff"/> + </group> + <group> + <clip-path + android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> + <path + android:pathData="M250.26,187.66L262.17,187.66A2.13,2.13 0,0 1,264.3 189.78L264.3,217.85A2.13,2.13 0,0 1,262.17 219.98L250.26,219.98A2.13,2.13 0,0 1,248.14 217.85L248.14,189.78A2.13,2.13 0,0 1,250.26 187.66z" + android:fillColor="#3ddc84"/> + </group> + <group> + <clip-path + android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> + <path + android:pathData="M250.12,170.29L262.32,170.29A1.98,1.98 0,0 1,264.3 172.26L264.3,176.39A1.98,1.98 0,0 1,262.32 178.37L250.12,178.37A1.98,1.98 0,0 1,248.14 176.39L248.14,172.26A1.98,1.98 0,0 1,250.12 170.29z" + android:fillColor="#3ddc84"/> + </group> + <group> + <clip-path + android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> + <path + android:pathData="M171.92,216.82h4.04v4.04h-4.04z" + android:fillColor="#fff"/> + </group> + <group> + <clip-path + android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> + <path + android:pathData="M188.8,275.73h4.04v4.04h-4.04z" + android:fillColor="#fff"/> + </group> + <group> + <clip-path + android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> + <path + android:pathData="M369.04,337.63h4.04v4.04h-4.04z" + android:fillColor="#fff"/> + </group> + <group> + <clip-path + android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> + <path + android:pathData="M285.93,252.22h4.04v4.04h-4.04z" + android:fillColor="#fff"/> + </group> + <group> + <clip-path + android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> + <path + android:pathData="M318.96,218.84h4.04v4.04h-4.04z" + android:fillColor="#fff"/> + </group> + <group> + <clip-path + android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> + <path + android:pathData="M294.05,288.55h4.04v4.04h-4.04z" + android:fillColor="#fff"/> + </group> + <group> + <clip-path + android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> + <path + android:pathData="M330.82,273.31h8.08v8.08h-8.08z" + android:fillColor="#fff"/> + </group> + <group> + <clip-path + android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> + <path + android:pathData="M188.8,298.95h4.04v4.04h-4.04z" + android:fillColor="#fff"/> + </group> + <group> + <clip-path + android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> + <path + android:pathData="M220.14,238.94h8.08v8.08h-8.08z" + android:fillColor="#fff"/> + </group> + <group> + <clip-path + android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> + <path + android:pathData="M272.1,318.9h8.08v8.08h-8.08z" + android:fillColor="#fff"/> + </group> + <group> + <clip-path + android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> + <path + android:pathData="M293.34,349.25h8.08v8.08h-8.08z" + android:fillColor="#fff"/> + </group> + <group> + <clip-path + android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> + <path + android:pathData="M161.05,254.24h8.08v8.08h-8.08z" + android:fillColor="#fff"/> + </group> + <group> + <clip-path + android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> + <path + android:pathData="M378.92,192h8.08v8.08h-8.08z" + android:fillColor="#fff"/> + </group> + <group> + <clip-path + android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0"/> + <path + android:pathData="M137.87,323.7h8.08v8.08h-8.08z" + android:fillColor="#fff"/> + </group> + <path + android:pathData="M256,256m-200,0a200,200 0,1 1,400 0a200,200 0,1 1,-400 0" + android:strokeWidth="56.561" + android:fillColor="#00000000" + android:strokeColor="#f86734"/> <path - android:pathData="M198.85,168.57l2.03,-6.03c12.55,70.48 45.87,256.56 45.87,45.41 0,-0.88 0.65,-1.63 1.46,-1.63h11.45c0.81,0 1.46,0.75 1.46,1.63 -0.18,369.8 -62.28,-39.37 -62.28,-39.37Z" - android:strokeWidth="0" - android:fillColor="#3ddc84"/> - <path - android:pathData="M186.69,167.97l-23.69,41.03c-1.36,2.36 -0.55,5.37 1.8,6.73 2.36,1.36 5.37,0.55 6.73,-1.8l23.99,-41.55c38.76,17.38 83.1,17.38 121.86,0l23.99,41.55c1.41,2.33 4.44,3.07 6.77,1.66 2.26,-1.37 3.04,-4.28 1.76,-6.59l-23.69,-41.03c40.68,-22.12 68.5,-63.31 72.57,-111.97H114.11c4.07,48.65 31.89,89.85 72.57,111.97Z" - android:strokeWidth="0" - android:fillColor="#fff"/> - <path - android:pathData="M248.46,168.59L259.2,168.59A1.92,1.92 0,0 1,261.12 170.5L261.12,195.83A1.92,1.92 0,0 1,259.2 197.75L248.46,197.75A1.92,1.92 0,0 1,246.54 195.83L246.54,170.5A1.92,1.92 0,0 1,248.46 168.59z" - android:strokeWidth="0" - android:fillColor="#3ddc84"/> - <path - android:pathData="M248.32,152.92L259.34,152.92A1.78,1.78 0,0 1,261.12 154.7L261.12,158.43A1.78,1.78 0,0 1,259.34 160.21L248.32,160.21A1.78,1.78 0,0 1,246.54 158.43L246.54,154.7A1.78,1.78 0,0 1,248.32 152.92z" - android:strokeWidth="0" + android:pathData="m256.22,126.57c-6.69,0 -12.12,5.27 -12.12,11.77v14.52c0,1.25 1.02,2.27 2.27,2.27h0c1.25,0 2.27,-1.02 2.27,-2.27v-3.91c0,-2.51 2.04,-4.55 4.55,-4.55h6.06c2.51,0 4.55,2.04 4.55,4.55v3.91c0,1.25 1.02,2.27 2.27,2.27s2.27,-1.02 2.27,-2.27v-14.52c0,-6.5 -5.43,-11.77 -12.12,-11.77Z" android:fillColor="#3ddc84"/> <path - android:pathData="M159.03,176.91h4.04v4.04h-4.04z" - android:strokeWidth="0" - android:fillColor="#fff"/> - <path - android:pathData="M188.8,275.73h4.04v4.04h-4.04z" - android:strokeWidth="0" - android:fillColor="#fff"/> - <path - android:pathData="M373.41,158.93h4.04v4.04h-4.04z" - android:strokeWidth="0" - android:fillColor="#fff"/> - <path - android:pathData="M112.1,129.34h4.04v4.04h-4.04z" - android:strokeWidth="0" - android:fillColor="#fff"/> - <path - android:pathData="M285.93,252.22h4.04v4.04h-4.04z" - android:strokeWidth="0" - android:fillColor="#fff"/> - <path - android:pathData="M318.96,218.84h4.04v4.04h-4.04z" - android:strokeWidth="0" - android:fillColor="#fff"/> - <path - android:pathData="M294.05,288.55h4.04v4.04h-4.04z" - android:strokeWidth="0" + android:pathData="m93.34,116.36l3.85,-4.36 29.64,9.76 -4.44,5.03 -6.23,-2.1 -7.86,8.91 2.86,5.92 -4.43,5.03 -13.39,-28.18ZM110.43,122.76l-8.86,-3.02 4.11,8.41 4.76,-5.39Z" android:fillColor="#fff"/> <path - android:pathData="M330.82,263.31h8.08v8.08h-8.08z" - android:strokeWidth="0" + android:pathData="m153.62,100.85l-21.71,-6.2 10.38,14.38 -5.21,3.76 -16.78,-23.26 4.49,-3.24 21.65,6.19 -10.35,-14.35 5.24,-3.78 16.78,23.26 -4.49,3.24Z" android:fillColor="#fff"/> <path - android:pathData="M188.8,298.95h4.04v4.04h-4.04z" - android:strokeWidth="0" + android:pathData="m161.46,63.15l8.99,-3.84c7.43,-3.18 15.96,0.12 19.09,7.44 3.13,7.32 -0.38,15.76 -7.81,18.94l-8.99,3.84 -11.28,-26.38ZM179.41,80.26c4.46,-1.91 5.96,-6.72 4.15,-10.96 -1.81,-4.24 -6.33,-6.48 -10.79,-4.57l-3.08,1.32 6.64,15.53 3.08,-1.32Z" android:fillColor="#fff"/> <path - android:pathData="M224.18,216.82h8.08v8.08h-8.08z" - android:strokeWidth="0" + android:pathData="m204.23,47.57l11.1,-2.2c5.31,-1.05 9.47,2.08 10.4,6.76 0.72,3.65 -0.76,6.37 -4.07,8.34l12.4,10.44 -7.57,1.5 -11.65,-9.76 -1.03,0.2 2.3,11.61 -6.3,1.25 -5.57,-28.14ZM216.78,56.7c1.86,-0.37 3,-1.71 2.68,-3.33 -0.34,-1.7 -1.88,-2.43 -3.74,-2.06l-4.04,0.8 1.07,5.39 4.04,-0.8Z" android:fillColor="#fff"/> <path - android:pathData="M272.1,318.9h8.08v8.08h-8.08z" - android:strokeWidth="0" + android:pathData="m244.29,55.6c0.13,-8.16 6.86,-14.72 15.06,-14.58 8.16,0.13 14.72,6.9 14.58,15.06s-6.91,14.72 -15.06,14.58c-8.2,-0.13 -14.71,-6.9 -14.58,-15.06ZM267.44,55.98c0.08,-4.64 -3.54,-8.66 -8.18,-8.74 -4.68,-0.08 -8.42,3.82 -8.5,8.47 -0.08,4.65 3.54,8.66 8.22,8.74 4.64,0.08 8.39,-3.82 8.46,-8.47Z" android:fillColor="#fff"/> <path - android:pathData="M293.34,339.25h8.08v8.08h-8.08z" - android:strokeWidth="0" + android:pathData="m294.39,44.84l6.31,1.23 -5.49,28.16 -6.31,-1.23 5.49,-28.16Z" android:fillColor="#fff"/> <path - android:pathData="M165.09,236.28h8.08v8.08h-8.08z" - android:strokeWidth="0" + android:pathData="m321.94,51.41l9.14,3.48c7.55,2.88 11.39,11.17 8.56,18.61 -2.83,7.44 -11.22,11.07 -18.77,8.19l-9.14,-3.48 10.22,-26.8ZM322.96,76.19c4.53,1.73 8.95,-0.69 10.6,-5 1.64,-4.3 -0.05,-9.06 -4.58,-10.78l-3.13,-1.19 -6.01,15.78 3.13,1.19Z" android:fillColor="#fff"/> <path - android:pathData="M378.92,192h8.08v8.08h-8.08z" - android:strokeWidth="0" + android:pathData="m381.41,89.24l-4.21,-3.21 3.65,-4.78 9.06,6.91 -17.4,22.81 -4.85,-3.7 13.75,-18.02Z" android:fillColor="#fff"/> <path - android:pathData="M204.28,314.86h8.08v8.08h-8.08z" - android:strokeWidth="0" + android:pathData="m397.96,126.37l-9.56,-10.26 3.61,-3.36 22.8,-1.25 3.74,4.02 -12.35,11.51 2.51,2.69 -4.08,3.8 -2.51,-2.69 -4.55,4.24 -4.16,-4.46 4.55,-4.24ZM407.83,117.17l-10.28,0.58 4.49,4.82 5.79,-5.4Z" android:fillColor="#fff"/> - <path - android:pathData="M253.83,118.47c-6.04,0 -10.93,4.76 -10.93,10.62v13.1c0,1.13 0.92,2.05 2.05,2.05h0c1.13,0 2.05,-0.92 2.05,-2.05v-3.53c0,-2.27 1.84,-4.1 4.1,-4.1h5.47c2.27,0 4.1,1.84 4.1,4.1v3.53c0,1.13 0.92,2.05 2.05,2.05s2.05,-0.92 2.05,-2.05v-13.1c0,-5.86 -4.9,-10.62 -10.93,-10.62Z" - android:strokeWidth="0" - android:fillColor="#3ddc84"/> - <path - android:pathData="M256,437.7c-26.38,0 -43.81,-18.3 -61.2,-48.42 -17.39,-30.12 -103.26,-178.86 -120.65,-208.98 -17.39,-30.12 -24.52,-54.37 -11.33,-77.21 13.19,-22.84 37.75,-28.79 72.53,-28.79 34.78,0 206.53,0 241.31,0 34.78,0 59.35,5.95 72.53,28.79 13.19,22.84 6.06,47.09 -11.33,77.21 -17.39,30.12 -103.26,178.86 -120.65,208.98 -17.39,30.12 -34.83,48.42 -61.2,48.42Z" - android:strokeWidth="55" - android:fillColor="#00000000" - android:strokeColor="#f86733"/> </vector> + diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/DefaultRadioTunerTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/DefaultRadioTunerTest.java index 63de759282cf..ddf3615d24ed 100644 --- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/DefaultRadioTunerTest.java +++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/DefaultRadioTunerTest.java @@ -20,8 +20,6 @@ import static com.google.common.truth.Truth.assertWithMessage; import static org.junit.Assert.assertThrows; -import android.graphics.Bitmap; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -88,12 +86,6 @@ public final class DefaultRadioTunerTest { return 0; } - @Nullable - @Override - public Bitmap getMetadataImage(int id) { - return null; - } - @Override public boolean startBackgroundScan() { return false; @@ -138,6 +130,16 @@ public final class DefaultRadioTunerTest { } @Test + public void getMetadataImage_forRadioTuner_throwsException() { + UnsupportedOperationException thrown = assertThrows(UnsupportedOperationException.class, + () -> DEFAULT_RADIO_TUNER.getMetadataImage(/* id= */ 1)); + + assertWithMessage("Exception for getting metadata image from default radio tuner") + .that(thrown).hasMessageThat() + .contains("Getting metadata image must be implemented in child classes"); + } + + @Test public void getDynamicProgramList_forRadioTuner_returnsNull() { assertWithMessage("Dynamic program list obtained from default radio tuner") .that(DEFAULT_RADIO_TUNER.getDynamicProgramList(new ProgramList.Filter())).isNull(); diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioMetadataTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioMetadataTest.java index fddfd397d068..b3a0aba0d352 100644 --- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioMetadataTest.java +++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/RadioMetadataTest.java @@ -290,13 +290,29 @@ public final class RadioMetadataTest extends ExtendedRadioMockitoTestCase { } @Test - public void getBitmapId_withIllegalKey() { + public void getBitmapId_withIllegalKey_fails() { String illegalKey = RadioMetadata.METADATA_KEY_ARTIST; RadioMetadata metadata = mBuilder.putInt(RadioMetadata.METADATA_KEY_ART, INT_KEY_VALUE) .build(); - mExpect.withMessage("Bitmap id value with non-bitmap-id-type key %s", illegalKey) - .that(metadata.getBitmapId(illegalKey)).isEqualTo(0); + IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, () -> { + metadata.getBitmapId(illegalKey); + }); + + mExpect.withMessage("Exception for getting string array for non-bitmap-id type key %s", + illegalKey).that(thrown).hasMessageThat().contains("bitmap key"); + } + + @Test + public void getBitmapId_withNullKey_fails() { + RadioMetadata metadata = mBuilder.build(); + + NullPointerException thrown = assertThrows(NullPointerException.class, () -> { + metadata.getBitmapId(/* key= */ null); + }); + + mExpect.withMessage("Exception for getting bitmap identifier with null key") + .that(thrown).hasMessageThat().contains("can not be null"); } @Test diff --git a/core/tests/BroadcastRadioTests/src/android/hardware/radio/TunerAdapterTest.java b/core/tests/BroadcastRadioTests/src/android/hardware/radio/TunerAdapterTest.java index 4cda26de2906..5aace81696cf 100644 --- a/core/tests/BroadcastRadioTests/src/android/hardware/radio/TunerAdapterTest.java +++ b/core/tests/BroadcastRadioTests/src/android/hardware/radio/TunerAdapterTest.java @@ -514,6 +514,26 @@ public final class TunerAdapterTest { } @Test + public void getMetadataImage_withImageIdUnavailable_fails() throws Exception { + int nonExistImageId = 2; + when(mTunerMock.getImage(nonExistImageId)).thenReturn(null); + IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, + () -> mRadioTuner.getMetadataImage(nonExistImageId)); + + assertWithMessage("Exception for getting metadata image with non-existing id") + .that(thrown).hasMessageThat().contains("is not available"); + } + + @Test + public void getMetadataImage_withInvalidId_fails() { + IllegalArgumentException thrown = assertThrows(IllegalArgumentException.class, + () -> mRadioTuner.getMetadataImage(/* id= */ 0)); + + assertWithMessage("Exception for getting metadata image for id 0").that(thrown) + .hasMessageThat().contains("Invalid metadata image id 0"); + } + + @Test public void getMetadataImage_whenServiceDied_fails() throws Exception { when(mTunerMock.getImage(anyInt())).thenThrow(new RemoteException()); diff --git a/core/tests/coretests/src/android/app/backup/BackupAgentTest.java b/core/tests/coretests/src/android/app/backup/BackupAgentTest.java index 9e8e2d6a2446..cd5deb6c4293 100644 --- a/core/tests/coretests/src/android/app/backup/BackupAgentTest.java +++ b/core/tests/coretests/src/android/app/backup/BackupAgentTest.java @@ -20,24 +20,33 @@ import static com.google.common.truth.Truth.assertThat; import static org.mockito.Mockito.when; +import android.app.IBackupAgent; import android.app.backup.BackupAgent.IncludeExcludeRules; import android.app.backup.BackupAnnotations.BackupDestination; import android.app.backup.BackupAnnotations.OperationType; import android.app.backup.FullBackup.BackupScheme.PathWithRequiredFlags; +import android.content.Context; import android.os.ParcelFileDescriptor; import android.os.UserHandle; import android.platform.test.annotations.Presubmit; +import android.platform.test.flag.junit.SetFlagsRule; import android.util.ArraySet; import androidx.test.runner.AndroidJUnit4; +import com.android.server.backup.Flags; + import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.Map; import java.util.Set; @@ -49,9 +58,12 @@ public class BackupAgentTest { private static final UserHandle USER_HANDLE = new UserHandle(15); private static final String DATA_TYPE_BACKED_UP = "test data type"; + @Mock IBackupManager mIBackupManager; @Mock FullBackup.BackupScheme mBackupScheme; + @Mock Context mContext; - private BackupAgent mBackupAgent; + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @Before public void setUp() { @@ -67,11 +79,11 @@ public class BackupAgentTest { excludePaths.add(path); IncludeExcludeRules expectedRules = new IncludeExcludeRules(includePaths, excludePaths); - mBackupAgent = getAgentForBackupDestination(BackupDestination.CLOUD); + BackupAgent backupAgent = getAgentForBackupDestination(BackupDestination.CLOUD); when(mBackupScheme.maybeParseAndGetCanonicalExcludePaths()).thenReturn(excludePaths); when(mBackupScheme.maybeParseAndGetCanonicalIncludePaths()).thenReturn(includePaths); - IncludeExcludeRules rules = mBackupAgent.getIncludeExcludeRules(mBackupScheme); + IncludeExcludeRules rules = backupAgent.getIncludeExcludeRules(mBackupScheme); assertThat(rules).isEqualTo(expectedRules); } @@ -137,6 +149,31 @@ public class BackupAgentTest { 0).getSuccessCount()).isEqualTo(1); } + @Test + public void doRestoreFile_agentOverrideIgnoresFile_consumesAllBytesInBuffer() throws Exception { + mSetFlagsRule.enableFlags(Flags.FLAG_ENABLE_CLEAR_PIPE_AFTER_RESTORE_FILE); + BackupAgent agent = new TestRestoreIgnoringFullBackupAgent(); + agent.attach(mContext); + agent.onCreate(USER_HANDLE, BackupDestination.CLOUD, OperationType.RESTORE); + IBackupAgent agentBinder = (IBackupAgent) agent.onBind(); + + ParcelFileDescriptor[] pipes = ParcelFileDescriptor.createPipe(); + FileOutputStream writeSide = new FileOutputStream( + pipes[1].getFileDescriptor()); + writeSide.write("Hello".getBytes(StandardCharsets.UTF_8)); + + agentBinder.doRestoreFile(pipes[0], /* length= */ 5, BackupAgent.TYPE_FILE, + FullBackup.FILES_TREE_TOKEN, /* path= */ "hello_file", /* mode= */ + 0666, /* mtime= */ 12345, /* token= */ 6789, mIBackupManager); + + try (FileInputStream in = new FileInputStream(pipes[0].getFileDescriptor())) { + assertThat(in.available()).isEqualTo(0); + } finally { + pipes[0].close(); + pipes[1].close(); + } + } + private BackupAgent getAgentForBackupDestination(@BackupDestination int backupDestination) { BackupAgent agent = new TestFullBackupAgent(); agent.onCreate(USER_HANDLE, backupDestination); @@ -144,7 +181,6 @@ public class BackupAgentTest { } private static class TestFullBackupAgent extends BackupAgent { - @Override public void onBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState) throws IOException { @@ -162,4 +198,14 @@ public class BackupAgentTest { // Left empty as this is a full backup agent. } } + + private static class TestRestoreIgnoringFullBackupAgent extends TestFullBackupAgent { + + @Override + protected void onRestoreFile(ParcelFileDescriptor data, long size, + int type, String domain, String path, long mode, long mtime) + throws IOException { + // Ignore the file and don't consume any data. + } + } } diff --git a/core/tests/coretests/src/android/os/BundleTest.java b/core/tests/coretests/src/android/os/BundleTest.java index e7b5dff60110..93c2e0e40593 100644 --- a/core/tests/coretests/src/android/os/BundleTest.java +++ b/core/tests/coretests/src/android/os/BundleTest.java @@ -20,6 +20,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThrows; import static org.junit.Assert.assertTrue; @@ -121,6 +122,14 @@ public class BundleTest { } @Test + public void testEmpty() throws Exception { + assertNotNull(Bundle.EMPTY); + assertEquals(0, Bundle.EMPTY.size()); + + new Bundle(Bundle.EMPTY); + } + + @Test @IgnoreUnderRavenwood(blockedBy = ParcelFileDescriptor.class) public void testCreateFromParcel() throws Exception { boolean withFd; diff --git a/core/tests/coretests/src/android/os/TestLooperManagerTest.java b/core/tests/coretests/src/android/os/TestLooperManagerTest.java new file mode 100644 index 000000000000..5959444e49cc --- /dev/null +++ b/core/tests/coretests/src/android/os/TestLooperManagerTest.java @@ -0,0 +1,91 @@ +/* + * 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 android.os; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import android.platform.test.ravenwood.RavenwoodRule; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +@RunWith(AndroidJUnit4.class) +public class TestLooperManagerTest { + private static final String TAG = "TestLooperManagerTest"; + + @Rule + public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder() + .setProvideMainThread(true) + .build(); + + @Test + public void testMainThread() throws Exception { + doTest(Looper.getMainLooper()); + } + + @Test + public void testCustomThread() throws Exception { + final HandlerThread thread = new HandlerThread(TAG); + thread.start(); + doTest(thread.getLooper()); + } + + private void doTest(Looper looper) throws Exception { + final TestLooperManager tlm = + InstrumentationRegistry.getInstrumentation().acquireLooperManager(looper); + + final Handler handler = new Handler(looper); + final CountDownLatch latch = new CountDownLatch(1); + + assertFalse(tlm.hasMessages(handler, null, 42)); + + handler.sendEmptyMessage(42); + handler.post(() -> { + latch.countDown(); + }); + assertTrue(tlm.hasMessages(handler, null, 42)); + assertFalse(latch.await(100, TimeUnit.MILLISECONDS)); + + final Message first = tlm.next(); + assertEquals(42, first.what); + assertNull(first.callback); + tlm.execute(first); + assertFalse(tlm.hasMessages(handler, null, 42)); + assertFalse(latch.await(100, TimeUnit.MILLISECONDS)); + tlm.recycle(first); + + final Message second = tlm.next(); + assertNotNull(second.callback); + tlm.execute(second); + assertFalse(tlm.hasMessages(handler, null, 42)); + assertTrue(latch.await(100, TimeUnit.MILLISECONDS)); + tlm.recycle(second); + + tlm.release(); + } +} diff --git a/graphics/java/android/graphics/text/LineBreakConfig.java b/graphics/java/android/graphics/text/LineBreakConfig.java index b21bf11088e2..7d55928aa656 100644 --- a/graphics/java/android/graphics/text/LineBreakConfig.java +++ b/graphics/java/android/graphics/text/LineBreakConfig.java @@ -176,6 +176,9 @@ public final class LineBreakConfig implements Parcelable { * - If at least one locale in the locale list contains Japanese script, this option is * equivalent to {@link #LINE_BREAK_STYLE_STRICT}. * - Otherwise, this option is equivalent to {@link #LINE_BREAK_STYLE_NONE}. + * + * <p> + * Note: future versions may have special line breaking style rules for other locales. */ @FlaggedApi(FLAG_WORD_STYLE_AUTO) public static final int LINE_BREAK_STYLE_AUTO = 5; @@ -249,6 +252,9 @@ public final class LineBreakConfig implements Parcelable { * option is equivalent to {@link #LINE_BREAK_WORD_STYLE_PHRASE} if the result of its line * count is less than 5 lines. * - Otherwise, this option is equivalent to {@link #LINE_BREAK_WORD_STYLE_NONE}. + * + * <p> + * Note: future versions may have special line breaking word style rules for other locales. */ @FlaggedApi(FLAG_WORD_STYLE_AUTO) public static final int LINE_BREAK_WORD_STYLE_AUTO = 2; diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig index 0e046581cb48..1cc8a8f5061b 100644 --- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig +++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig @@ -30,13 +30,6 @@ flag { } flag { - name: "enable_pip_ui_state_on_entering" - namespace: "multitasking" - description: "Enables PiP UI state callback on entering" - bug: "303718131" -} - -flag { name: "enable_pip2_implementation" namespace: "multitasking" description: "Enables the new implementation of PiP (PiP2)" diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewTest.kt index 4c76168cdeaa..398fd554f030 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewTest.kt @@ -22,7 +22,6 @@ import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest 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 @@ -30,6 +29,8 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify @SmallTest @RunWith(AndroidJUnit4::class) @@ -37,10 +38,11 @@ class BubbleTaskViewTest { private lateinit var bubbleTaskView: BubbleTaskView private val context = ApplicationProvider.getApplicationContext<Context>() + private lateinit var taskView: TaskView @Before fun setUp() { - val taskView = TaskView(context, mock<TaskViewTaskController>()) + taskView = mock() bubbleTaskView = BubbleTaskView(taskView, directExecutor()) } @@ -72,4 +74,19 @@ class BubbleTaskViewTest { assertThat(actualTaskId).isEqualTo(123) assertThat(actualComponentName).isEqualTo(componentName) } + + @Test + fun cleanup_invalidTaskId_doesNotRemoveTask() { + bubbleTaskView.cleanup() + verify(taskView, never()).removeTask() + } + + @Test + fun cleanup_validTaskId_removesTask() { + val componentName = ComponentName(context, "TestClass") + bubbleTaskView.listener.onTaskCreated(123, componentName) + + bubbleTaskView.cleanup() + verify(taskView).removeTask() + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java index 87c8f526c6fa..f32f030ff06e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java @@ -51,6 +51,8 @@ import com.android.launcher3.icons.BubbleIconFactory; import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView; import com.android.wm.shell.bubbles.bar.BubbleBarLayerView; import com.android.wm.shell.common.bubbles.BubbleInfo; +import com.android.wm.shell.taskview.TaskView; +import com.android.wm.shell.taskview.TaskViewTaskController; import java.io.PrintWriter; import java.util.List; @@ -105,6 +107,8 @@ public class Bubble implements BubbleViewProvider { private BubbleExpandedView mExpandedView; @Nullable private BubbleBarExpandedView mBubbleBarExpandedView; + @Nullable + private BubbleTaskView mBubbleTaskView; private BubbleViewInfoTask mInflationTask; private boolean mInflateSynchronously; @@ -394,6 +398,21 @@ public class Bubble implements BubbleViewProvider { } /** + * Returns the existing {@link #mBubbleTaskView} if it's not {@code null}. Otherwise a new + * instance of {@link BubbleTaskView} is created. + */ + public BubbleTaskView getOrCreateBubbleTaskView(Context context, BubbleController controller) { + if (mBubbleTaskView == null) { + TaskViewTaskController taskViewTaskController = new TaskViewTaskController(context, + controller.getTaskOrganizer(), + controller.getTaskViewTransitions(), controller.getSyncTransactionQueue()); + TaskView taskView = new TaskView(context, taskViewTaskController); + mBubbleTaskView = new BubbleTaskView(taskView, controller.getMainExecutor()); + } + return mBubbleTaskView; + } + + /** * @return the ShortcutInfo id if it exists, or the metadata shortcut id otherwise. */ String getShortcutId() { @@ -415,6 +434,10 @@ public class Bubble implements BubbleViewProvider { * the bubble. */ void cleanupExpandedView() { + cleanupExpandedView(true); + } + + private void cleanupExpandedView(boolean cleanupTaskView) { if (mExpandedView != null) { mExpandedView.cleanUpExpandedState(); mExpandedView = null; @@ -423,17 +446,37 @@ public class Bubble implements BubbleViewProvider { mBubbleBarExpandedView.cleanUpExpandedState(); mBubbleBarExpandedView = null; } + if (cleanupTaskView) { + cleanupTaskView(); + } if (mIntent != null) { mIntent.unregisterCancelListener(mIntentCancelListener); } mIntentActive = false; } + private void cleanupTaskView() { + if (mBubbleTaskView != null) { + mBubbleTaskView.cleanup(); + mBubbleTaskView = null; + } + } + /** * Call when all the views should be removed/cleaned up. */ void cleanupViews() { - cleanupExpandedView(); + cleanupViews(true); + } + + /** + * Call when all the views should be removed/cleaned up. + * + * <p>If we're switching between bar and floating modes, pass {@code false} on + * {@code cleanupTaskView} to avoid recreating it in the new mode. + */ + void cleanupViews(boolean cleanupTaskView) { + cleanupExpandedView(cleanupTaskView); mIconView = null; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index a5f7880cfd41..1a6bf2849410 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -173,7 +173,7 @@ public class BubbleController implements ConfigurationChangeListener, private final Context mContext; private final BubblesImpl mImpl = new BubblesImpl(); private Bubbles.BubbleExpandListener mExpandListener; - @Nullable private BubbleStackView.SurfaceSynchronizer mSurfaceSynchronizer; + @Nullable private final BubbleStackView.SurfaceSynchronizer mSurfaceSynchronizer; private final FloatingContentCoordinator mFloatingContentCoordinator; private final BubbleDataRepository mDataRepository; private final WindowManagerShellWrapper mWindowManagerShellWrapper; @@ -197,12 +197,12 @@ public class BubbleController implements ConfigurationChangeListener, private final Handler mMainHandler; private final ShellExecutor mBackgroundExecutor; - private BubbleLogger mLogger; - private BubbleData mBubbleData; + private final BubbleLogger mLogger; + private final BubbleData mBubbleData; @Nullable private BubbleStackView mStackView; @Nullable private BubbleBarLayerView mLayerView; private BubbleIconFactory mBubbleIconFactory; - private BubblePositioner mBubblePositioner; + private final BubblePositioner mBubblePositioner; private Bubbles.SysuiProxy mSysuiProxy; // Tracks the id of the current (foreground) user. @@ -232,13 +232,17 @@ public class BubbleController implements ConfigurationChangeListener, /** Whether or not the BubbleStackView has been added to the WindowManager. */ private boolean mAddedToWindowManager = false; - /** Saved screen density, used to detect display size changes in {@link #onConfigChanged}. */ + /** + * Saved screen density, used to detect display size changes in {@link #onConfigurationChanged}. + */ private int mDensityDpi = Configuration.DENSITY_DPI_UNDEFINED; - /** Saved screen bounds, used to detect screen size changes in {@link #onConfigChanged}. **/ - private Rect mScreenBounds = new Rect(); + /** + * Saved screen bounds, used to detect screen size changes in {@link #onConfigurationChanged}. + */ + private final Rect mScreenBounds = new Rect(); - /** Saved font scale, used to detect font size changes in {@link #onConfigChanged}. */ + /** Saved font scale, used to detect font size changes in {@link #onConfigurationChanged}. */ private float mFontScale = 0; /** Saved direction, used to detect layout direction changes @link #onConfigChanged}. */ @@ -253,9 +257,9 @@ public class BubbleController implements ConfigurationChangeListener, private boolean mIsStatusBarShade = true; /** One handed mode controller to register transition listener. */ - private Optional<OneHandedController> mOneHandedOptional; + private final Optional<OneHandedController> mOneHandedOptional; /** Drag and drop controller to register listener for onDragStarted. */ - private Optional<DragAndDropController> mDragAndDropController; + private final Optional<DragAndDropController> mDragAndDropController; /** Used to send bubble events to launcher. */ private Bubbles.BubbleStateListener mBubbleStateListener; @@ -731,9 +735,11 @@ public class BubbleController implements ConfigurationChangeListener, } } else { if (mStackView == null) { + BubbleStackViewManager bubbleStackViewManager = + BubbleStackViewManager.fromBubbleController(this); mStackView = new BubbleStackView( - mContext, this, mBubbleData, mSurfaceSynchronizer, - mFloatingContentCoordinator, this, mMainExecutor); + mContext, bubbleStackViewManager, mBubblePositioner, mBubbleData, + mSurfaceSynchronizer, mFloatingContentCoordinator, this, mMainExecutor); mStackView.onOrientationChanged(); if (mExpandListener != null) { mStackView.setExpandListener(mExpandListener); @@ -893,7 +899,6 @@ public class BubbleController implements ConfigurationChangeListener, * Called by the BubbleStackView and whenever all bubbles have animated out, and none have been * added in the meantime. */ - @VisibleForTesting public void onAllBubblesAnimatedOut() { if (mStackView != null) { mStackView.setVisibility(INVISIBLE); @@ -1047,7 +1052,6 @@ public class BubbleController implements ConfigurationChangeListener, return mBubbleData.hasBubbles() || mBubbleData.isShowingOverflow(); } - @VisibleForTesting public boolean isStackExpanded() { return mBubbleData.isExpanded(); } @@ -1105,9 +1109,8 @@ public class BubbleController implements ConfigurationChangeListener, * <p>This is used by external callers (launcher). */ @VisibleForTesting - public void expandStackAndSelectBubbleFromLauncher(String key, int bubbleBarOffsetX, - int bubbleBarOffsetY) { - mBubblePositioner.setBubbleBarPosition(bubbleBarOffsetX, bubbleBarOffsetY); + public void expandStackAndSelectBubbleFromLauncher(String key, Rect bubbleBarBounds) { + mBubblePositioner.setBubbleBarPosition(bubbleBarBounds); if (BubbleOverflow.KEY.equals(key)) { mBubbleData.setSelectedBubbleFromLauncher(mBubbleData.getOverflow()); @@ -1366,8 +1369,9 @@ public class BubbleController implements ConfigurationChangeListener, mStackView.resetOverflowView(); mStackView.removeAllViews(); } - // cleanup existing bubble views so they can be recreated later if needed. - mBubbleData.getBubbles().forEach(Bubble::cleanupViews); + // cleanup existing bubble views so they can be recreated later if needed, but retain + // TaskView. + mBubbleData.getBubbles().forEach(b -> b.cleanupViews(/* cleanupTaskView= */ false)); // remove the current bubble container from window manager, null it out, and create a new // container based on the current mode. @@ -1478,7 +1482,6 @@ public class BubbleController implements ConfigurationChangeListener, * <p> * Must be called from the main thread. */ - @VisibleForTesting @MainThread public void removeBubble(String key, int reason) { if (mBubbleData.hasAnyBubbleWithKey(key)) { @@ -1486,36 +1489,6 @@ public class BubbleController implements ConfigurationChangeListener, } } - // TODO(b/316358859): remove this method after task views are shared across modes - /** - * Removes the bubble with the given key after task removal, unless the task was removed as - * a result of mode switching, in which case, the bubble isn't removed because it will be - * re-inflated for the new mode. - */ - @MainThread - public void removeFloatingBubbleAfterTaskRemoval(String key, int reason) { - // if we're floating remove the bubble. otherwise, we're here because the task was removed - // after switching modes. See b/316358859 - if (!isShowingAsBubbleBar()) { - removeBubble(key, reason); - } - } - - // TODO(b/316358859): remove this method after task views are shared across modes - /** - * Removes the bubble with the given key after task removal, unless the task was removed as - * a result of mode switching, in which case, the bubble isn't removed because it will be - * re-inflated for the new mode. - */ - @MainThread - public void removeBarBubbleAfterTaskRemoval(String key, int reason) { - // if we're showing as bubble bar remove the bubble. otherwise, we're here because the task - // was removed after switching modes. See b/316358859 - if (isShowingAsBubbleBar()) { - removeBubble(key, reason); - } - } - /** * Removes all the bubbles. * <p> @@ -2198,10 +2171,10 @@ public class BubbleController implements ConfigurationChangeListener, } @Override - public void showBubble(String key, int bubbleBarOffsetX, int bubbleBarOffsetY) { + public void showBubble(String key, Rect bubbleBarBounds) { mMainExecutor.execute( () -> mController.expandStackAndSelectBubbleFromLauncher( - key, bubbleBarOffsetX, bubbleBarOffsetY)); + key, bubbleBarBounds)); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java index 9f7d0ac9bafe..efc4d8b95f4f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java @@ -27,12 +27,10 @@ import static com.android.wm.shell.bubbles.BubbleDebugConfig.DEBUG_BUBBLE_EXPAND import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES; import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.wm.shell.bubbles.BubblePositioner.MAX_HEIGHT; -import static com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS; import android.annotation.NonNull; import android.annotation.SuppressLint; import android.app.ActivityOptions; -import android.app.ActivityTaskManager; import android.app.PendingIntent; import android.content.ComponentName; import android.content.Context; @@ -49,7 +47,6 @@ import android.graphics.PointF; import android.graphics.PorterDuff; import android.graphics.Rect; import android.graphics.drawable.ShapeDrawable; -import android.os.RemoteException; import android.util.AttributeSet; import android.util.FloatProperty; import android.util.IntProperty; @@ -311,8 +308,7 @@ public class BubbleExpandedView extends LinearLayout { + " bubble=" + getBubbleKey()); } if (mBubble != null) { - mController.removeFloatingBubbleAfterTaskRemoval( - mBubble.getKey(), Bubbles.DISMISS_TASK_FINISHED); + mController.removeBubble(mBubble.getKey(), Bubbles.DISMISS_TASK_FINISHED); } if (mTaskView != null) { // Release the surface @@ -1105,32 +1101,11 @@ public class BubbleExpandedView extends LinearLayout { return ((LinearLayout.LayoutParams) mManageButton.getLayoutParams()).getMarginStart(); } - /** - * Cleans up anything related to the task. The TaskView itself is released after the task - * has been removed. - * - * If this view should be reused after this method is called, then - * {@link #initialize(BubbleController, BubbleStackView, boolean, BubbleTaskView)} - * must be invoked first. - */ + /** Hide the task view. */ public void cleanUpExpandedState() { if (DEBUG_BUBBLE_EXPANDED_VIEW) { Log.d(TAG, "cleanUpExpandedState: bubble=" + getBubbleKey() + " task=" + mTaskId); } - if (getTaskId() != INVALID_TASK_ID) { - // Ensure the task is removed from WM - if (ENABLE_SHELL_TRANSITIONS) { - if (mTaskView != null) { - mTaskView.removeTask(); - } - } else { - try { - ActivityTaskManager.getService().removeTask(getTaskId()); - } catch (RemoteException e) { - Log.w(TAG, e.getMessage()); - } - } - } if (mTaskView != null) { mTaskView.setVisibility(GONE); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java index a76bd269f31b..d62c86ce386f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java @@ -19,7 +19,6 @@ package com.android.wm.shell.bubbles; import android.content.Context; import android.content.res.Resources; import android.graphics.Insets; -import android.graphics.Point; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.RectF; @@ -97,7 +96,7 @@ public class BubblePositioner { private PointF mRestingStackPosition; private boolean mShowingInBubbleBar; - private final Point mBubbleBarPosition = new Point(); + private final Rect mBubbleBarBounds = new Rect(); public BubblePositioner(Context context, WindowManager windowManager) { mContext = context; @@ -791,15 +790,10 @@ public class BubblePositioner { } /** - * Sets the position of the bubble bar in screen coordinates. - * - * @param offsetX the offset of the bubble bar from the edge of the screen on the X axis - * @param offsetY the offset of the bubble bar from the edge of the screen on the Y axis + * Sets the position of the bubble bar in display coordinates. */ - public void setBubbleBarPosition(int offsetX, int offsetY) { - mBubbleBarPosition.set( - getAvailableRect().width() - offsetX, - getAvailableRect().height() + mInsets.top + mInsets.bottom - offsetY); + public void setBubbleBarPosition(Rect bubbleBarBounds) { + mBubbleBarBounds.set(bubbleBarBounds); } /** @@ -820,7 +814,7 @@ public class BubblePositioner { /** The bottom position of the expanded view when showing above the bubble bar. */ public int getExpandedViewBottomForBubbleBar() { - return mBubbleBarPosition.y - mExpandedViewPadding; + return mBubbleBarBounds.top - mExpandedViewPadding; } /** @@ -831,9 +825,9 @@ public class BubblePositioner { } /** - * Returns the on screen co-ordinates of the bubble bar. + * Returns the display coordinates of the bubble bar. */ - public Point getBubbleBarPosition() { - return mBubbleBarPosition; + public Rect getBubbleBarBounds() { + return mBubbleBarBounds; } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java index a619401301aa..9facef36a74e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java @@ -204,7 +204,7 @@ public class BubbleStackView extends FrameLayout Choreographer.getInstance().postFrameCallback(frameCallback); } }; - private final BubbleController mBubbleController; + private final BubbleStackViewManager mManager; private final BubbleData mBubbleData; private final Bubbles.SysuiProxy.Provider mSysuiProxyProvider; private StackViewState mStackViewState = new StackViewState(); @@ -858,6 +858,7 @@ public class BubbleStackView extends FrameLayout private BubbleOverflow mBubbleOverflow; private StackEducationView mStackEduView; + private StackEducationView.Manager mStackEducationViewManager; private ManageEducationView mManageEduView; private DismissView mDismissView; @@ -873,15 +874,16 @@ public class BubbleStackView extends FrameLayout private BubblePositioner mPositioner; @SuppressLint("ClickableViewAccessibility") - public BubbleStackView(Context context, BubbleController bubbleController, - BubbleData data, @Nullable SurfaceSynchronizer synchronizer, + public BubbleStackView(Context context, BubbleStackViewManager bubbleStackViewManager, + BubblePositioner bubblePositioner, BubbleData data, + @Nullable SurfaceSynchronizer synchronizer, FloatingContentCoordinator floatingContentCoordinator, Bubbles.SysuiProxy.Provider sysuiProxyProvider, ShellExecutor mainExecutor) { super(context); mMainExecutor = mainExecutor; - mBubbleController = bubbleController; + mManager = bubbleStackViewManager; mBubbleData = data; mSysuiProxyProvider = sysuiProxyProvider; @@ -893,7 +895,7 @@ public class BubbleStackView extends FrameLayout mExpandedViewPadding = res.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding); int elevation = res.getDimensionPixelSize(R.dimen.bubble_elevation); - mPositioner = mBubbleController.getPositioner(); + mPositioner = bubblePositioner; final TypedArray ta = mContext.obtainStyledAttributes( new int[]{android.R.attr.dialogCornerRadius}); @@ -903,7 +905,7 @@ public class BubbleStackView extends FrameLayout final Runnable onBubbleAnimatedOut = () -> { if (getBubbleCount() == 0) { mExpandedViewTemporarilyHidden = false; - mBubbleController.onAllBubblesAnimatedOut(); + mManager.onAllBubblesAnimatedOut(); } }; mStackAnimationController = new StackAnimationController( @@ -1383,7 +1385,9 @@ public class BubbleStackView extends FrameLayout return false; } if (mStackEduView == null) { - mStackEduView = new StackEducationView(mContext, mPositioner, mBubbleController); + mStackEducationViewManager = mManager::updateWindowFlagsForBackpress; + mStackEduView = + new StackEducationView(mContext, mPositioner, mStackEducationViewManager); addView(mStackEduView); } return showStackEdu(); @@ -1412,7 +1416,9 @@ public class BubbleStackView extends FrameLayout private void updateUserEdu() { if (isStackEduVisible() && !mStackEduView.isHiding()) { removeView(mStackEduView); - mStackEduView = new StackEducationView(mContext, mPositioner, mBubbleController); + mStackEducationViewManager = mManager::updateWindowFlagsForBackpress; + mStackEduView = + new StackEducationView(mContext, mPositioner, mStackEducationViewManager); addView(mStackEduView); showStackEdu(); } @@ -2106,7 +2112,7 @@ public class BubbleStackView extends FrameLayout logBubbleEvent(mExpandedBubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__EXPANDED); logBubbleEvent(mExpandedBubble, FrameworkStatsLog.BUBBLE_UICHANGED__ACTION__STACK_EXPANDED); - mBubbleController.isNotificationPanelExpanded(notifPanelExpanded -> { + mManager.checkNotificationPanelExpandedState(notifPanelExpanded -> { if (!notifPanelExpanded && mIsExpanded) { startMonitoringSwipeUpGesture(); } @@ -2227,7 +2233,7 @@ public class BubbleStackView extends FrameLayout */ void hideCurrentInputMethod() { mPositioner.setImeVisible(false, 0); - mBubbleController.hideCurrentInputMethod(); + mManager.hideCurrentInputMethod(); } /** Set the stack position to whatever the positioner says. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackViewManager.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackViewManager.kt new file mode 100644 index 000000000000..fb597a05663b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackViewManager.kt @@ -0,0 +1,60 @@ +/* + * 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.wm.shell.bubbles + +import java.util.function.Consumer + +/** Defines callbacks from [BubbleStackView] to its manager. */ +interface BubbleStackViewManager { + + /** Notifies that all bubbles animated out. */ + fun onAllBubblesAnimatedOut() + + /** Notifies whether backpress should be intercepted. */ + fun updateWindowFlagsForBackpress(interceptBack: Boolean) + + /** + * Checks the current expansion state of the notification panel, and invokes [callback] with the + * result. + */ + fun checkNotificationPanelExpandedState(callback: Consumer<Boolean>) + + /** Requests to hide the current input method. */ + fun hideCurrentInputMethod() + + companion object { + + @JvmStatic + fun fromBubbleController(controller: BubbleController) = object : BubbleStackViewManager { + override fun onAllBubblesAnimatedOut() { + controller.onAllBubblesAnimatedOut() + } + + override fun updateWindowFlagsForBackpress(interceptBack: Boolean) { + controller.updateWindowFlagsForBackpress(interceptBack) + } + + override fun checkNotificationPanelExpandedState(callback: Consumer<Boolean>) { + controller.isNotificationPanelExpanded(callback) + } + + override fun hideCurrentInputMethod() { + controller.hideCurrentInputMethod() + } + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskView.kt index 2fcd133c7b20..65f8e48eb822 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskView.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskView.kt @@ -16,10 +16,14 @@ package com.android.wm.shell.bubbles +import android.app.ActivityTaskManager import android.app.ActivityTaskManager.INVALID_TASK_ID import android.content.ComponentName +import android.os.RemoteException +import android.util.Log import androidx.annotation.VisibleForTesting import com.android.wm.shell.taskview.TaskView +import com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS import java.util.concurrent.Executor /** @@ -78,4 +82,28 @@ class BubbleTaskView(val taskView: TaskView, executor: Executor) { init { taskView.setListener(executor, listener) } + + /** + * Removes the [TaskView] from window manager. + * + * This should be called after all other cleanup animations have finished. + */ + fun cleanup() { + if (taskId != INVALID_TASK_ID) { + // Ensure the task is removed from WM + if (ENABLE_SHELL_TRANSITIONS) { + taskView.removeTask() + } else { + try { + ActivityTaskManager.getService().removeTask(taskId) + } catch (e: RemoteException) { + Log.w(TAG, e.message ?: "") + } + } + } + } + + private companion object { + const val TAG = "BubbleTaskView" + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java index 5855a81333d4..5fc67d7b5f00 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java @@ -29,7 +29,6 @@ import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.graphics.Rect; -import android.os.RemoteException; import android.util.Log; import android.view.View; @@ -183,8 +182,11 @@ public class BubbleTaskViewHelper { + " bubble=" + getBubbleKey()); } if (mBubble != null) { - mController.removeBarBubbleAfterTaskRemoval( - mBubble.getKey(), Bubbles.DISMISS_TASK_FINISHED); + mController.removeBubble(mBubble.getKey(), Bubbles.DISMISS_TASK_FINISHED); + } + if (mTaskView != null) { + mTaskView.release(); + mTaskView = null; } } @@ -228,24 +230,6 @@ public class BubbleTaskViewHelper { return false; } - /** Cleans up anything related to the task and {@code TaskView}. */ - public void cleanUpTaskView() { - if (DEBUG_BUBBLE_EXPANDED_VIEW) { - Log.d(TAG, "cleanUpExpandedState: bubble=" + getBubbleKey() + " task=" + mTaskId); - } - if (mTaskId != INVALID_TASK_ID) { - try { - ActivityTaskManager.getService().removeTask(mTaskId); - } catch (RemoteException e) { - Log.w(TAG, e.getMessage()); - } - } - if (mTaskView != null) { - mTaskView.release(); - mTaskView = null; - } - } - /** Returns the bubble key associated with this view. */ @Nullable public String getBubbleKey() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java index c3d899e7dac7..5fc10a9f6b69 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java @@ -46,8 +46,6 @@ import com.android.launcher3.icons.BubbleIconFactory; import com.android.wm.shell.R; import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView; import com.android.wm.shell.bubbles.bar.BubbleBarLayerView; -import com.android.wm.shell.taskview.TaskView; -import com.android.wm.shell.taskview.TaskViewTaskController; import java.lang.ref.WeakReference; import java.util.Objects; @@ -175,7 +173,7 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask BubbleViewInfo info = new BubbleViewInfo(); if (!skipInflation && !b.isInflated()) { - BubbleTaskView bubbleTaskView = createBubbleTaskView(c, controller); + BubbleTaskView bubbleTaskView = b.getOrCreateBubbleTaskView(c, controller); LayoutInflater inflater = LayoutInflater.from(c); info.bubbleBarExpandedView = (BubbleBarExpandedView) inflater.inflate( R.layout.bubble_bar_expanded_view, layerView, false /* attachToRoot */); @@ -205,7 +203,7 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask R.layout.bubble_view, stackView, false /* attachToRoot */); info.imageView.initialize(controller.getPositioner()); - BubbleTaskView bubbleTaskView = createBubbleTaskView(c, controller); + BubbleTaskView bubbleTaskView = b.getOrCreateBubbleTaskView(c, controller); info.expandedView = (BubbleExpandedView) inflater.inflate( R.layout.bubble_expanded_view, stackView, false /* attachToRoot */); info.expandedView.initialize( @@ -225,15 +223,6 @@ public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask } return info; } - - private static BubbleTaskView createBubbleTaskView( - Context context, BubbleController controller) { - TaskViewTaskController taskViewTaskController = new TaskViewTaskController(context, - controller.getTaskOrganizer(), - controller.getTaskViewTransitions(), controller.getSyncTransactionQueue()); - TaskView taskView = new TaskView(context, taskViewTaskController); - return new BubbleTaskView(taskView, controller.getMainExecutor()); - } } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl index 5776ad109d19..7a5afec934f5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl @@ -17,6 +17,7 @@ package com.android.wm.shell.bubbles; import android.content.Intent; +import android.graphics.Rect; import com.android.wm.shell.bubbles.IBubblesListener; /** @@ -29,7 +30,7 @@ interface IBubbles { oneway void unregisterBubbleListener(in IBubblesListener listener) = 2; - oneway void showBubble(in String key, in int bubbleBarOffsetX, in int bubbleBarOffsetY) = 3; + oneway void showBubble(in String key, in Rect bubbleBarBounds) = 3; oneway void removeBubble(in String key) = 4; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt index 95f101722e89..c4108c4129e9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/StackEducationView.kt @@ -35,7 +35,7 @@ import com.android.wm.shell.animation.Interpolators class StackEducationView( context: Context, private val positioner: BubblePositioner, - private val controller: BubbleController + private val manager: Manager ) : LinearLayout(context) { companion object { @@ -44,6 +44,12 @@ class StackEducationView( private const val ANIMATE_DURATION_SHORT: Long = 40 } + /** Callbacks to notify managers of [StackEducationView] about events. */ + interface Manager { + /** Notifies whether backpress should be intercepted. */ + fun updateWindowFlagsForBackpress(interceptBack: Boolean) + } + private val view by lazy { requireViewById<View>(R.id.stack_education_layout) } private val titleTextView by lazy { requireViewById<TextView>(R.id.stack_education_title) } private val descTextView by lazy { requireViewById<TextView>(R.id.stack_education_description) } @@ -93,7 +99,7 @@ class StackEducationView( override fun onDetachedFromWindow() { super.onDetachedFromWindow() setOnKeyListener(null) - controller.updateWindowFlagsForBackpress(false /* interceptBack */) + manager.updateWindowFlagsForBackpress(false /* interceptBack */) } private fun setTextColor() { @@ -124,7 +130,7 @@ class StackEducationView( isHiding = false if (visibility == VISIBLE) return false - controller.updateWindowFlagsForBackpress(true /* interceptBack */) + manager.updateWindowFlagsForBackpress(true /* interceptBack */) layoutParams.width = if (positioner.isLargeScreen || positioner.isLandscape) context.resources.getDimensionPixelSize(R.dimen.bubbles_user_education_width) @@ -185,7 +191,7 @@ class StackEducationView( if (visibility != VISIBLE || isHiding) return isHiding = true - controller.updateWindowFlagsForBackpress(false /* interceptBack */) + manager.updateWindowFlagsForBackpress(false /* interceptBack */) animate() .alpha(0f) .setDuration(if (isExpanding) ANIMATE_DURATION_SHORT else ANIMATE_DURATION) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java index 893a87fe4885..84a616f1e1a0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java @@ -22,6 +22,7 @@ import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.content.Context; import android.graphics.Point; +import android.graphics.Rect; import android.util.Log; import android.util.Size; import android.view.View; @@ -149,12 +150,12 @@ public class BubbleBarAnimationHelper { bbev.setVisibility(VISIBLE); // Set the pivot point for the scale, so the view animates out from the bubble bar. - Point bubbleBarPosition = mPositioner.getBubbleBarPosition(); + Rect bubbleBarBounds = mPositioner.getBubbleBarBounds(); mExpandedViewContainerMatrix.setScale( 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, 1f - EXPANDED_VIEW_ANIMATE_SCALE_AMOUNT, - bubbleBarPosition.x, - bubbleBarPosition.y); + bubbleBarBounds.centerX(), + bubbleBarBounds.top); bbev.setAnimationMatrix(mExpandedViewContainerMatrix); 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 3cf23ac114ee..00d683e861e0 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 @@ -272,7 +272,6 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView if (mTaskView != null) { removeView(mTaskView); } - mBubbleTaskViewHelper.cleanUpTaskView(); } mMenuViewController.hideMenu(false /* animated */); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipMenuController.java index 0775f5279e31..2f1189a8a984 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipMenuController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipMenuController.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.pip; +package com.android.wm.shell.common.pip; import static android.view.WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY; @@ -33,12 +33,13 @@ import android.view.SurfaceControl; import android.view.WindowManager; import com.android.wm.shell.R; +import com.android.wm.shell.ShellTaskOrganizer; import java.util.List; /** - * Interface to allow {@link com.android.wm.shell.pip.PipTaskOrganizer} to call into - * PiP menu when certain events happen (task appear/vanish, PiP move, etc.) + * Interface to interact with PiP menu when certain events happen + * (task appear/vanish, PiP move, etc.). */ public interface PipMenuController { @@ -52,15 +53,15 @@ public interface PipMenuController { float ALPHA_NO_CHANGE = -1f; /** - * Called when - * {@link PipTaskOrganizer#onTaskAppeared(RunningTaskInfo, SurfaceControl)} + * Called when out implementation of + * {@link ShellTaskOrganizer.TaskListener#onTaskAppeared(RunningTaskInfo, SurfaceControl)} * is called. */ void attach(SurfaceControl leash); /** - * Called when - * {@link PipTaskOrganizer#onTaskVanished(RunningTaskInfo)} is called. + * Called when our implementation of + * {@link ShellTaskOrganizer.TaskListener#onTaskVanished(RunningTaskInfo)} is called. */ void detach(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java index 7b98fa6523cb..8eecf1c58db0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java @@ -18,18 +18,23 @@ package com.android.wm.shell.dagger.pip; import android.annotation.NonNull; import android.content.Context; +import android.os.Handler; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayInsetsController; import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.SystemWindows; import com.android.wm.shell.common.annotations.ShellMainThread; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; import com.android.wm.shell.common.pip.PipDisplayLayoutState; +import com.android.wm.shell.common.pip.PipMediaController; +import com.android.wm.shell.common.pip.PipUiEventLogger; import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.dagger.WMShellBaseModule; import com.android.wm.shell.dagger.WMSingleton; +import com.android.wm.shell.pip2.phone.PhonePipMenuController; import com.android.wm.shell.pip2.phone.PipController; import com.android.wm.shell.pip2.phone.PipScheduler; import com.android.wm.shell.pip2.phone.PipTransition; @@ -86,4 +91,16 @@ public abstract class Pip2Module { @ShellMainThread ShellExecutor mainExecutor) { return new PipScheduler(context, pipBoundsState, mainExecutor); } + + @WMSingleton + @Provides + static PhonePipMenuController providePipPhoneMenuController(Context context, + PipBoundsState pipBoundsState, PipMediaController pipMediaController, + SystemWindows systemWindows, + PipUiEventLogger pipUiEventLogger, + @ShellMainThread ShellExecutor mainExecutor, + @ShellMainThread Handler mainHandler) { + return new PhonePipMenuController(context, pipBoundsState, pipMediaController, + systemWindows, pipUiEventLogger, mainExecutor, mainHandler); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt index c0fc02fadd4d..f82212d97bd3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepository.kt @@ -86,10 +86,10 @@ class DesktopModeTaskRepository { ) { visibleTasksListeners[visibleTasksListener] = executor displayData.keyIterator().forEach { displayId -> - val visibleTasks = getVisibleTaskCount(displayId) + val visibleTasksCount = getVisibleTaskCount(displayId) val stashed = isStashed(displayId) executor.execute { - visibleTasksListener.onVisibilityChanged(displayId, visibleTasks > 0) + visibleTasksListener.onTasksVisibilityChanged(displayId, visibleTasksCount) visibleTasksListener.onStashedChanged(displayId, stashed) } } @@ -222,10 +222,8 @@ class DesktopModeTaskRepository { val otherDisplays = displayData.keyIterator().asSequence().filter { it != displayId } for (otherDisplayId in otherDisplays) { if (displayData[otherDisplayId].visibleTasks.remove(taskId)) { - // Task removed from other display, check if we should notify listeners - if (displayData[otherDisplayId].visibleTasks.isEmpty()) { - notifyVisibleTaskListeners(otherDisplayId, hasVisibleFreeformTasks = false) - } + notifyVisibleTaskListeners(otherDisplayId, + displayData[otherDisplayId].visibleTasks.size) } } } @@ -248,15 +246,15 @@ class DesktopModeTaskRepository { ) } - // Check if count changed and if there was no tasks or this is the first task - if (prevCount != newCount && (prevCount == 0 || newCount == 0)) { - notifyVisibleTaskListeners(displayId, newCount > 0) + // Check if count changed + if (prevCount != newCount) { + notifyVisibleTaskListeners(displayId, newCount) } } - private fun notifyVisibleTaskListeners(displayId: Int, hasVisibleFreeformTasks: Boolean) { + private fun notifyVisibleTaskListeners(displayId: Int, visibleTasksCount: Int) { visibleTasksListeners.forEach { (listener, executor) -> - executor.execute { listener.onVisibilityChanged(displayId, hasVisibleFreeformTasks) } + executor.execute { listener.onTasksVisibilityChanged(displayId, visibleTasksCount) } } } @@ -379,9 +377,9 @@ class DesktopModeTaskRepository { */ interface VisibleTasksListener { /** - * Called when the desktop starts or stops showing freeform tasks. + * Called when the desktop changes the number of visible freeform tasks. */ - fun onVisibilityChanged(displayId: Int, hasVisibleFreeformTasks: Boolean) {} + fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) {} /** * Called when the desktop stashed status changes. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java index da1ca8d57940..6250fc5820aa 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java @@ -17,6 +17,8 @@ package com.android.wm.shell.desktopmode; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; +import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; +import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; import static com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler.FINAL_FREEFORM_SCALE; @@ -86,7 +88,6 @@ public class DesktopModeVisualIndicator { mTaskSurface = taskSurface; mRootTdaOrganizer = taskDisplayAreaOrganizer; mCurrentType = IndicatorType.NO_INDICATOR; - createView(); } /** @@ -127,34 +128,15 @@ public class DesktopModeVisualIndicator { mView = new View(mContext); final SurfaceControl.Builder builder = new SurfaceControl.Builder(); mRootTdaOrganizer.attachToDisplayArea(mTaskInfo.displayId, builder); - String description; - switch (mCurrentType) { - case TO_DESKTOP_INDICATOR: - description = "Desktop indicator"; - break; - case TO_FULLSCREEN_INDICATOR: - description = "Fullscreen indicator"; - break; - case TO_SPLIT_LEFT_INDICATOR: - description = "Split Left indicator"; - break; - case TO_SPLIT_RIGHT_INDICATOR: - description = "Split Right indicator"; - break; - default: - description = "Invalid indicator"; - break; - } mLeash = builder - .setName(description) + .setName("Desktop Mode Visual Indicator") .setContainerLayer() .build(); t.show(mLeash); final WindowManager.LayoutParams lp = - new WindowManager.LayoutParams(screenWidth, screenHeight, - WindowManager.LayoutParams.TYPE_APPLICATION, - WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT); - lp.setTitle(description + " for Task=" + mTaskInfo.taskId); + new WindowManager.LayoutParams(screenWidth, screenHeight, TYPE_APPLICATION, + FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT); + lp.setTitle("Desktop Mode Visual Indicator"); lp.setTrustedOverlay(); final WindowlessWindowManager windowManager = new WindowlessWindowManager( mTaskInfo.configuration, mLeash, @@ -201,6 +183,9 @@ public class DesktopModeVisualIndicator { */ private void transitionIndicator(IndicatorType newType) { if (mCurrentType == newType) return; + if (mView == null) { + createView(); + } if (mCurrentType == IndicatorType.NO_INDICATOR) { fadeInIndicator(newType); } else if (newType == IndicatorType.NO_INDICATOR) { 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 4f5c39a60120..a089e81ff6dd 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 @@ -106,8 +106,8 @@ class DesktopTasksController( visualIndicator = null } private val taskVisibilityListener = object : VisibleTasksListener { - override fun onVisibilityChanged(displayId: Int, hasVisibleFreeformTasks: Boolean) { - launchAdjacentController.launchAdjacentEnabled = !hasVisibleFreeformTasks + override fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) { + launchAdjacentController.launchAdjacentEnabled = visibleTasksCount == 0 } } private val dragToDesktopStateListener = object : DragToDesktopStateListener { @@ -919,22 +919,27 @@ class DesktopTasksController( } if (taskBounds.top <= transitionAreaHeight) { moveToFullscreenWithAnimation(taskInfo, position) + return } if (inputCoordinate.x <= transitionAreaWidth) { releaseVisualIndicator() - var wct = WindowContainerTransaction() + val wct = WindowContainerTransaction() addMoveToSplitChanges(wct, taskInfo) splitScreenController.requestEnterSplitSelect(taskInfo, wct, SPLIT_POSITION_TOP_OR_LEFT, taskBounds) + return } if (inputCoordinate.x >= (displayController.getDisplayLayout(taskInfo.displayId)?.width() ?.minus(transitionAreaWidth) ?: return)) { releaseVisualIndicator() - var wct = WindowContainerTransaction() + val wct = WindowContainerTransaction() addMoveToSplitChanges(wct, taskInfo) splitScreenController.requestEnterSplitSelect(taskInfo, wct, SPLIT_POSITION_BOTTOM_OR_RIGHT, taskBounds) + return } + // A freeform drag-move ended, remove the indicator immediately. + releaseVisualIndicator() } /** @@ -1028,14 +1033,16 @@ class DesktopTasksController( SingleInstanceRemoteListener<DesktopTasksController, IDesktopTaskListener> private val listener: VisibleTasksListener = object : VisibleTasksListener { - override fun onVisibilityChanged(displayId: Int, visible: Boolean) { + override fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) { KtProtoLog.v( WM_SHELL_DESKTOP_MODE, - "IDesktopModeImpl: onVisibilityChanged display=%d visible=%b", + "IDesktopModeImpl: onVisibilityChanged display=%d visible=%d", displayId, - visible + visibleTasksCount ) - remoteListener.call { l -> l.onVisibilityChanged(displayId, visible) } + remoteListener.call { + l -> l.onTasksVisibilityChanged(displayId, visibleTasksCount) + } } override fun onStashedChanged(displayId: Int, stashed: Boolean) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt index c3a82ce258df..731fec7899ae 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt @@ -239,7 +239,7 @@ class DragToDesktopTransitionHandler( show(change.leash) } } else if (TransitionInfo.isIndependent(change, info)) { - // Root. + // Root(s). when (state) { is TransitionState.FromSplit -> { state.splitRootChange = change @@ -256,6 +256,9 @@ class DragToDesktopTransitionHandler( } } is TransitionState.FromFullscreen -> { + // Most of the time we expect one change/task here, which should be the + // same that initiated the drag and that should be layered on top of + // everything. if (change.taskInfo?.taskId == state.draggedTaskId) { state.draggedTaskChange = change val bounds = change.endAbsBounds @@ -265,7 +268,18 @@ class DragToDesktopTransitionHandler( show(change.leash) } } else { - throw IllegalStateException("Expected root to be dragged task") + // It's possible to see an additional change that isn't the dragged + // task when the dragged task is translucent and so the task behind it + // is included in the transition since it was visible and is now being + // occluded by the Home task. Just layer it at the bottom and save it + // in case we need to restore order if the drag is cancelled. + state.otherRootChanges.add(change) + val bounds = change.endAbsBounds + startTransaction.apply { + setLayer(change.leash, appLayers - i) + setWindowCrop(change.leash, bounds.width(), bounds.height()) + show(change.leash) + } } } } @@ -515,8 +529,18 @@ class DragToDesktopTransitionHandler( val wct = WindowContainerTransaction() when (state) { is TransitionState.FromFullscreen -> { + // There may have been tasks sent behind home that are not the dragged task (like + // when the dragged task is translucent and that makes the task behind it visible). + // Restore the order of those first. + state.otherRootChanges.mapNotNull { it.container }.forEach { wc -> + // TODO(b/322852244): investigate why even though these "other" tasks are + // reordered in front of home and behind the translucent dragged task, its + // surface is not visible on screen. + wct.reorder(wc, true /* toTop */) + } val wc = state.draggedTaskChange?.container ?: error("Dragged task should be non-null before cancelling") + // Then the dragged task a the very top. wct.reorder(wc, true /* toTop */) } is TransitionState.FromSplit -> { @@ -574,6 +598,7 @@ class DragToDesktopTransitionHandler( override var draggedTaskChange: Change? = null, override var cancelled: Boolean = false, override var startAborted: Boolean = false, + var otherRootChanges: MutableList<Change> = mutableListOf() ) : TransitionState() data class FromSplit( override val draggedTaskId: Int, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl index 39128a863ec9..8ed87f23bf40 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl @@ -22,8 +22,8 @@ package com.android.wm.shell.desktopmode; */ interface IDesktopTaskListener { - /** Desktop task visibility has change. Visible if at least 1 task is visible. */ - oneway void onVisibilityChanged(int displayId, boolean visible); + /** Desktop tasks visibility has changed. Visible if at least 1 task is visible. */ + oneway void onTasksVisibilityChanged(int displayId, int visibleTasksCount); /** Desktop task stashed status has changed. */ oneway void onStashedChanged(int displayId, boolean stashed); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java index 07b8f11458be..52a06e0bbe7f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java @@ -86,6 +86,7 @@ import com.android.wm.shell.common.annotations.ShellMainThread; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; import com.android.wm.shell.common.pip.PipDisplayLayoutState; +import com.android.wm.shell.common.pip.PipMenuController; import com.android.wm.shell.common.pip.PipUiEventLogger; import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.pip.phone.PipMotionHelper; 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 8e375a9ef5ee..e7392662bdf1 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 @@ -68,6 +68,7 @@ import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; import com.android.wm.shell.common.pip.PipDisplayLayoutState; +import com.android.wm.shell.common.pip.PipMenuController; import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.splitscreen.SplitScreenController; 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 0e7073688ec4..d1fd207c4a66 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 @@ -40,6 +40,7 @@ import androidx.annotation.NonNull; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; +import com.android.wm.shell.common.pip.PipMenuController; import com.android.wm.shell.common.split.SplitScreenUtils; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java index 760652625f9e..d8e8b587004a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PhonePipMenuController.java @@ -41,8 +41,8 @@ import com.android.wm.shell.common.SystemWindows; import com.android.wm.shell.common.pip.PipBoundsState; import com.android.wm.shell.common.pip.PipMediaController; import com.android.wm.shell.common.pip.PipMediaController.ActionListener; +import com.android.wm.shell.common.pip.PipMenuController; import com.android.wm.shell.common.pip.PipUiEventLogger; -import com.android.wm.shell.pip.PipMenuController; import com.android.wm.shell.pip.PipSurfaceTransactionHelper; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.splitscreen.SplitScreenController; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java index c6803f7beebd..843c84a06f8c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java @@ -40,7 +40,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.common.ProtoLog; import com.android.wm.shell.R; import com.android.wm.shell.common.SystemWindows; -import com.android.wm.shell.pip.PipMenuController; +import com.android.wm.shell.common.pip.PipMenuController; import com.android.wm.shell.protolog.ShellProtoLogGroup; import java.util.List; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java index 21223c9ac362..cac63eb2a2ad 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTaskOrganizer.java @@ -28,10 +28,10 @@ import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; import com.android.wm.shell.common.pip.PipDisplayLayoutState; +import com.android.wm.shell.common.pip.PipMenuController; import com.android.wm.shell.common.pip.PipUiEventLogger; import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.pip.PipAnimationController; -import com.android.wm.shell.pip.PipMenuController; import com.android.wm.shell.pip.PipParamsChangedForwarder; import com.android.wm.shell.pip.PipSurfaceTransactionHelper; import com.android.wm.shell.pip.PipTaskOrganizer; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java index 571c839adf11..d16a692dbd0a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java @@ -25,10 +25,10 @@ import static android.view.WindowManager.TRANSIT_PIP; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.transitTypeToString; +import static com.android.wm.shell.common.pip.PipMenuController.ALPHA_NO_CHANGE; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_REMOVE_STACK; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_TO_PIP; -import static com.android.wm.shell.pip.PipMenuController.ALPHA_NO_CHANGE; import static com.android.wm.shell.pip.PipTransitionState.ENTERED_PIP; import static com.android.wm.shell.pip.PipTransitionState.ENTERING_PIP; import static com.android.wm.shell.pip.PipTransitionState.EXITING_PIP; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/PipSurfaceTransactionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/PipSurfaceTransactionHelper.java new file mode 100644 index 000000000000..24077a35d41c --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/PipSurfaceTransactionHelper.java @@ -0,0 +1,277 @@ +/* + * Copyright (C) 2020 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.wm.shell.pip2; + +import android.content.Context; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.graphics.RectF; +import android.view.Choreographer; +import android.view.SurfaceControl; + +import com.android.wm.shell.R; +import com.android.wm.shell.transition.Transitions; + +/** + * Abstracts the common operations on {@link SurfaceControl.Transaction} for PiP transition. + */ +public class PipSurfaceTransactionHelper { + /** for {@link #scale(SurfaceControl.Transaction, SurfaceControl, Rect, Rect)} operation */ + private final Matrix mTmpTransform = new Matrix(); + private final float[] mTmpFloat9 = new float[9]; + private final RectF mTmpSourceRectF = new RectF(); + private final RectF mTmpDestinationRectF = new RectF(); + private final Rect mTmpDestinationRect = new Rect(); + + private int mCornerRadius; + private int mShadowRadius; + + public PipSurfaceTransactionHelper(Context context) { + onDensityOrFontScaleChanged(context); + } + + /** + * Called when display size or font size of settings changed + * + * @param context the current context + */ + public void onDensityOrFontScaleChanged(Context context) { + mCornerRadius = context.getResources().getDimensionPixelSize(R.dimen.pip_corner_radius); + mShadowRadius = context.getResources().getDimensionPixelSize(R.dimen.pip_shadow_radius); + } + + /** + * Operates the alpha on a given transaction and leash + * @return same {@link PipSurfaceTransactionHelper} instance for method chaining + */ + public PipSurfaceTransactionHelper alpha(SurfaceControl.Transaction tx, SurfaceControl leash, + float alpha) { + tx.setAlpha(leash, alpha); + return this; + } + + /** + * Operates the crop (and position) on a given transaction and leash + * @return same {@link PipSurfaceTransactionHelper} instance for method chaining + */ + public PipSurfaceTransactionHelper crop(SurfaceControl.Transaction tx, SurfaceControl leash, + Rect destinationBounds) { + tx.setWindowCrop(leash, destinationBounds.width(), destinationBounds.height()) + .setPosition(leash, destinationBounds.left, destinationBounds.top); + return this; + } + + /** + * Operates the scale (setMatrix) on a given transaction and leash + * @return same {@link PipSurfaceTransactionHelper} instance for method chaining + */ + public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash, + Rect sourceBounds, Rect destinationBounds) { + mTmpDestinationRectF.set(destinationBounds); + return scale(tx, leash, sourceBounds, mTmpDestinationRectF, 0 /* degrees */); + } + + /** + * Operates the scale (setMatrix) on a given transaction and leash + * @return same {@link PipSurfaceTransactionHelper} instance for method chaining + */ + public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash, + Rect sourceBounds, RectF destinationBounds) { + return scale(tx, leash, sourceBounds, destinationBounds, 0 /* degrees */); + } + + /** + * Operates the scale (setMatrix) on a given transaction and leash + * @return same {@link PipSurfaceTransactionHelper} instance for method chaining + */ + public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash, + Rect sourceBounds, Rect destinationBounds, float degrees) { + mTmpDestinationRectF.set(destinationBounds); + return scale(tx, leash, sourceBounds, mTmpDestinationRectF, degrees); + } + + /** + * Operates the scale (setMatrix) on a given transaction and leash, along with a rotation. + * @return same {@link PipSurfaceTransactionHelper} instance for method chaining + */ + public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash, + Rect sourceBounds, RectF destinationBounds, float degrees) { + mTmpSourceRectF.set(sourceBounds); + // We want the matrix to position the surface relative to the screen coordinates so offset + // the source to 0,0 + mTmpSourceRectF.offsetTo(0, 0); + mTmpDestinationRectF.set(destinationBounds); + mTmpTransform.setRectToRect(mTmpSourceRectF, mTmpDestinationRectF, Matrix.ScaleToFit.FILL); + mTmpTransform.postRotate(degrees, + mTmpDestinationRectF.centerX(), mTmpDestinationRectF.centerY()); + tx.setMatrix(leash, mTmpTransform, mTmpFloat9); + return this; + } + + /** + * Operates the scale (setMatrix) on a given transaction and leash + * @return same {@link PipSurfaceTransactionHelper} instance for method chaining + */ + public PipSurfaceTransactionHelper scaleAndCrop(SurfaceControl.Transaction tx, + SurfaceControl leash, Rect sourceRectHint, + Rect sourceBounds, Rect destinationBounds, Rect insets, + boolean isInPipDirection, float fraction) { + mTmpDestinationRect.set(sourceBounds); + // Similar to {@link #scale}, we want to position the surface relative to the screen + // coordinates so offset the bounds to 0,0 + mTmpDestinationRect.offsetTo(0, 0); + mTmpDestinationRect.inset(insets); + // Scale to the bounds no smaller than the destination and offset such that the top/left + // of the scaled inset source rect aligns with the top/left of the destination bounds + final float scale; + if (isInPipDirection + && sourceRectHint != null && sourceRectHint.width() < sourceBounds.width()) { + // scale by sourceRectHint if it's not edge-to-edge, for entering PiP transition only. + final float endScale = sourceBounds.width() <= sourceBounds.height() + ? (float) destinationBounds.width() / sourceRectHint.width() + : (float) destinationBounds.height() / sourceRectHint.height(); + final float startScale = sourceBounds.width() <= sourceBounds.height() + ? (float) destinationBounds.width() / sourceBounds.width() + : (float) destinationBounds.height() / sourceBounds.height(); + scale = (1 - fraction) * startScale + fraction * endScale; + } else { + scale = Math.max((float) destinationBounds.width() / sourceBounds.width(), + (float) destinationBounds.height() / sourceBounds.height()); + } + final float left = destinationBounds.left - insets.left * scale; + final float top = destinationBounds.top - insets.top * scale; + mTmpTransform.setScale(scale, scale); + tx.setMatrix(leash, mTmpTransform, mTmpFloat9) + .setCrop(leash, mTmpDestinationRect) + .setPosition(leash, left, top); + return this; + } + + /** + * Operates the rotation according to the given degrees and scale (setMatrix) according to the + * source bounds and rotated destination bounds. The crop will be the unscaled source bounds. + * @return same {@link PipSurfaceTransactionHelper} instance for method chaining + */ + public PipSurfaceTransactionHelper rotateAndScaleWithCrop(SurfaceControl.Transaction tx, + SurfaceControl leash, Rect sourceBounds, Rect destinationBounds, Rect insets, + float degrees, float positionX, float positionY, boolean isExpanding, + boolean clockwise) { + mTmpDestinationRect.set(sourceBounds); + mTmpDestinationRect.inset(insets); + final int srcW = mTmpDestinationRect.width(); + final int srcH = mTmpDestinationRect.height(); + final int destW = destinationBounds.width(); + final int destH = destinationBounds.height(); + // Scale by the short side so there won't be empty area if the aspect ratio of source and + // destination are different. + final float scale = srcW <= srcH ? (float) destW / srcW : (float) destH / srcH; + final Rect crop = mTmpDestinationRect; + crop.set(0, 0, Transitions.SHELL_TRANSITIONS_ROTATION ? destH + : destW, Transitions.SHELL_TRANSITIONS_ROTATION ? destW : destH); + // Inverse scale for crop to fit in screen coordinates. + crop.scale(1 / scale); + crop.offset(insets.left, insets.top); + if (isExpanding) { + // Expand bounds (shrink insets) in source orientation. + positionX -= insets.left * scale; + positionY -= insets.top * scale; + } else { + // Shrink bounds (expand insets) in destination orientation. + if (clockwise) { + positionX -= insets.top * scale; + positionY += insets.left * scale; + } else { + positionX += insets.top * scale; + positionY -= insets.left * scale; + } + } + mTmpTransform.setScale(scale, scale); + mTmpTransform.postRotate(degrees); + mTmpTransform.postTranslate(positionX, positionY); + tx.setMatrix(leash, mTmpTransform, mTmpFloat9).setCrop(leash, crop); + return this; + } + + /** + * Resets the scale (setMatrix) on a given transaction and leash if there's any + * + * @return same {@link PipSurfaceTransactionHelper} instance for method chaining + */ + public PipSurfaceTransactionHelper resetScale(SurfaceControl.Transaction tx, + SurfaceControl leash, + Rect destinationBounds) { + tx.setMatrix(leash, Matrix.IDENTITY_MATRIX, mTmpFloat9) + .setPosition(leash, destinationBounds.left, destinationBounds.top); + return this; + } + + /** + * Operates the round corner radius on a given transaction and leash + * @return same {@link PipSurfaceTransactionHelper} instance for method chaining + */ + public PipSurfaceTransactionHelper round(SurfaceControl.Transaction tx, SurfaceControl leash, + boolean applyCornerRadius) { + tx.setCornerRadius(leash, applyCornerRadius ? mCornerRadius : 0); + return this; + } + + /** + * Operates the round corner radius on a given transaction and leash, scaled by bounds + * @return same {@link PipSurfaceTransactionHelper} instance for method chaining + */ + public PipSurfaceTransactionHelper round(SurfaceControl.Transaction tx, SurfaceControl leash, + Rect fromBounds, Rect toBounds) { + final float scale = (float) (Math.hypot(fromBounds.width(), fromBounds.height()) + / Math.hypot(toBounds.width(), toBounds.height())); + tx.setCornerRadius(leash, mCornerRadius * scale); + return this; + } + + /** + * Operates the shadow radius on a given transaction and leash + * @return same {@link PipSurfaceTransactionHelper} instance for method chaining + */ + public PipSurfaceTransactionHelper shadow(SurfaceControl.Transaction tx, SurfaceControl leash, + boolean applyShadowRadius) { + tx.setShadowRadius(leash, applyShadowRadius ? mShadowRadius : 0); + return this; + } + + /** + * Interface to standardize {@link SurfaceControl.Transaction} generation across PiP. + */ + public interface SurfaceControlTransactionFactory { + /** + * @return a new transaction to operate on. + */ + SurfaceControl.Transaction getTransaction(); + } + + /** + * Implementation of {@link SurfaceControlTransactionFactory} that returns + * {@link SurfaceControl.Transaction} with VsyncId being set. + */ + public static class VsyncSurfaceControlTransactionFactory + implements SurfaceControlTransactionFactory { + @Override + public SurfaceControl.Transaction getTransaction() { + final SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); + tx.setFrameTimelineVsync(Choreographer.getInstance().getVsyncId()); + return tx; + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PhonePipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PhonePipMenuController.java new file mode 100644 index 000000000000..2478252213a7 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PhonePipMenuController.java @@ -0,0 +1,608 @@ +/* + * Copyright (C) 2020 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.wm.shell.pip2.phone; + +import static android.view.WindowManager.SHELL_ROOT_LAYER_PIP; + +import android.annotation.Nullable; +import android.app.ActivityManager; +import android.app.RemoteAction; +import android.content.Context; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.graphics.RectF; +import android.os.Debug; +import android.os.Handler; +import android.os.RemoteException; +import android.util.Size; +import android.view.MotionEvent; +import android.view.SurfaceControl; +import android.view.View; +import android.view.ViewRootImpl; +import android.view.WindowManagerGlobal; + +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.SystemWindows; +import com.android.wm.shell.common.pip.PipBoundsState; +import com.android.wm.shell.common.pip.PipMediaController; +import com.android.wm.shell.common.pip.PipMediaController.ActionListener; +import com.android.wm.shell.common.pip.PipMenuController; +import com.android.wm.shell.common.pip.PipUiEventLogger; +import com.android.wm.shell.pip2.PipSurfaceTransactionHelper; +import com.android.wm.shell.protolog.ShellProtoLogGroup; + +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; + +/** + * Manages the PiP menu view which can show menu options or a scrim. + * + * The current media session provides actions whenever there are no valid actions provided by the + * current PiP activity. Otherwise, those actions always take precedence. + */ +public class PhonePipMenuController implements PipMenuController { + + private static final String TAG = "PhonePipMenuController"; + private static final boolean DEBUG = false; + + public static final int MENU_STATE_NONE = 0; + public static final int MENU_STATE_FULL = 1; + + /** + * A listener interface to receive notification on changes in PIP. + */ + public interface Listener { + /** + * Called when the PIP menu visibility change has started. + * + * @param menuState the new, about-to-change state of the menu + * @param resize whether or not to resize the PiP with the state change + */ + void onPipMenuStateChangeStart(int menuState, boolean resize, Runnable callback); + + /** + * Called when the PIP menu state has finished changing/animating. + * + * @param menuState the new state of the menu. + */ + void onPipMenuStateChangeFinish(int menuState); + + /** + * Called when the PIP requested to be expanded. + */ + void onPipExpand(); + + /** + * Called when the PIP requested to be dismissed. + */ + void onPipDismiss(); + + /** + * Called when the PIP requested to show the menu. + */ + void onPipShowMenu(); + + /** + * Called when the PIP requested to enter Split. + */ + void onEnterSplit(); + } + + private final Matrix mMoveTransform = new Matrix(); + private final Rect mTmpSourceBounds = new Rect(); + private final RectF mTmpSourceRectF = new RectF(); + private final RectF mTmpDestinationRectF = new RectF(); + private final Context mContext; + private final PipBoundsState mPipBoundsState; + private final PipMediaController mMediaController; + private final ShellExecutor mMainExecutor; + private final Handler mMainHandler; + + private final PipSurfaceTransactionHelper.SurfaceControlTransactionFactory + mSurfaceControlTransactionFactory; + private final float[] mTmpTransform = new float[9]; + + private final ArrayList<Listener> mListeners = new ArrayList<>(); + private final SystemWindows mSystemWindows; + private final PipUiEventLogger mPipUiEventLogger; + + private List<RemoteAction> mAppActions; + private RemoteAction mCloseAction; + private List<RemoteAction> mMediaActions; + + private int mMenuState; + + private PipMenuView mPipMenuView; + + private SurfaceControl mLeash; + + private ActionListener mMediaActionListener = new ActionListener() { + @Override + public void onMediaActionsChanged(List<RemoteAction> mediaActions) { + mMediaActions = new ArrayList<>(mediaActions); + updateMenuActions(); + } + }; + + public PhonePipMenuController(Context context, PipBoundsState pipBoundsState, + PipMediaController mediaController, SystemWindows systemWindows, + PipUiEventLogger pipUiEventLogger, + ShellExecutor mainExecutor, Handler mainHandler) { + mContext = context; + mPipBoundsState = pipBoundsState; + mMediaController = mediaController; + mSystemWindows = systemWindows; + mMainExecutor = mainExecutor; + mMainHandler = mainHandler; + mPipUiEventLogger = pipUiEventLogger; + + mSurfaceControlTransactionFactory = + new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory(); + } + + public boolean isMenuVisible() { + return mPipMenuView != null && mMenuState != MENU_STATE_NONE; + } + + /** + * Attach the menu when the PiP task first appears. + */ + @Override + public void attach(SurfaceControl leash) { + mLeash = leash; + attachPipMenuView(); + } + + /** + * Detach the menu when the PiP task is gone. + */ + @Override + public void detach() { + hideMenu(); + detachPipMenuView(); + mLeash = null; + } + + void attachPipMenuView() { + // In case detach was not called (e.g. PIP unexpectedly closed) + if (mPipMenuView != null) { + detachPipMenuView(); + } + mPipMenuView = new PipMenuView(mContext, this, mMainExecutor, mMainHandler, + mPipUiEventLogger); + mPipMenuView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View v) { + v.getViewRootImpl().addSurfaceChangedCallback( + new ViewRootImpl.SurfaceChangedCallback() { + @Override + public void surfaceCreated(SurfaceControl.Transaction t) { + final SurfaceControl sc = getSurfaceControl(); + if (sc != null) { + t.reparent(sc, mLeash); + // make menu on top of the surface + t.setLayer(sc, Integer.MAX_VALUE); + } + } + + @Override + public void surfaceReplaced(SurfaceControl.Transaction t) { + } + + @Override + public void surfaceDestroyed() { + } + }); + } + + @Override + public void onViewDetachedFromWindow(View v) { + } + }); + + mSystemWindows.addView(mPipMenuView, + getPipMenuLayoutParams(mContext, MENU_WINDOW_TITLE, 0 /* width */, 0 /* height */), + 0, SHELL_ROOT_LAYER_PIP); + setShellRootAccessibilityWindow(); + + // Make sure the initial actions are set + updateMenuActions(); + } + + private void detachPipMenuView() { + if (mPipMenuView == null) { + return; + } + + mSystemWindows.removeView(mPipMenuView); + mPipMenuView = null; + } + + /** + * Updates the layout parameters of the menu. + * @param destinationBounds New Menu bounds. + */ + @Override + public void updateMenuBounds(Rect destinationBounds) { + mSystemWindows.updateViewLayout(mPipMenuView, + getPipMenuLayoutParams(mContext, MENU_WINDOW_TITLE, destinationBounds.width(), + destinationBounds.height())); + updateMenuLayout(destinationBounds); + } + + @Override + public void onFocusTaskChanged(ActivityManager.RunningTaskInfo taskInfo) { + if (mPipMenuView != null) { + mPipMenuView.onFocusTaskChanged(taskInfo); + } + } + + /** + * Tries to grab a surface control from {@link PipMenuView}. If this isn't available for some + * reason (ie. the window isn't ready yet, thus {@link ViewRootImpl} is + * {@code null}), it will get the leash that the WindowlessWM has assigned to it. + */ + public SurfaceControl getSurfaceControl() { + return mSystemWindows.getViewSurface(mPipMenuView); + } + + /** + * Adds a new menu activity listener. + */ + public void addListener(Listener listener) { + if (!mListeners.contains(listener)) { + mListeners.add(listener); + } + } + + @Nullable + Size getEstimatedMinMenuSize() { + return mPipMenuView == null ? null : mPipMenuView.getEstimatedMinMenuSize(); + } + + /** + * When other components requests the menu controller directly to show the menu, we must + * first fire off the request to the other listeners who will then propagate the call + * back to the controller with the right parameters. + */ + @Override + public void showMenu() { + mListeners.forEach(Listener::onPipShowMenu); + } + + /** + * Similar to {@link #showMenu(int, Rect, boolean, boolean, boolean)} but only show the menu + * upon PiP window transition is finished. + */ + public void showMenuWithPossibleDelay(int menuState, Rect stackBounds, boolean allowMenuTimeout, + boolean willResizeMenu, boolean showResizeHandle) { + if (willResizeMenu) { + // hide all visible controls including close button and etc. first, this is to ensure + // menu is totally invisible during the transition to eliminate unpleasant artifacts + fadeOutMenu(); + } + showMenuInternal(menuState, stackBounds, allowMenuTimeout, willResizeMenu, + willResizeMenu /* withDelay=willResizeMenu here */, showResizeHandle); + } + + /** + * Shows the menu activity immediately. + */ + public void showMenu(int menuState, Rect stackBounds, boolean allowMenuTimeout, + boolean willResizeMenu, boolean showResizeHandle) { + showMenuInternal(menuState, stackBounds, allowMenuTimeout, willResizeMenu, + false /* withDelay */, showResizeHandle); + } + + private void showMenuInternal(int menuState, Rect stackBounds, boolean allowMenuTimeout, + boolean willResizeMenu, boolean withDelay, boolean showResizeHandle) { + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: showMenu() state=%s" + + " isMenuVisible=%s" + + " allowMenuTimeout=%s" + + " willResizeMenu=%s" + + " withDelay=%s" + + " showResizeHandle=%s" + + " callers=\n%s", TAG, menuState, isMenuVisible(), allowMenuTimeout, + willResizeMenu, withDelay, showResizeHandle, Debug.getCallers(5, " ")); + } + + if (!checkPipMenuState()) { + return; + } + + // Sync the menu bounds before showing it in case it is out of sync. + movePipMenu(null /* pipLeash */, null /* transaction */, stackBounds, + PipMenuController.ALPHA_NO_CHANGE); + updateMenuBounds(stackBounds); + + mPipMenuView.showMenu(menuState, stackBounds, allowMenuTimeout, willResizeMenu, withDelay, + showResizeHandle); + } + + /** + * Move the PiP menu, which does a translation and possibly a scale transformation. + */ + @Override + public void movePipMenu(@Nullable SurfaceControl pipLeash, + @Nullable SurfaceControl.Transaction t, + Rect destinationBounds, float alpha) { + if (destinationBounds.isEmpty()) { + return; + } + + if (!checkPipMenuState()) { + return; + } + + // TODO(b/286307861) transaction should be applied outside of PiP menu controller + if (pipLeash != null && t != null) { + t.apply(); + } + } + + /** + * Does an immediate window crop of the PiP menu. + */ + @Override + public void resizePipMenu(@Nullable SurfaceControl pipLeash, + @Nullable SurfaceControl.Transaction t, + Rect destinationBounds) { + if (destinationBounds.isEmpty()) { + return; + } + + if (!checkPipMenuState()) { + return; + } + + // TODO(b/286307861) transaction should be applied outside of PiP menu controller + if (pipLeash != null && t != null) { + t.apply(); + } + } + + private boolean checkPipMenuState() { + if (mPipMenuView == null || mPipMenuView.getViewRootImpl() == null) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Not going to move PiP, either menu or its parent is not created.", TAG); + return false; + } + + return true; + } + + /** + * Pokes the menu, indicating that the user is interacting with it. + */ + public void pokeMenu() { + final boolean isMenuVisible = isMenuVisible(); + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: pokeMenu() isMenuVisible=%b", TAG, isMenuVisible); + } + if (isMenuVisible) { + mPipMenuView.pokeMenu(); + } + } + + private void fadeOutMenu() { + final boolean isMenuVisible = isMenuVisible(); + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: fadeOutMenu() isMenuVisible=%b", TAG, isMenuVisible); + } + if (isMenuVisible) { + mPipMenuView.fadeOutMenu(); + } + } + + /** + * Hides the menu view. + */ + public void hideMenu() { + final boolean isMenuVisible = isMenuVisible(); + if (isMenuVisible) { + mPipMenuView.hideMenu(); + } + } + + /** + * Hides the menu view. + * + * @param animationType the animation type to use upon hiding the menu + * @param resize whether or not to resize the PiP with the state change + */ + public void hideMenu(@PipMenuView.AnimationType int animationType, boolean resize) { + final boolean isMenuVisible = isMenuVisible(); + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: hideMenu() state=%s" + + " isMenuVisible=%s" + + " animationType=%s" + + " resize=%s" + + " callers=\n%s", TAG, mMenuState, isMenuVisible, + animationType, resize, + Debug.getCallers(5, " ")); + } + if (isMenuVisible) { + mPipMenuView.hideMenu(resize, animationType); + } + } + + /** + * Hides the menu activity. + */ + public void hideMenu(Runnable onStartCallback, Runnable onEndCallback) { + if (isMenuVisible()) { + // If the menu is visible in either the closed or full state, then hide the menu and + // trigger the animation trigger afterwards + if (onStartCallback != null) { + onStartCallback.run(); + } + mPipMenuView.hideMenu(onEndCallback); + } + } + + /** + * Sets the menu actions to the actions provided by the current PiP menu. + */ + @Override + public void setAppActions(List<RemoteAction> appActions, + RemoteAction closeAction) { + mAppActions = appActions; + mCloseAction = closeAction; + updateMenuActions(); + } + + void onPipExpand() { + mListeners.forEach(Listener::onPipExpand); + } + + void onPipDismiss() { + mListeners.forEach(Listener::onPipDismiss); + } + + void onEnterSplit() { + mListeners.forEach(Listener::onEnterSplit); + } + + /** + * @return the best set of actions to show in the PiP menu. + */ + private List<RemoteAction> resolveMenuActions() { + if (isValidActions(mAppActions)) { + return mAppActions; + } + return mMediaActions; + } + + /** + * Updates the PiP menu with the best set of actions provided. + */ + private void updateMenuActions() { + if (mPipMenuView != null) { + mPipMenuView.setActions(mPipBoundsState.getBounds(), + resolveMenuActions(), mCloseAction); + } + } + + /** + * Returns whether the set of actions are valid. + */ + private static boolean isValidActions(List<?> actions) { + return actions != null && actions.size() > 0; + } + + /** + * Handles changes in menu visibility. + */ + void onMenuStateChangeStart(int menuState, boolean resize, Runnable callback) { + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: onMenuStateChangeStart() mMenuState=%s" + + " menuState=%s resize=%s" + + " callers=\n%s", TAG, mMenuState, menuState, resize, + Debug.getCallers(5, " ")); + } + + if (menuState != mMenuState) { + mListeners.forEach(l -> l.onPipMenuStateChangeStart(menuState, resize, callback)); + if (menuState == MENU_STATE_FULL) { + // Once visible, start listening for media action changes. This call will trigger + // the menu actions to be updated again. + mMediaController.addActionListener(mMediaActionListener); + } else { + // Once hidden, stop listening for media action changes. This call will trigger + // the menu actions to be updated again. + mMediaController.removeActionListener(mMediaActionListener); + } + + try { + WindowManagerGlobal.getWindowSession().grantEmbeddedWindowFocus(null /* window */, + mSystemWindows.getFocusGrantToken(mPipMenuView), + menuState != MENU_STATE_NONE /* grantFocus */); + } catch (RemoteException e) { + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Unable to update focus as menu appears/disappears, %s", TAG, e); + } + } + } + + void onMenuStateChangeFinish(int menuState) { + if (menuState != mMenuState) { + mListeners.forEach(l -> l.onPipMenuStateChangeFinish(menuState)); + } + mMenuState = menuState; + setShellRootAccessibilityWindow(); + } + + private void setShellRootAccessibilityWindow() { + switch (mMenuState) { + case MENU_STATE_NONE: + mSystemWindows.setShellRootAccessibilityWindow(0, SHELL_ROOT_LAYER_PIP, null); + break; + default: + mSystemWindows.setShellRootAccessibilityWindow(0, SHELL_ROOT_LAYER_PIP, + mPipMenuView); + break; + } + } + + /** + * Handles a pointer event sent from pip input consumer. + */ + void handlePointerEvent(MotionEvent ev) { + if (mPipMenuView == null) { + return; + } + + if (ev.isTouchEvent()) { + mPipMenuView.dispatchTouchEvent(ev); + } else { + mPipMenuView.dispatchGenericMotionEvent(ev); + } + } + + /** + * Tell the PIP Menu to recalculate its layout given its current position on the display. + */ + public void updateMenuLayout(Rect bounds) { + final boolean isMenuVisible = isMenuVisible(); + if (DEBUG) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: updateMenuLayout() state=%s" + + " isMenuVisible=%s" + + " callers=\n%s", TAG, mMenuState, isMenuVisible, + Debug.getCallers(5, " ")); + } + if (isMenuVisible) { + mPipMenuView.updateMenuLayout(bounds); + } + } + + void dump(PrintWriter pw, String prefix) { + final String innerPrefix = prefix + " "; + pw.println(prefix + TAG); + pw.println(innerPrefix + "mMenuState=" + mMenuState); + pw.println(innerPrefix + "mPipMenuView=" + mPipMenuView); + pw.println(innerPrefix + "mListeners=" + mListeners.size()); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuActionView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuActionView.java new file mode 100644 index 000000000000..7252675dc52d --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuActionView.java @@ -0,0 +1,56 @@ +/* + * Copyright (C) 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.pip2.phone; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.view.View; +import android.widget.FrameLayout; +import android.widget.ImageView; + +import com.android.wm.shell.R; + +/** + * Container layout wraps single action image view drawn in PiP menu and can restrict the size of + * action image view (see pip_menu_action.xml). + */ +public class PipMenuActionView extends FrameLayout { + private ImageView mImageView; + private View mCustomCloseBackground; + + public PipMenuActionView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mImageView = findViewById(R.id.image); + mCustomCloseBackground = findViewById(R.id.custom_close_bg); + } + + /** pass through to internal {@link #mImageView} */ + public void setImageDrawable(Drawable drawable) { + mImageView.setImageDrawable(drawable); + } + + /** pass through to internal {@link #mCustomCloseBackground} */ + public void setCustomCloseBackgroundVisibility(@Visibility int visibility) { + mCustomCloseBackground.setVisibility(visibility); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuIconsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuIconsAlgorithm.java new file mode 100644 index 000000000000..b5e575ba33f2 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuIconsAlgorithm.java @@ -0,0 +1,73 @@ +/* + * Copyright (C) 2020 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.wm.shell.pip2.phone; + +import android.content.Context; +import android.graphics.Rect; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +/** + * Helper class to calculate and place the menu icons on the PIP Menu. + */ +public class PipMenuIconsAlgorithm { + + private static final String TAG = "PipMenuIconsAlgorithm"; + + protected ViewGroup mViewRoot; + protected ViewGroup mTopEndContainer; + protected View mDragHandle; + protected View mEnterSplitButton; + protected View mSettingsButton; + protected View mDismissButton; + + protected PipMenuIconsAlgorithm(Context context) { + } + + /** + * Bind the necessary views. + */ + public void bindViews(ViewGroup viewRoot, ViewGroup topEndContainer, View dragHandle, + View enterSplitButton, View settingsButton, View dismissButton) { + mViewRoot = viewRoot; + mTopEndContainer = topEndContainer; + mDragHandle = dragHandle; + mEnterSplitButton = enterSplitButton; + mSettingsButton = settingsButton; + mDismissButton = dismissButton; + } + + /** + * Updates the position of the drag handle based on where the PIP window is on the screen. + */ + public void onBoundsChanged(Rect bounds) { + // On phones, the menu icons are always static and will never move based on the PIP window + // position. No need to do anything here. + } + + /** + * Set the gravity on the given view. + */ + protected static void setLayoutGravity(View v, int gravity) { + if (v.getLayoutParams() instanceof FrameLayout.LayoutParams) { + FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) v.getLayoutParams(); + params.gravity = gravity; + v.setLayoutParams(params); + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuView.java new file mode 100644 index 000000000000..a5b76c7df20b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuView.java @@ -0,0 +1,630 @@ +/* + * Copyright (C) 2020 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.wm.shell.pip2.phone; + +import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK; +import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; +import static android.provider.Settings.ACTION_PICTURE_IN_PICTURE_SETTINGS; +import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_CONTROLS; +import static android.view.accessibility.AccessibilityManager.FLAG_CONTENT_ICONS; +import static android.view.accessibility.AccessibilityNodeInfo.ACTION_CLICK; + +import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_FULL; +import static com.android.wm.shell.pip.phone.PhonePipMenuController.MENU_STATE_NONE; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.ActivityManager; +import android.app.PendingIntent; +import android.app.RemoteAction; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.graphics.Color; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; +import android.graphics.drawable.Icon; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.UserHandle; +import android.util.Pair; +import android.util.Size; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityNodeInfo; +import android.widget.FrameLayout; +import android.widget.LinearLayout; + +import com.android.internal.protolog.common.ProtoLog; +import com.android.wm.shell.R; +import com.android.wm.shell.animation.Interpolators; +import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.common.pip.PipUiEventLogger; +import com.android.wm.shell.common.pip.PipUtils; +import com.android.wm.shell.protolog.ShellProtoLogGroup; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Translucent window that gets started on top of a task in PIP to allow the user to control it. + */ +public class PipMenuView extends FrameLayout { + + private static final String TAG = "PipMenuView"; + + private static final int ANIMATION_NONE_DURATION_MS = 0; + private static final int ANIMATION_HIDE_DURATION_MS = 125; + + /** No animation performed during menu hide. */ + public static final int ANIM_TYPE_NONE = 0; + /** Fade out the menu until it's invisible. Used when the PIP window remains visible. */ + public static final int ANIM_TYPE_HIDE = 1; + /** Fade out the menu in sync with the PIP window. */ + public static final int ANIM_TYPE_DISMISS = 2; + + @IntDef(prefix = { "ANIM_TYPE_" }, value = { + ANIM_TYPE_NONE, + ANIM_TYPE_HIDE, + ANIM_TYPE_DISMISS + }) + @Retention(RetentionPolicy.SOURCE) + public @interface AnimationType {} + + private static final int INITIAL_DISMISS_DELAY = 3500; + private static final int POST_INTERACTION_DISMISS_DELAY = 2000; + private static final long MENU_SHOW_ON_EXPAND_START_DELAY = 30; + + private static final float MENU_BACKGROUND_ALPHA = 0.54f; + private static final float DISABLED_ACTION_ALPHA = 0.54f; + + private int mMenuState; + private boolean mAllowMenuTimeout = true; + private boolean mAllowTouches = true; + private int mDismissFadeOutDurationMs; + private final List<RemoteAction> mActions = new ArrayList<>(); + private RemoteAction mCloseAction; + + private AccessibilityManager mAccessibilityManager; + private Drawable mBackgroundDrawable; + private View mMenuContainer; + private LinearLayout mActionsGroup; + private int mBetweenActionPaddingLand; + + private AnimatorSet mMenuContainerAnimator; + private final PhonePipMenuController mController; + private final PipUiEventLogger mPipUiEventLogger; + + private ValueAnimator.AnimatorUpdateListener mMenuBgUpdateListener = + new ValueAnimator.AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(ValueAnimator animation) { + final float alpha = (float) animation.getAnimatedValue(); + mBackgroundDrawable.setAlpha((int) (MENU_BACKGROUND_ALPHA * alpha * 255)); + } + }; + + private ShellExecutor mMainExecutor; + private Handler mMainHandler; + + /** + * Whether the most recent showing of the menu caused a PIP resize, such as when PIP is too + * small and it is resized on menu show to fit the actions. + */ + private boolean mDidLastShowMenuResize; + private final Runnable mHideMenuRunnable = this::hideMenu; + + protected View mViewRoot; + protected View mSettingsButton; + protected View mDismissButton; + protected View mEnterSplitButton; + protected View mTopEndContainer; + protected PipMenuIconsAlgorithm mPipMenuIconsAlgorithm; + + // How long the shell will wait for the app to close the PiP if a custom action is set. + private final int mPipForceCloseDelay; + + public PipMenuView(Context context, PhonePipMenuController controller, + ShellExecutor mainExecutor, Handler mainHandler, PipUiEventLogger pipUiEventLogger) { + super(context, null, 0); + mContext = context; + mController = controller; + mMainExecutor = mainExecutor; + mMainHandler = mainHandler; + mPipUiEventLogger = pipUiEventLogger; + + mAccessibilityManager = context.getSystemService(AccessibilityManager.class); + inflate(context, R.layout.pip_menu, this); + + mPipForceCloseDelay = context.getResources().getInteger( + R.integer.config_pipForceCloseDelay); + + mBackgroundDrawable = mContext.getDrawable(R.drawable.pip_menu_background); + mBackgroundDrawable.setAlpha(0); + mViewRoot = findViewById(R.id.background); + mViewRoot.setBackground(mBackgroundDrawable); + mMenuContainer = findViewById(R.id.menu_container); + mMenuContainer.setAlpha(0); + mTopEndContainer = findViewById(R.id.top_end_container); + mSettingsButton = findViewById(R.id.settings); + mSettingsButton.setAlpha(0); + mSettingsButton.setOnClickListener((v) -> { + if (v.getAlpha() != 0) { + showSettings(); + } + }); + mDismissButton = findViewById(R.id.dismiss); + mDismissButton.setAlpha(0); + mDismissButton.setOnClickListener(v -> dismissPip()); + findViewById(R.id.expand_button).setOnClickListener(v -> { + if (mMenuContainer.getAlpha() != 0) { + expandPip(); + } + }); + + mEnterSplitButton = findViewById(R.id.enter_split); + mEnterSplitButton.setAlpha(0); + mEnterSplitButton.setOnClickListener(v -> { + if (mEnterSplitButton.getAlpha() != 0) { + enterSplit(); + } + }); + + // this disables the ripples + mEnterSplitButton.setEnabled(false); + + findViewById(R.id.resize_handle).setAlpha(0); + + mActionsGroup = findViewById(R.id.actions_group); + mBetweenActionPaddingLand = getResources().getDimensionPixelSize( + R.dimen.pip_between_action_padding_land); + mPipMenuIconsAlgorithm = new PipMenuIconsAlgorithm(mContext); + mPipMenuIconsAlgorithm.bindViews((ViewGroup) mViewRoot, (ViewGroup) mTopEndContainer, + findViewById(R.id.resize_handle), mEnterSplitButton, mSettingsButton, + mDismissButton); + mDismissFadeOutDurationMs = context.getResources() + .getInteger(R.integer.config_pipExitAnimationDuration); + + initAccessibility(); + } + + private void initAccessibility() { + this.setAccessibilityDelegate(new AccessibilityDelegate() { + @Override + public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { + super.onInitializeAccessibilityNodeInfo(host, info); + String label = getResources().getString(R.string.pip_menu_title); + info.addAction(new AccessibilityNodeInfo.AccessibilityAction(ACTION_CLICK, label)); + } + + @Override + public boolean performAccessibilityAction(View host, int action, Bundle args) { + if (action == ACTION_CLICK && mMenuState != MENU_STATE_FULL) { + mController.showMenu(); + } + return super.performAccessibilityAction(host, action, args); + } + }); + } + + @Override + public boolean onKeyUp(int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_ESCAPE) { + hideMenu(); + return true; + } + return super.onKeyUp(keyCode, event); + } + + @Override + public boolean shouldDelayChildPressedState() { + return true; + } + + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + if (!mAllowTouches) { + return false; + } + + if (mAllowMenuTimeout) { + repostDelayedHide(POST_INTERACTION_DISMISS_DELAY); + } + + return super.dispatchTouchEvent(ev); + } + + @Override + public boolean dispatchGenericMotionEvent(MotionEvent event) { + if (mAllowMenuTimeout) { + repostDelayedHide(POST_INTERACTION_DISMISS_DELAY); + } + + return super.dispatchGenericMotionEvent(event); + } + + void onFocusTaskChanged(ActivityManager.RunningTaskInfo taskInfo) {} + + void showMenu(int menuState, Rect stackBounds, boolean allowMenuTimeout, + boolean resizeMenuOnShow, boolean withDelay, boolean showResizeHandle) { + mAllowMenuTimeout = allowMenuTimeout; + mDidLastShowMenuResize = resizeMenuOnShow; + final boolean enableEnterSplit = + mContext.getResources().getBoolean(R.bool.config_pipEnableEnterSplitButton); + if (mMenuState != menuState) { + // Disallow touches if the menu needs to resize while showing, and we are transitioning + // to/from a full menu state. + boolean disallowTouchesUntilAnimationEnd = resizeMenuOnShow + && (mMenuState == MENU_STATE_FULL || menuState == MENU_STATE_FULL); + mAllowTouches = !disallowTouchesUntilAnimationEnd; + cancelDelayedHide(); + if (mMenuContainerAnimator != null) { + mMenuContainerAnimator.cancel(); + } + mMenuContainerAnimator = new AnimatorSet(); + ObjectAnimator menuAnim = ObjectAnimator.ofFloat(mMenuContainer, View.ALPHA, + mMenuContainer.getAlpha(), 1f); + menuAnim.addUpdateListener(mMenuBgUpdateListener); + ObjectAnimator settingsAnim = ObjectAnimator.ofFloat(mSettingsButton, View.ALPHA, + mSettingsButton.getAlpha(), 1f); + ObjectAnimator dismissAnim = ObjectAnimator.ofFloat(mDismissButton, View.ALPHA, + mDismissButton.getAlpha(), 1f); + if (menuState == MENU_STATE_FULL) { + mMenuContainerAnimator.playTogether(menuAnim, settingsAnim, dismissAnim); + } + mMenuContainerAnimator.setInterpolator(Interpolators.ALPHA_IN); + mMenuContainerAnimator.setDuration(ANIMATION_HIDE_DURATION_MS); + mMenuContainerAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + mAllowTouches = true; + notifyMenuStateChangeFinish(menuState); + if (allowMenuTimeout) { + repostDelayedHide(INITIAL_DISMISS_DELAY); + } + } + + @Override + public void onAnimationCancel(Animator animation) { + mAllowTouches = true; + } + }); + if (withDelay) { + // starts the menu container animation after window expansion is completed + notifyMenuStateChangeStart(menuState, resizeMenuOnShow, () -> { + if (mMenuContainerAnimator == null) { + return; + } + mMenuContainerAnimator.setStartDelay(MENU_SHOW_ON_EXPAND_START_DELAY); + setVisibility(VISIBLE); + mMenuContainerAnimator.start(); + }); + } else { + notifyMenuStateChangeStart(menuState, resizeMenuOnShow, null); + setVisibility(VISIBLE); + mMenuContainerAnimator.start(); + } + updateActionViews(menuState, stackBounds); + } else { + // If we are already visible, then just start the delayed dismiss and unregister any + // existing input consumers from the previous drag + if (allowMenuTimeout) { + repostDelayedHide(POST_INTERACTION_DISMISS_DELAY); + } + } + } + + /** + * Different from {@link #hideMenu()}, this function does not try to finish this menu activity + * and instead, it fades out the controls by setting the alpha to 0 directly without menu + * visibility callbacks invoked. + */ + void fadeOutMenu() { + mMenuContainer.setAlpha(0f); + mSettingsButton.setAlpha(0f); + mDismissButton.setAlpha(0f); + mEnterSplitButton.setAlpha(0f); + } + + void pokeMenu() { + cancelDelayedHide(); + } + + void updateMenuLayout(Rect bounds) { + mPipMenuIconsAlgorithm.onBoundsChanged(bounds); + } + + void hideMenu() { + hideMenu(null); + } + + void hideMenu(Runnable animationEndCallback) { + hideMenu(animationEndCallback, true /* notifyMenuVisibility */, mDidLastShowMenuResize, + ANIM_TYPE_HIDE); + } + + void hideMenu(boolean resize, @AnimationType int animationType) { + hideMenu(null /* animationFinishedRunnable */, true /* notifyMenuVisibility */, resize, + animationType); + } + + void hideMenu(final Runnable animationFinishedRunnable, boolean notifyMenuVisibility, + boolean resize, @AnimationType int animationType) { + if (mMenuState != MENU_STATE_NONE) { + cancelDelayedHide(); + if (notifyMenuVisibility) { + notifyMenuStateChangeStart(MENU_STATE_NONE, resize, null); + } + mMenuContainerAnimator = new AnimatorSet(); + ObjectAnimator menuAnim = ObjectAnimator.ofFloat(mMenuContainer, View.ALPHA, + mMenuContainer.getAlpha(), 0f); + menuAnim.addUpdateListener(mMenuBgUpdateListener); + ObjectAnimator settingsAnim = ObjectAnimator.ofFloat(mSettingsButton, View.ALPHA, + mSettingsButton.getAlpha(), 0f); + ObjectAnimator dismissAnim = ObjectAnimator.ofFloat(mDismissButton, View.ALPHA, + mDismissButton.getAlpha(), 0f); + ObjectAnimator enterSplitAnim = ObjectAnimator.ofFloat(mEnterSplitButton, View.ALPHA, + mEnterSplitButton.getAlpha(), 0f); + mMenuContainerAnimator.playTogether(menuAnim, settingsAnim, dismissAnim, + enterSplitAnim); + mMenuContainerAnimator.setInterpolator(Interpolators.ALPHA_OUT); + mMenuContainerAnimator.setDuration(getFadeOutDuration(animationType)); + mMenuContainerAnimator.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + setVisibility(GONE); + if (notifyMenuVisibility) { + notifyMenuStateChangeFinish(MENU_STATE_NONE); + } + if (animationFinishedRunnable != null) { + animationFinishedRunnable.run(); + } + } + }); + mMenuContainerAnimator.start(); + } + } + + /** + * @return Estimated minimum {@link Size} to hold the actions. + * See also {@link #updateActionViews(Rect)} + */ + Size getEstimatedMinMenuSize() { + final int pipActionSize = getResources().getDimensionPixelSize(R.dimen.pip_action_size); + // the minimum width would be (2 * pipActionSize) since we have settings and dismiss button + // on the top action container. + final int width = Math.max(2, mActions.size()) * pipActionSize; + final int height = getResources().getDimensionPixelSize(R.dimen.pip_expand_action_size) + + getResources().getDimensionPixelSize(R.dimen.pip_action_padding) + + getResources().getDimensionPixelSize(R.dimen.pip_expand_container_edge_margin); + return new Size(width, height); + } + + void setActions(Rect stackBounds, @Nullable List<RemoteAction> actions, + @Nullable RemoteAction closeAction) { + mActions.clear(); + if (actions != null && !actions.isEmpty()) { + mActions.addAll(actions); + } + mCloseAction = closeAction; + if (mMenuState == MENU_STATE_FULL) { + updateActionViews(mMenuState, stackBounds); + } + } + + private void updateActionViews(int menuState, Rect stackBounds) { + ViewGroup expandContainer = findViewById(R.id.expand_container); + ViewGroup actionsContainer = findViewById(R.id.actions_container); + actionsContainer.setOnTouchListener((v, ev) -> { + // Do nothing, prevent click through to parent + return true; + }); + + // Update the expand button only if it should show with the menu + expandContainer.setVisibility(menuState == MENU_STATE_FULL + ? View.VISIBLE + : View.INVISIBLE); + + LayoutParams expandedLp = + (LayoutParams) expandContainer.getLayoutParams(); + if (mActions.isEmpty() || menuState == MENU_STATE_NONE) { + actionsContainer.setVisibility(View.INVISIBLE); + + // Update the expand container margin to adjust the center of the expand button to + // account for the existence of the action container + expandedLp.topMargin = 0; + expandedLp.bottomMargin = 0; + } else { + actionsContainer.setVisibility(View.VISIBLE); + if (mActionsGroup != null) { + // Ensure we have as many buttons as actions + final LayoutInflater inflater = LayoutInflater.from(mContext); + while (mActionsGroup.getChildCount() < mActions.size()) { + final PipMenuActionView actionView = (PipMenuActionView) inflater.inflate( + R.layout.pip_menu_action, mActionsGroup, false); + mActionsGroup.addView(actionView); + } + + // Update the visibility of all views + for (int i = 0; i < mActionsGroup.getChildCount(); i++) { + mActionsGroup.getChildAt(i).setVisibility(i < mActions.size() + ? View.VISIBLE + : View.GONE); + } + + // Recreate the layout + final boolean isLandscapePip = stackBounds != null + && (stackBounds.width() > stackBounds.height()); + for (int i = 0; i < mActions.size(); i++) { + final RemoteAction action = mActions.get(i); + final PipMenuActionView actionView = + (PipMenuActionView) mActionsGroup.getChildAt(i); + final boolean isCloseAction = mCloseAction != null && Objects.equals( + mCloseAction.getActionIntent(), action.getActionIntent()); + + final int iconType = action.getIcon().getType(); + if (iconType == Icon.TYPE_URI || iconType == Icon.TYPE_URI_ADAPTIVE_BITMAP) { + // Disallow loading icon from content URI + actionView.setImageDrawable(null); + } else { + // TODO: Check if the action drawable has changed before we reload it + action.getIcon().loadDrawableAsync(mContext, d -> { + if (d != null) { + d.setTint(Color.WHITE); + actionView.setImageDrawable(d); + } + }, mMainHandler); + } + actionView.setCustomCloseBackgroundVisibility( + isCloseAction ? View.VISIBLE : View.GONE); + actionView.setContentDescription(action.getContentDescription()); + if (action.isEnabled()) { + actionView.setOnClickListener( + v -> onActionViewClicked(action.getActionIntent(), isCloseAction)); + } + actionView.setEnabled(action.isEnabled()); + actionView.setAlpha(action.isEnabled() ? 1f : DISABLED_ACTION_ALPHA); + + // Update the margin between actions + LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) + actionView.getLayoutParams(); + lp.leftMargin = (isLandscapePip && i > 0) ? mBetweenActionPaddingLand : 0; + } + } + + // Update the expand container margin to adjust the center of the expand button to + // account for the existence of the action container + expandedLp.topMargin = getResources().getDimensionPixelSize( + R.dimen.pip_action_padding); + expandedLp.bottomMargin = getResources().getDimensionPixelSize( + R.dimen.pip_expand_container_edge_margin); + } + expandContainer.requestLayout(); + } + + private void notifyMenuStateChangeStart(int menuState, boolean resize, Runnable callback) { + mController.onMenuStateChangeStart(menuState, resize, callback); + } + + private void notifyMenuStateChangeFinish(int menuState) { + mMenuState = menuState; + mController.onMenuStateChangeFinish(menuState); + } + + private void expandPip() { + // Do not notify menu visibility when hiding the menu, the controller will do this when it + // handles the message + hideMenu(mController::onPipExpand, false /* notifyMenuVisibility */, true /* resize */, + ANIM_TYPE_HIDE); + mPipUiEventLogger.log( + PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_EXPAND_TO_FULLSCREEN); + } + + private void dismissPip() { + if (mMenuState != MENU_STATE_NONE) { + // Do not call hideMenu() directly. Instead, let the menu controller handle it just as + // any other dismissal that will update the touch state and fade out the PIP task + // and the menu view at the same time. + mController.onPipDismiss(); + mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_TAP_TO_REMOVE); + } + } + + /** + * Execute the {@link PendingIntent} attached to the {@link PipMenuActionView}. + * If the given {@link PendingIntent} matches {@link #mCloseAction}, we need to make sure + * the PiP is removed after a certain timeout in case the app does not respond in a + * timely manner. + */ + private void onActionViewClicked(@NonNull PendingIntent intent, boolean isCloseAction) { + try { + intent.send(); + } catch (PendingIntent.CanceledException e) { + ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "%s: Failed to send action, %s", TAG, e); + } + if (isCloseAction) { + mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_CUSTOM_CLOSE); + mAllowTouches = false; + mMainExecutor.executeDelayed(() -> { + hideMenu(); + // TODO: it's unsafe to call onPipDismiss with a delay here since + // we may have a different PiP by the time this runnable is executed. + mController.onPipDismiss(); + mAllowTouches = true; + }, mPipForceCloseDelay); + } + } + + private void enterSplit() { + // Do not notify menu visibility when hiding the menu, the controller will do this when it + // handles the message + hideMenu(mController::onEnterSplit, false /* notifyMenuVisibility */, true /* resize */, + ANIM_TYPE_HIDE); + } + + + private void showSettings() { + final Pair<ComponentName, Integer> topPipActivityInfo = + PipUtils.getTopPipActivity(mContext); + if (topPipActivityInfo.first != null) { + final Intent settingsIntent = new Intent(ACTION_PICTURE_IN_PICTURE_SETTINGS, + Uri.fromParts("package", topPipActivityInfo.first.getPackageName(), null)); + settingsIntent.setFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TASK); + mContext.startActivityAsUser(settingsIntent, UserHandle.of(topPipActivityInfo.second)); + mPipUiEventLogger.log(PipUiEventLogger.PipUiEventEnum.PICTURE_IN_PICTURE_SHOW_SETTINGS); + } + } + + private void cancelDelayedHide() { + mMainExecutor.removeCallbacks(mHideMenuRunnable); + } + + private void repostDelayedHide(int delay) { + int recommendedTimeout = mAccessibilityManager.getRecommendedTimeoutMillis(delay, + FLAG_CONTENT_ICONS | FLAG_CONTENT_CONTROLS); + mMainExecutor.removeCallbacks(mHideMenuRunnable); + mMainExecutor.executeDelayed(mHideMenuRunnable, recommendedTimeout); + } + + private long getFadeOutDuration(@AnimationType int animationType) { + switch (animationType) { + case ANIM_TYPE_NONE: + return ANIMATION_NONE_DURATION_MS; + case ANIM_TYPE_HIDE: + return ANIMATION_HIDE_DURATION_MS; + case ANIM_TYPE_DISMISS: + return mDismissFadeOutDurationMs; + default: + throw new IllegalStateException("Invalid animation type " + animationType); + } + } +} 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 f3d178aef4ea..fbf4d13a0c19 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 @@ -42,8 +42,8 @@ import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; +import com.android.wm.shell.common.pip.PipMenuController; import com.android.wm.shell.common.pip.PipUtils; -import com.android.wm.shell.pip.PipMenuController; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java index e6faa6391cca..96eaa1edbae4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java @@ -287,7 +287,7 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL } boolean isHandlingDragResize() { - return mDragResizeListener.isHandlingDragResize(); + return mDragResizeListener != null && mDragResizeListener.isHandlingDragResize(); } private void closeDragResizeListener() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index 4ba05ce8aef1..a8b39c41e6bb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -345,7 +345,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { mTaskOperations.injectBackKey(); } else if (id == R.id.caption_handle || id == R.id.open_menu_button) { if (!decoration.isHandleMenuActive()) { - moveTaskToFront(mTaskOrganizer.getRunningTaskInfo(mTaskId)); + moveTaskToFront(decoration.mTaskInfo); decoration.createHandleMenu(); } else { decoration.closeHandleMenu(); @@ -419,10 +419,10 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { && id != R.id.maximize_window) { return false; } - moveTaskToFront(mTaskOrganizer.getRunningTaskInfo(mTaskId)); + final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); + moveTaskToFront(decoration.mTaskInfo); if (!mHasLongClicked && id != R.id.maximize_window) { - final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); decoration.closeMaximizeMenuIfNeeded(e); } @@ -466,7 +466,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { */ @Override public boolean handleMotionEvent(@Nullable View v, MotionEvent e) { - final RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId); + final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); + final RunningTaskInfo taskInfo = decoration.mTaskInfo; if (DesktopModeStatus.isEnabled() && taskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) { return false; @@ -492,8 +493,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { } case MotionEvent.ACTION_MOVE: { mShouldClick = false; - final DesktopModeWindowDecoration decoration = - mWindowDecorByTaskId.get(mTaskId); // If a decor's resize drag zone is active, don't also try to reposition it. if (decoration.isHandlingDragResize()) break; decoration.closeMaximizeMenu(); @@ -557,9 +556,10 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { && action != MotionEvent.ACTION_CANCEL)) { return false; } - final RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId); - mDesktopTasksController.ifPresent(c -> c.toggleDesktopTaskSize(taskInfo, - mWindowDecorByTaskId.get(taskInfo.taskId))); + mDesktopTasksController.ifPresent(c -> { + final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); + c.toggleDesktopTaskSize(decoration.mTaskInfo, decoration); + }); return true; } } @@ -843,7 +843,18 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel { @Nullable private DesktopModeWindowDecoration getRelevantWindowDecor(MotionEvent ev) { - if (mSplitScreenController != null && mSplitScreenController.isSplitScreenVisible()) { + final DesktopModeWindowDecoration focusedDecor = getFocusedDecor(); + if (focusedDecor == null) { + return null; + } + final boolean splitScreenVisible = mSplitScreenController != null + && mSplitScreenController.isSplitScreenVisible(); + // It's possible that split tasks are visible but neither is focused, such as when there's + // a fullscreen translucent window on top of them. In that case, the relevant decor should + // just be that translucent focused window. + final boolean focusedTaskInSplit = mSplitScreenController != null + && mSplitScreenController.isTaskInSplitScreen(focusedDecor.mTaskInfo.taskId); + if (splitScreenVisible && focusedTaskInSplit) { // We can't look at focused task here as only one task will have focus. DesktopModeWindowDecoration splitTaskDecor = getSplitScreenDecor(ev); return splitTaskDecor == null ? getFocusedDecor() : splitTaskDecor; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index 20233331997f..3f0a28118597 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -36,7 +36,6 @@ import android.graphics.Rect; import android.graphics.Region; import android.graphics.drawable.Drawable; import android.os.Handler; -import android.util.Log; import android.view.Choreographer; import android.view.MotionEvent; import android.view.SurfaceControl; @@ -388,27 +387,20 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } boolean isHandlingDragResize() { - return mDragResizeListener.isHandlingDragResize(); + return mDragResizeListener != null && mDragResizeListener.isHandlingDragResize(); } private void loadAppInfo() { - String packageName = mTaskInfo.realActivity.getPackageName(); PackageManager pm = mContext.getApplicationContext().getPackageManager(); - try { - final IconProvider provider = new IconProvider(mContext); - mAppIconDrawable = provider.getIcon(pm.getActivityInfo(mTaskInfo.baseActivity, - PackageManager.ComponentInfoFlags.of(0))); - final Resources resources = mContext.getResources(); - final BaseIconFactory factory = new BaseIconFactory(mContext, - resources.getDisplayMetrics().densityDpi, - resources.getDimensionPixelSize(R.dimen.desktop_mode_caption_icon_radius)); - mAppIconBitmap = factory.createScaledBitmap(mAppIconDrawable, MODE_DEFAULT); - final ApplicationInfo applicationInfo = pm.getApplicationInfo(packageName, - PackageManager.ApplicationInfoFlags.of(0)); - mAppName = pm.getApplicationLabel(applicationInfo); - } catch (PackageManager.NameNotFoundException e) { - Log.w(TAG, "Package not found: " + packageName, e); - } + final IconProvider provider = new IconProvider(mContext); + mAppIconDrawable = provider.getIcon(mTaskInfo.topActivityInfo); + final Resources resources = mContext.getResources(); + final BaseIconFactory factory = new BaseIconFactory(mContext, + resources.getDisplayMetrics().densityDpi, + resources.getDimensionPixelSize(R.dimen.desktop_mode_caption_icon_radius)); + mAppIconBitmap = factory.createScaledBitmap(mAppIconDrawable, MODE_DEFAULT); + final ApplicationInfo applicationInfo = mTaskInfo.topActivityInfo.applicationInfo; + mAppName = pm.getApplicationLabel(applicationInfo); } private void closeDragResizeListener() { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java index 8b38f991a2db..d902621180d8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java @@ -353,6 +353,7 @@ class DragResizeInputListener implements AutoCloseable { private boolean mShouldHandleEvents; private int mLastCursorType = PointerIcon.TYPE_DEFAULT; private Rect mDragStartTaskBounds; + private final Rect mTmpRect = new Rect(); private TaskResizeInputEventReceiver( InputChannel inputChannel, Handler handler, Choreographer choreographer) { @@ -477,14 +478,15 @@ class DragResizeInputListener implements AutoCloseable { } private void updateInputSinkRegionForDrag(Rect taskBounds) { + mTmpRect.set(taskBounds); final DisplayLayout layout = mDisplayController.getDisplayLayout(mDisplayId); final Region dragTouchRegion = new Region(-taskBounds.left, -taskBounds.top, -taskBounds.left + layout.width(), -taskBounds.top + layout.height()); // Remove the localized task bounds from the touch region. - taskBounds.offsetTo(0, 0); - dragTouchRegion.op(taskBounds, Region.Op.DIFFERENCE); + mTmpRect.offsetTo(0, 0); + dragTouchRegion.op(mTmpRect, Region.Op.DIFFERENCE); updateSinkInputChannel(dragTouchRegion); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java index 368231e2d7f0..b0d3b5090ef0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/ResizeVeil.java @@ -125,6 +125,7 @@ public class ResizeVeil { relayout(taskBounds, t); if (fadeIn) { + cancelAnimation(); mVeilAnimator = new ValueAnimator(); mVeilAnimator.setFloatValues(0f, 1f); mVeilAnimator.setDuration(RESIZE_ALPHA_DURATION); @@ -210,15 +211,16 @@ public class ResizeVeil { * Animate veil's alpha to 0, fading it out. */ public void hideVeil() { - final ValueAnimator animator = new ValueAnimator(); - animator.setFloatValues(1, 0); - animator.setDuration(RESIZE_ALPHA_DURATION); - animator.addUpdateListener(animation -> { + cancelAnimation(); + mVeilAnimator = new ValueAnimator(); + mVeilAnimator.setFloatValues(1, 0); + mVeilAnimator.setDuration(RESIZE_ALPHA_DURATION); + mVeilAnimator.addUpdateListener(animation -> { SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); - t.setAlpha(mVeilSurface, 1 - animator.getAnimatedFraction()); + t.setAlpha(mVeilSurface, 1 - mVeilAnimator.getAnimatedFraction()); t.apply(); }); - animator.addListener(new AnimatorListenerAdapter() { + mVeilAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); @@ -226,7 +228,7 @@ public class ResizeVeil { t.apply(); } }); - animator.start(); + mVeilAnimator.start(); } @ColorRes @@ -240,10 +242,20 @@ public class ResizeVeil { } } + private void cancelAnimation() { + if (mVeilAnimator != null) { + mVeilAnimator.removeAllUpdateListeners(); + mVeilAnimator.cancel(); + } + } + /** * Dispose of veil when it is no longer needed, likely on close of its container decor. */ void dispose() { + cancelAnimation(); + mVeilAnimator = null; + if (mViewHost != null) { mViewHost.release(); mViewHost = null; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt index 4878df806f82..75965d6d68d9 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt @@ -56,9 +56,7 @@ import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.whenever -/** - * Tests for loading / inflating views & icons for a bubble. - */ +/** Tests for loading / inflating views & icons for a bubble. */ @SmallTest @RunWith(AndroidTestingRunner::class) @RunWithLooper(setAsMainLooper = true) @@ -76,25 +74,33 @@ class BubbleViewInfoTest : ShellTestCase() { @Before fun setup() { metadataFlagListener = Bubbles.BubbleMetadataFlagListener {} - iconFactory = BubbleIconFactory(context, + iconFactory = + BubbleIconFactory( + context, 60, 30, Color.RED, - mContext.resources.getDimensionPixelSize( - R.dimen.importance_ring_stroke_width)) + mContext.resources.getDimensionPixelSize(R.dimen.importance_ring_stroke_width) + ) mainExecutor = TestShellExecutor() val windowManager = context.getSystemService(WindowManager::class.java) val shellInit = ShellInit(mainExecutor) val shellCommandHandler = ShellCommandHandler() - val shellController = ShellController(context, shellInit, shellCommandHandler, - mainExecutor) + val shellController = ShellController(context, shellInit, shellCommandHandler, mainExecutor) val bubblePositioner = BubblePositioner(context, windowManager) - val bubbleData = BubbleData(context, mock<BubbleLogger>(), bubblePositioner, - BubbleEducationController(context), mainExecutor) + val bubbleData = + BubbleData( + context, + mock<BubbleLogger>(), + bubblePositioner, + BubbleEducationController(context), + mainExecutor + ) val surfaceSynchronizer = { obj: Runnable -> obj.run() } - bubbleController = BubbleController( + bubbleController = + BubbleController( context, shellInit, shellCommandHandler, @@ -122,18 +128,36 @@ class BubbleViewInfoTest : ShellTestCase() { mock<Transitions>(), mock<SyncTransactionQueue>(), mock<IWindowManager>(), - mock<BubbleProperties>()) + mock<BubbleProperties>() + ) - bubbleStackView = BubbleStackView(context, bubbleController, bubbleData, - surfaceSynchronizer, FloatingContentCoordinator(), bubbleController, mainExecutor) + val bubbleStackViewManager = BubbleStackViewManager.fromBubbleController(bubbleController) + bubbleStackView = + BubbleStackView( + context, + bubbleStackViewManager, + bubblePositioner, + bubbleData, + surfaceSynchronizer, + FloatingContentCoordinator(), + bubbleController, + mainExecutor + ) bubbleBarLayerView = BubbleBarLayerView(context, bubbleController) } @Test fun testPopulate() { bubble = createBubbleWithShortcut() - val info = BubbleViewInfoTask.BubbleViewInfo.populate(context, - bubbleController, bubbleStackView, iconFactory, bubble, false /* skipInflation */) + val info = + BubbleViewInfoTask.BubbleViewInfo.populate( + context, + bubbleController, + bubbleStackView, + iconFactory, + bubble, + false /* skipInflation */ + ) assertThat(info!!).isNotNull() assertThat(info.imageView).isNotNull() @@ -151,9 +175,15 @@ class BubbleViewInfoTest : ShellTestCase() { @Test fun testPopulateForBubbleBar() { bubble = createBubbleWithShortcut() - val info = BubbleViewInfoTask.BubbleViewInfo.populateForBubbleBar(context, - bubbleController, bubbleBarLayerView, iconFactory, bubble, - false /* skipInflation */) + val info = + BubbleViewInfoTask.BubbleViewInfo.populateForBubbleBar( + context, + bubbleController, + bubbleBarLayerView, + iconFactory, + bubble, + false /* skipInflation */ + ) assertThat(info!!).isNotNull() assertThat(info.imageView).isNull() @@ -176,12 +206,18 @@ class BubbleViewInfoTest : ShellTestCase() { // exception here if the app has an issue loading the shortcut icon; we default to // the app icon in that case / none of the icons will be null. val mockIconFactory = mock<BubbleIconFactory>() - whenever(mockIconFactory.getBubbleDrawable(eq(context), eq(bubble.shortcutInfo), - any())).doThrow(RuntimeException()) + whenever(mockIconFactory.getBubbleDrawable(eq(context), eq(bubble.shortcutInfo), any())) + .doThrow(RuntimeException()) - val info = BubbleViewInfoTask.BubbleViewInfo.populateForBubbleBar(context, - bubbleController, bubbleBarLayerView, iconFactory, bubble, - true /* skipInflation */) + val info = + BubbleViewInfoTask.BubbleViewInfo.populateForBubbleBar( + context, + bubbleController, + bubbleBarLayerView, + iconFactory, + bubble, + true /* skipInflation */ + ) assertThat(info).isNotNull() assertThat(info?.shortcutInfo).isNotNull() @@ -194,8 +230,17 @@ class BubbleViewInfoTest : ShellTestCase() { private fun createBubbleWithShortcut(): Bubble { val shortcutInfo = ShortcutInfo.Builder(mContext, "mockShortcutId").build() - return Bubble("mockKey", shortcutInfo, 1000, Resources.ID_NULL, - "mockTitle", 0 /* taskId */, "mockLocus", true /* isDismissible */, - mainExecutor, metadataFlagListener) + return Bubble( + "mockKey", + shortcutInfo, + 1000, + Resources.ID_NULL, + "mockTitle", + 0 /* taskId */, + "mockLocus", + true /* isDismissible */, + mainExecutor, + metadataFlagListener + ) } -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt index 3fe78efdf2b1..445f74a52b0d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTaskRepositoryTest.kt @@ -124,7 +124,7 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { repo.addVisibleTasksListener(listener, executor) executor.flushAll() - assertThat(listener.hasVisibleTasksOnDefaultDisplay).isTrue() + assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(1) assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(1) } @@ -148,7 +148,7 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { repo.addVisibleTasksListener(listener, executor) executor.flushAll() - assertThat(listener.hasVisibleTasksOnDefaultDisplay).isFalse() + assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(0) // One call as adding listener notifies it assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(0) } @@ -162,8 +162,8 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 2, visible = true) executor.flushAll() - assertThat(listener.hasVisibleTasksOnDefaultDisplay).isTrue() - assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(1) + assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(2) + assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(2) } @Test @@ -175,16 +175,16 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true) executor.flushAll() - assertThat(listener.hasVisibleTasksOnDefaultDisplay).isTrue() + assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(1) assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(1) - assertThat(listener.hasVisibleTasksOnSecondaryDisplay).isFalse() + assertThat(listener.visibleTasksCountOnSecondaryDisplay).isEqualTo(0) assertThat(listener.visibleChangesOnSecondaryDisplay).isEqualTo(0) repo.updateVisibleFreeformTasks(displayId = 1, taskId = 2, visible = true) executor.flushAll() // Listener for secondary display is notified - assertThat(listener.hasVisibleTasksOnSecondaryDisplay).isTrue() + assertThat(listener.visibleTasksCountOnSecondaryDisplay).isEqualTo(1) assertThat(listener.visibleChangesOnSecondaryDisplay).isEqualTo(1) // No changes to listener for default display assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(1) @@ -198,7 +198,7 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = true) executor.flushAll() - assertThat(listener.hasVisibleTasksOnDefaultDisplay).isTrue() + assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(1) // Mark task 1 visible on secondary display repo.updateVisibleFreeformTasks(displayId = 1, taskId = 1, visible = true) @@ -208,11 +208,11 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { // 1 - visible task added // 2 - visible task removed assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(2) - assertThat(listener.hasVisibleTasksOnDefaultDisplay).isFalse() + assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(0) // Secondary display should have 1 call for visible task added assertThat(listener.visibleChangesOnSecondaryDisplay).isEqualTo(1) - assertThat(listener.hasVisibleTasksOnSecondaryDisplay).isTrue() + assertThat(listener.visibleTasksCountOnSecondaryDisplay).isEqualTo(1) } @Test @@ -224,17 +224,17 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 2, visible = true) executor.flushAll() - assertThat(listener.hasVisibleTasksOnDefaultDisplay).isTrue() + assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(2) repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 1, visible = false) executor.flushAll() - assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(1) + assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(3) repo.updateVisibleFreeformTasks(DEFAULT_DISPLAY, taskId = 2, visible = false) executor.flushAll() - assertThat(listener.hasVisibleTasksOnDefaultDisplay).isFalse() - assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(2) + assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(0) + assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(4) } @Test @@ -397,8 +397,8 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { } class TestVisibilityListener : DesktopModeTaskRepository.VisibleTasksListener { - var hasVisibleTasksOnDefaultDisplay = false - var hasVisibleTasksOnSecondaryDisplay = false + var visibleTasksCountOnDefaultDisplay = 0 + var visibleTasksCountOnSecondaryDisplay = 0 var visibleChangesOnDefaultDisplay = 0 var visibleChangesOnSecondaryDisplay = 0 @@ -409,14 +409,14 @@ class DesktopModeTaskRepositoryTest : ShellTestCase() { var stashedChangesOnDefaultDisplay = 0 var stashedChangesOnSecondaryDisplay = 0 - override fun onVisibilityChanged(displayId: Int, hasVisibleFreeformTasks: Boolean) { + override fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) { when (displayId) { DEFAULT_DISPLAY -> { - hasVisibleTasksOnDefaultDisplay = hasVisibleFreeformTasks + visibleTasksCountOnDefaultDisplay = visibleTasksCount visibleChangesOnDefaultDisplay++ } SECOND_DISPLAY -> { - hasVisibleTasksOnSecondaryDisplay = hasVisibleFreeformTasks + visibleTasksCountOnSecondaryDisplay = visibleTasksCount visibleChangesOnSecondaryDisplay++ } else -> fail("Visible task listener received unexpected display id: $displayId") diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java index 193f16da3e39..40e61dd95f51 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java @@ -27,6 +27,8 @@ import static org.mockito.Mockito.when; import android.app.ActivityManager; import android.content.ComponentName; +import android.content.pm.ActivityInfo; +import android.content.pm.ApplicationInfo; import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; @@ -202,6 +204,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { .setTaskDescriptionBuilder(taskDescriptionBuilder) .setVisible(visible) .build(); + taskInfo.topActivityInfo = new ActivityInfo(); + taskInfo.topActivityInfo.applicationInfo = new ApplicationInfo(); taskInfo.realActivity = new ComponentName("com.android.wm.shell.windowdecor", "DesktopModeWindowDecorationTests"); taskInfo.baseActivity = new ComponentName("com.android.wm.shell.windowdecor", diff --git a/location/api/current.txt b/location/api/current.txt index c55676bc1e78..c7954feb9d4f 100644 --- a/location/api/current.txt +++ b/location/api/current.txt @@ -682,6 +682,7 @@ package android.location.altitude { public final class AltitudeConverter { ctor public AltitudeConverter(); method @WorkerThread public void addMslAltitudeToLocation(@NonNull android.content.Context, @NonNull android.location.Location) throws java.io.IOException; + method @FlaggedApi(Flags.FLAG_GEOID_HEIGHTS_VIA_ALTITUDE_HAL) public boolean addMslAltitudeToLocation(@NonNull android.location.Location); } } diff --git a/location/java/android/location/altitude/AltitudeConverter.java b/location/java/android/location/altitude/AltitudeConverter.java index 6f8891216bed..461dafb91916 100644 --- a/location/java/android/location/altitude/AltitudeConverter.java +++ b/location/java/android/location/altitude/AltitudeConverter.java @@ -16,12 +16,14 @@ package android.location.altitude; +import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.WorkerThread; import android.content.Context; import android.frameworks.location.altitude.GetGeoidHeightRequest; import android.frameworks.location.altitude.GetGeoidHeightResponse; import android.location.Location; +import android.location.flags.Flags; import com.android.internal.location.altitude.GeoidMap; import com.android.internal.location.altitude.S2CellIdUtils; @@ -213,12 +215,12 @@ public final class AltitudeConverter { } /** - * Same as {@link #addMslAltitudeToLocation(Context, Location)} except that data will not be - * loaded from raw assets. Returns true if a Mean Sea Level altitude is added to the - * {@code location}; otherwise, returns false and leaves the {@code location} unchanged. - * - * @hide + * Same as {@link #addMslAltitudeToLocation(Context, Location)} except that this method can be + * called on the main thread as data will not be loaded from raw assets. Returns true if a Mean + * Sea Level altitude is added to the {@code location}; otherwise, returns false and leaves the + * {@code location} unchanged. */ + @FlaggedApi(Flags.FLAG_GEOID_HEIGHTS_VIA_ALTITUDE_HAL) public boolean addMslAltitudeToLocation(@NonNull Location location) { validate(location); MapParamsProto geoidHeightParams = GeoidMap.getGeoidHeightParams(); diff --git a/location/java/android/location/flags/gnss.aconfig b/location/java/android/location/flags/location.aconfig index 8c7c8716b2dc..a96fe47f2381 100644 --- a/location/java/android/location/flags/gnss.aconfig +++ b/location/java/android/location/flags/location.aconfig @@ -1,6 +1,20 @@ package: "android.location.flags" flag { + name: "fix_service_watcher" + namespace: "location" + description: "Enable null explicit services in ServiceWatcher" + bug: "311210517" +} + +flag { + name: "geoid_heights_via_altitude_hal" + namespace: "location" + description: "Flag for making geoid heights available via the Altitude HAL" + bug: "304375846" +} + +flag { name: "gnss_api_navic_l1" namespace: "location" description: "Flag for GNSS API for NavIC L1" diff --git a/media/java/android/media/MediaRecorder.java b/media/java/android/media/MediaRecorder.java index aa307485c32e..bdfa63010adc 100644 --- a/media/java/android/media/MediaRecorder.java +++ b/media/java/android/media/MediaRecorder.java @@ -65,7 +65,8 @@ import java.util.concurrent.Executor; * * <p>A common case of using MediaRecorder to record audio works as follows: * - * <pre>MediaRecorder recorder = new MediaRecorder(); + * <pre> + * MediaRecorder recorder = new MediaRecorder(context); * recorder.setAudioSource(MediaRecorder.AudioSource.MIC); * recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); * recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); diff --git a/media/java/android/media/metrics/EditingEndedEvent.java b/media/java/android/media/metrics/EditingEndedEvent.java index 72e6db8d987f..5ed8d40af63e 100644 --- a/media/java/android/media/metrics/EditingEndedEvent.java +++ b/media/java/android/media/metrics/EditingEndedEvent.java @@ -86,7 +86,10 @@ public final class EditingEndedEvent extends Event implements Parcelable { */ public static final int ERROR_CODE_IO_NO_PERMISSION = 8; - /** */ + /** + * Caused by failing to load data via cleartext HTTP, when the app's network security + * configuration does not permit it. + */ public static final int ERROR_CODE_IO_CLEARTEXT_NOT_PERMITTED = 9; /** Caused by reading data out of the data bounds. */ @@ -146,6 +149,9 @@ public final class EditingEndedEvent extends Event implements Parcelable { @Retention(java.lang.annotation.RetentionPolicy.SOURCE) public @interface ErrorCode {} + /** Special value for unknown {@linkplain #getTimeSinceCreatedMillis() time since creation}. */ + public static final int TIME_SINCE_CREATED_UNKNOWN = -1; + private final @ErrorCode int mErrorCode; @SuppressWarnings("HidingField") // Hiding field from superclass as for playback events. private final long mTimeSinceCreatedMillis; @@ -174,16 +180,16 @@ public final class EditingEndedEvent extends Event implements Parcelable { } /** - * Gets the elapsed time since creating of the editing session, in milliseconds, or -1 if - * unknown. + * Gets the elapsed time since creating of the editing session, in milliseconds, or {@link + * #TIME_SINCE_CREATED_UNKNOWN} if unknown. * - * @return The elapsed time since creating the editing session, in milliseconds, or -1 if - * unknown. + * @return The elapsed time since creating the editing session, in milliseconds, or {@link + * #TIME_SINCE_CREATED_UNKNOWN} if unknown. * @see LogSessionId * @see EditingSession */ @Override - @IntRange(from = -1) + @IntRange(from = TIME_SINCE_CREATED_UNKNOWN) public long getTimeSinceCreatedMillis() { return mTimeSinceCreatedMillis; } @@ -283,7 +289,7 @@ public final class EditingEndedEvent extends Event implements Parcelable { public Builder(@FinalState int finalState) { mFinalState = finalState; mErrorCode = ERROR_CODE_NONE; - mTimeSinceCreatedMillis = -1; + mTimeSinceCreatedMillis = TIME_SINCE_CREATED_UNKNOWN; mMetricsBundle = new Bundle(); } @@ -291,11 +297,11 @@ public final class EditingEndedEvent extends Event implements Parcelable { * Sets the elapsed time since creating the editing session, in milliseconds. * * @param timeSinceCreatedMillis The elapsed time since creating the editing session, in - * milliseconds, or -1 if the value is unknown. + * milliseconds, or {@link #TIME_SINCE_CREATED_UNKNOWN} if unknown. * @see #getTimeSinceCreatedMillis() */ public @NonNull Builder setTimeSinceCreatedMillis( - @IntRange(from = -1) long timeSinceCreatedMillis) { + @IntRange(from = TIME_SINCE_CREATED_UNKNOWN) long timeSinceCreatedMillis) { mTimeSinceCreatedMillis = timeSinceCreatedMillis; return this; } diff --git a/media/java/android/media/projection/IMediaProjection.aidl b/media/java/android/media/projection/IMediaProjection.aidl index 388b2c5ca6fe..2fb0af5557b5 100644 --- a/media/java/android/media/projection/IMediaProjection.aidl +++ b/media/java/android/media/projection/IMediaProjection.aidl @@ -18,6 +18,7 @@ package android.media.projection; import android.media.projection.IMediaProjectionCallback; import android.os.IBinder; +import android.app.ActivityOptions.LaunchCookie; /** {@hide} */ interface IMediaProjection { @@ -38,22 +39,22 @@ interface IMediaProjection { void unregisterCallback(IMediaProjectionCallback callback); /** - * Returns the {@link android.os.IBinder} identifying the task to record, or {@code null} if + * Returns the {@link LaunchCookie} identifying the task to record, or {@code null} if * there is none. */ @EnforcePermission("MANAGE_MEDIA_PROJECTION") @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + ".permission.MANAGE_MEDIA_PROJECTION)") - IBinder getLaunchCookie(); + LaunchCookie getLaunchCookie(); /** - * Updates the {@link android.os.IBinder} identifying the task to record, or {@code null} if + * Updates the {@link LaunchCookie} identifying the task to record, or {@code null} if * there is none. */ @EnforcePermission("MANAGE_MEDIA_PROJECTION") @JavaPassthrough(annotation = "@android.annotation.RequiresPermission(android.Manifest" + ".permission.MANAGE_MEDIA_PROJECTION)") - void setLaunchCookie(in IBinder launchCookie); + void setLaunchCookie(in LaunchCookie launchCookie); /** * Returns {@code true} if this token is still valid. A token is valid as long as the token diff --git a/media/java/android/media/projection/MediaProjectionInfo.java b/media/java/android/media/projection/MediaProjectionInfo.java index c82039297d6e..cd0763d4d459 100644 --- a/media/java/android/media/projection/MediaProjectionInfo.java +++ b/media/java/android/media/projection/MediaProjectionInfo.java @@ -16,7 +16,7 @@ package android.media.projection; -import android.os.IBinder; +import android.app.ActivityOptions.LaunchCookie; import android.os.Parcel; import android.os.Parcelable; import android.os.UserHandle; @@ -27,9 +27,9 @@ import java.util.Objects; public final class MediaProjectionInfo implements Parcelable { private final String mPackageName; private final UserHandle mUserHandle; - private final IBinder mLaunchCookie; + private final LaunchCookie mLaunchCookie; - public MediaProjectionInfo(String packageName, UserHandle handle, IBinder launchCookie) { + public MediaProjectionInfo(String packageName, UserHandle handle, LaunchCookie launchCookie) { mPackageName = packageName; mUserHandle = handle; mLaunchCookie = launchCookie; @@ -38,7 +38,7 @@ public final class MediaProjectionInfo implements Parcelable { public MediaProjectionInfo(Parcel in) { mPackageName = in.readString(); mUserHandle = UserHandle.readFromParcel(in); - mLaunchCookie = in.readStrongBinder(); + mLaunchCookie = LaunchCookie.readFromParcel(in); } public String getPackageName() { @@ -49,7 +49,7 @@ public final class MediaProjectionInfo implements Parcelable { return mUserHandle; } - public IBinder getLaunchCookie() { + public LaunchCookie getLaunchCookie() { return mLaunchCookie; } @@ -72,7 +72,7 @@ public final class MediaProjectionInfo implements Parcelable { public String toString() { return "MediaProjectionInfo{mPackageName=" + mPackageName + ", mUserHandle=" - + mUserHandle + ", mLaunchCookie" + + mUserHandle + ", mLaunchCookie=" + mLaunchCookie + "}"; } @@ -85,7 +85,7 @@ public final class MediaProjectionInfo implements Parcelable { public void writeToParcel(Parcel out, int flags) { out.writeString(mPackageName); UserHandle.writeToParcel(mUserHandle, out); - out.writeStrongBinder(mLaunchCookie); + LaunchCookie.writeToParcel(mLaunchCookie, out); } public static final @android.annotation.NonNull Parcelable.Creator<MediaProjectionInfo> CREATOR = diff --git a/media/java/android/media/projection/MediaProjectionManager.java b/media/java/android/media/projection/MediaProjectionManager.java index 9790d02025b7..e3290d604794 100644 --- a/media/java/android/media/projection/MediaProjectionManager.java +++ b/media/java/android/media/projection/MediaProjectionManager.java @@ -18,8 +18,11 @@ package android.media.projection; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SuppressLint; import android.annotation.SystemService; +import android.annotation.TestApi; import android.app.Activity; +import android.app.ActivityOptions.LaunchCookie; import android.content.ComponentName; import android.content.Context; import android.content.Intent; @@ -73,6 +76,9 @@ public final class MediaProjectionManager { /** @hide */ public static final String EXTRA_MEDIA_PROJECTION = "android.media.projection.extra.EXTRA_MEDIA_PROJECTION"; + /** @hide */ + public static final String EXTRA_LAUNCH_COOKIE = + "android.media.projection.extra.EXTRA_LAUNCH_COOKIE"; /** @hide */ public static final int TYPE_SCREEN_CAPTURE = 0; @@ -158,17 +164,29 @@ public final class MediaProjectionManager { */ @NonNull public Intent createScreenCaptureIntent(@NonNull MediaProjectionConfig config) { - Intent i = new Intent(); - final ComponentName mediaProjectionPermissionDialogComponent = - ComponentName.unflattenFromString(mContext.getResources() - .getString(com.android.internal.R.string - .config_mediaProjectionPermissionDialogComponent)); - i.setComponent(mediaProjectionPermissionDialogComponent); + Intent i = createScreenCaptureIntent(); i.putExtra(EXTRA_MEDIA_PROJECTION_CONFIG, config); return i; } /** + * Returns an intent similar to {@link #createScreenCaptureIntent()} that will enable screen + * recording of the task with the specified launch cookie. This method should only be used for + * testing. + * + * @param launchCookie the launch cookie corresponding to the task to record. + * @hide + */ + @SuppressLint("UnflaggedApi") + @TestApi + @NonNull + public Intent createScreenCaptureIntent(@Nullable LaunchCookie launchCookie) { + Intent i = createScreenCaptureIntent(); + i.putExtra(EXTRA_LAUNCH_COOKIE, launchCookie); + return i; + } + + /** * Retrieves the {@link MediaProjection} obtained from a successful screen * capture request. The result code and data from the request are provided by overriding * {@link Activity#onActivityResult(int, int, Intent) onActivityResult(int, int, Intent)}, diff --git a/media/java/android/media/tv/SignalingDataRequest.aidl b/media/java/android/media/tv/SignalingDataRequest.aidl new file mode 100644 index 000000000000..29e89fe4142b --- /dev/null +++ b/media/java/android/media/tv/SignalingDataRequest.aidl @@ -0,0 +1,3 @@ +package android.media.tv; + +parcelable SignalingDataRequest; diff --git a/media/java/android/media/tv/SignalingDataRequest.java b/media/java/android/media/tv/SignalingDataRequest.java new file mode 100644 index 000000000000..dcf1d48aaf3a --- /dev/null +++ b/media/java/android/media/tv/SignalingDataRequest.java @@ -0,0 +1,170 @@ +/* + * 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 android.media.tv; + +import android.annotation.NonNull; +import android.os.Parcelable; + +/** + * Request to retrieve the Low-level Signalling Tables (LLS) and Service-layer Signalling (SLS) + * metadata. + * + * <p>For more details on each type of metadata that can be requested, refer to the ATSC standard + * A/344:2023-5 9.2.10 - Query Signaling Data API. + * + * @hide + */ +public class SignalingDataRequest extends BroadcastInfoRequest implements Parcelable { + private static final @TvInputManager.BroadcastInfoType int REQUEST_TYPE = + TvInputManager.BROADCAST_INFO_TYPE_SIGNALING_DATA; + + public static final @NonNull Parcelable.Creator<SignalingDataRequest> CREATOR = + new Parcelable.Creator<SignalingDataRequest>() { + @Override + public SignalingDataRequest[] newArray(int size) { + return new SignalingDataRequest[size]; + } + + @Override + public SignalingDataRequest createFromParcel(@NonNull android.os.Parcel in) { + return new SignalingDataRequest(in); + } + }; + + /** SLS Metadata: All metadata objects for the requested service(s) */ + public static final int SLS_METADATA_ALL = 0x7FFFFFF; + + /** SLS Metadata: APD for the requested service(s) */ + public static final int SLS_METADATA_APD = 1; + + /** SLS Metadata: USBD for the requested service(s) */ + public static final int SLS_METADATA_USBD = 1 << 1; + + /** SLS Metadata: S-TSID for the requested service(s) */ + public static final int SLS_METADATA_STSID = 1 << 2; + + /** SLS Metadata: DASH MPD for the requested service(s) */ + public static final int SLS_METADATA_MPD = 1 << 3; + + /** SLS Metadata: User Service Description for MMTP */ + public static final int SLS_METADATA_USD = 1 << 4; + + /** SLS Metadata: MMT Package Access Table for the requested service(s) */ + public static final int SLS_METADATA_PAT = 1 << 5; + + /** SLS Metadata: MMT Package Table for the requested service(s) */ + public static final int SLS_METADATA_MPT = 1 << 6; + + /** SLS Metadata: MMT Media Presentation Information Table for the requested service(s) */ + public static final int SLS_METADATA_MPIT = 1 << 7; + + /** SLS Metadata: MMT Clock Relation Information for the requested service(s) */ + public static final int SLS_METADATA_CRIT = 1 << 8; + + /** SLS Metadata: MMT Device Capabilities Information Table for the requested service(s) */ + public static final int SLS_METADATA_DCIT = 1 << 9; + + /** SLS Metadata: HTML Entry Pages Location Description for the requested service(s) */ + public static final int SLS_METADATA_HELD = 1 << 10; + + /** SLS Metadata: Distribution Window Desciription for the requested service(s) */ + public static final int SLS_METADATA_DWD = 1 << 11; + + /** SLS Metadata: MMT Application Event Information for the requested service(s) */ + public static final int SLS_METADATA_AEI = 1 << 12; + + /** SLS Metadata: Video Stream Properties Descriptor */ + public static final int SLS_METADATA_VSPD = 1 << 13; + + /** SLS Metadata: ATSC Staggercast Descriptor */ + public static final int SLS_METADATA_ASD = 1 << 14; + + /** SLS Metadata: Inband Event Descriptor */ + public static final int SLS_METADATA_IED = 1 << 15; + + /** SLS Metadata: Caption Asset Descriptor */ + public static final int SLS_METADATA_CAD = 1 << 16; + + /** SLS Metadata: Audio Stream Properties Descriptor */ + public static final int SLS_METADATA_ASPD = 1 << 17; + + /** SLS Metadata: Security Properties Descriptor */ + public static final int SLS_METADATA_SSD = 1 << 18; + + /** SLS Metadata: ROUTE/DASH Application Dynamic Event for the requested service(s) */ + public static final int SLS_METADATA_EMSG = 1 << 19; + + /** SLS Metadata: MMT Application Dynamic Event for the requested service(s) */ + public static final int SLS_METADATA_EVTI = 1 << 20; + + /** Regional Service Availability Table for the requested service(s) */ + public static final int SLS_METADATA_RSAT = 1 << 21; + + private final int mGroup; + private @NonNull final int[] mLlsTableIds; + private final int mSlsMetadataTypes; + + SignalingDataRequest( + int requestId, + int option, + int group, + @NonNull int[] llsTableIds, + int slsMetadataTypes) { + super(REQUEST_TYPE, requestId, option); + mGroup = group; + mLlsTableIds = llsTableIds; + mSlsMetadataTypes = slsMetadataTypes; + } + + SignalingDataRequest(@NonNull android.os.Parcel in) { + super(REQUEST_TYPE, in); + + int group = in.readInt(); + int[] llsTableIds = in.createIntArray(); + int slsMetadataTypes = in.readInt(); + + this.mGroup = group; + this.mLlsTableIds = llsTableIds; + com.android.internal.util.AnnotationValidations.validate(NonNull.class, null, mLlsTableIds); + this.mSlsMetadataTypes = slsMetadataTypes; + } + + public int getGroup() { + return mGroup; + } + + public @NonNull int[] getLlsTableIds() { + return mLlsTableIds; + } + + public int getSlsMetadataTypes() { + return mSlsMetadataTypes; + } + + @Override + public void writeToParcel(@NonNull android.os.Parcel dest, int flags) { + super.writeToParcel(dest, flags); + dest.writeInt(mGroup); + dest.writeIntArray(mLlsTableIds); + dest.writeInt(mSlsMetadataTypes); + } + + @Override + public int describeContents() { + return 0; + } +} diff --git a/media/java/android/media/tv/TvInputManager.java b/media/java/android/media/tv/TvInputManager.java index 2b31bfef412e..be1b67547103 100644 --- a/media/java/android/media/tv/TvInputManager.java +++ b/media/java/android/media/tv/TvInputManager.java @@ -489,10 +489,19 @@ public final class TvInputManager { /** @hide */ @Retention(RetentionPolicy.SOURCE) - @IntDef(prefix = "BROADCAST_INFO_TYPE_", value = - {BROADCAST_INFO_TYPE_TS, BROADCAST_INFO_TYPE_TABLE, BROADCAST_INFO_TYPE_SECTION, - BROADCAST_INFO_TYPE_PES, BROADCAST_INFO_STREAM_EVENT, BROADCAST_INFO_TYPE_DSMCC, - BROADCAST_INFO_TYPE_COMMAND, BROADCAST_INFO_TYPE_TIMELINE}) + @IntDef( + prefix = "BROADCAST_INFO_TYPE_", + value = { + BROADCAST_INFO_TYPE_TS, + BROADCAST_INFO_TYPE_TABLE, + BROADCAST_INFO_TYPE_SECTION, + BROADCAST_INFO_TYPE_PES, + BROADCAST_INFO_STREAM_EVENT, + BROADCAST_INFO_TYPE_DSMCC, + BROADCAST_INFO_TYPE_COMMAND, + BROADCAST_INFO_TYPE_TIMELINE, + BROADCAST_INFO_TYPE_SIGNALING_DATA + }) public @interface BroadcastInfoType {} public static final int BROADCAST_INFO_TYPE_TS = 1; @@ -505,6 +514,9 @@ public final class TvInputManager { public static final int BROADCAST_INFO_TYPE_TIMELINE = 8; /** @hide */ + public static final int BROADCAST_INFO_TYPE_SIGNALING_DATA = 9; + + /** @hide */ @Retention(RetentionPolicy.SOURCE) @IntDef(prefix = "SIGNAL_STRENGTH_", value = {SIGNAL_STRENGTH_LOST, SIGNAL_STRENGTH_WEAK, SIGNAL_STRENGTH_STRONG}) diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl index 7b5853169923..1404d7c9841c 100644 --- a/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl +++ b/media/java/android/media/tv/interactive/ITvInteractiveAppClient.aidl @@ -63,6 +63,8 @@ oneway interface ITvInteractiveAppClient { void onRequestTvRecordingInfoList(in int type, int seq); void onRequestSigning(in String id, in String algorithm, in String alias, in byte[] data, int seq); + void onRequestSigning2(in String id, in String algorithm, in String host, + int port, in byte[] data, int seq); void onRequestCertificate(in String host, int port, int seq); void onAdRequest(in AdRequest request, int Seq); } diff --git a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl index cb89181fd714..3c91a3eeb1dc 100644 --- a/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl +++ b/media/java/android/media/tv/interactive/ITvInteractiveAppSessionCallback.aidl @@ -61,6 +61,7 @@ oneway interface ITvInteractiveAppSessionCallback { void onRequestTvRecordingInfo(in String recordingId); void onRequestTvRecordingInfoList(in int type); void onRequestSigning(in String id, in String algorithm, in String alias, in byte[] data); + void onRequestSigning2(in String id, in String algorithm, in String host, int port, in byte[] data); void onRequestCertificate(in String host, int port); void onAdRequest(in AdRequest request); } diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java index 011744f94edb..498eec604b9c 100755 --- a/media/java/android/media/tv/interactive/TvInteractiveAppManager.java +++ b/media/java/android/media/tv/interactive/TvInteractiveAppManager.java @@ -657,6 +657,19 @@ public final class TvInteractiveAppManager { } @Override + public void onRequestSigning2( + String id, String algorithm, String host, int port, byte[] data, int seq) { + synchronized (mSessionCallbackRecordMap) { + SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); + if (record == null) { + Log.e(TAG, "Callback not found for seq " + seq); + return; + } + record.postRequestSigning(id, algorithm, host, port, data); + } + } + + @Override public void onRequestCertificate(String host, int port, int seq) { synchronized (mSessionCallbackRecordMap) { SessionCallbackRecord record = mSessionCallbackRecordMap.get(seq); @@ -2258,6 +2271,17 @@ public final class TvInteractiveAppManager { }); } + void postRequestSigning(String id, String algorithm, String host, int port, + byte[] data) { + mHandler.post(new Runnable() { + @Override + public void run() { + mSessionCallback.onRequestSigning(mSession, id, algorithm, host, + port, data); + } + }); + } + void postRequestCertificate(String host, int port) { mHandler.post(new Runnable() { @Override @@ -2609,6 +2633,25 @@ public final class TvInteractiveAppManager { } /** + * This is called when + * {@link TvInteractiveAppService.Session#requestSigning(String, String, String, int, byte[])} is + * called. + * + * @param session A {@link TvInteractiveAppService.Session} associated with this callback. + * @param signingId the ID to identify the request. + * @param algorithm the standard name of the signature algorithm requested, such as + * MD5withRSA, SHA256withDSA, etc. + * @param host The host of the SSL CLient Authentication Server + * @param port The port of the SSL Client Authentication Server + * @param data the original bytes to be signed. + * @hide + */ + public void onRequestSigning( + Session session, String signingId, String algorithm, String host, + int port, byte[] data) { + } + + /** * This is called when the service requests a SSL certificate for client validation. * * @param session A {@link TvInteractiveAppService.Session} associated with this callback. diff --git a/media/java/android/media/tv/interactive/TvInteractiveAppService.java b/media/java/android/media/tv/interactive/TvInteractiveAppService.java index 054b272d820f..7b6dc38fe22c 100755 --- a/media/java/android/media/tv/interactive/TvInteractiveAppService.java +++ b/media/java/android/media/tv/interactive/TvInteractiveAppService.java @@ -1645,6 +1645,50 @@ public abstract class TvInteractiveAppService extends Service { } /** + * Requests signing of the given data. + * + * <p>This is used when the corresponding server of the broadcast-independent interactive + * app requires signing during handshaking, and the interactive app service doesn't have + * the built-in private key. The private key is provided by the content providers and + * pre-built in the related app, such as TV app. + * + * @param signingId the ID to identify the request. When a result is received, this ID can + * be used to correlate the result with the request. + * @param algorithm the standard name of the signature algorithm requested, such as + * MD5withRSA, SHA256withDSA, etc. The name is from standards like + * FIPS PUB 186-4 and PKCS #1. + * @param host the host of the SSL client authentication server. + * @param port the port of the SSL client authentication server. + * @param data the original bytes to be signed. + * + * @see #onSigningResult(String, byte[]) + * @see TvInteractiveAppView#createBiInteractiveApp(Uri, Bundle) + * @see TvInteractiveAppView#BI_INTERACTIVE_APP_KEY_ALIAS + * @hide + */ + @CallSuper + public void requestSigning(@NonNull String signingId, @NonNull String algorithm, + @NonNull String host, int port, @NonNull byte[] data) { + executeOrPostRunnableOnMainThread(new Runnable() { + @MainThread + @Override + public void run() { + try { + if (DEBUG) { + Log.d(TAG, "requestSigning"); + } + if (mSessionCallback != null) { + mSessionCallback.onRequestSigning2(signingId, algorithm, + host, port, data); + } + } catch (RemoteException e) { + Log.w(TAG, "error in requestSigning", e); + } + } + }); + } + + /** * Requests a SSL certificate for client validation. * * @param host the host name of the SSL authentication server. diff --git a/media/jni/soundpool/SoundDecoder.cpp b/media/jni/soundpool/SoundDecoder.cpp index 5ed10b0d785f..ae576342c6cc 100644 --- a/media/jni/soundpool/SoundDecoder.cpp +++ b/media/jni/soundpool/SoundDecoder.cpp @@ -29,14 +29,15 @@ static constexpr size_t kMaxQueueSize = 128; // before the SoundDecoder thread closes. static constexpr int32_t kWaitTimeBeforeCloseMs = 1000; -SoundDecoder::SoundDecoder(SoundManager* soundManager, size_t threads) +SoundDecoder::SoundDecoder(SoundManager* soundManager, size_t threads, int32_t threadPriority) : mSoundManager(soundManager) { ALOGV("%s(%p, %zu)", __func__, soundManager, threads); // ThreadPool is created, but we don't launch any threads. mThreadPool = std::make_unique<ThreadPool>( std::min(threads, (size_t)std::thread::hardware_concurrency()), - "SoundDecoder_"); + "SoundDecoder_", + threadPriority); } SoundDecoder::~SoundDecoder() diff --git a/media/jni/soundpool/SoundDecoder.h b/media/jni/soundpool/SoundDecoder.h index 7b62114483cf..3f44a0d977e1 100644 --- a/media/jni/soundpool/SoundDecoder.h +++ b/media/jni/soundpool/SoundDecoder.h @@ -28,7 +28,7 @@ namespace android::soundpool { */ class SoundDecoder { public: - SoundDecoder(SoundManager* soundManager, size_t threads); + SoundDecoder(SoundManager* soundManager, size_t threads, int32_t threadPriority); ~SoundDecoder(); void loadSound(int32_t soundID) NO_THREAD_SAFETY_ANALYSIS; // uses unique_lock void quit(); diff --git a/media/jni/soundpool/SoundManager.cpp b/media/jni/soundpool/SoundManager.cpp index 5b16174eef2b..fa35813391a6 100644 --- a/media/jni/soundpool/SoundManager.cpp +++ b/media/jni/soundpool/SoundManager.cpp @@ -29,7 +29,7 @@ namespace android::soundpool { static const size_t kDecoderThreads = std::thread::hardware_concurrency() >= 4 ? 2 : 1; SoundManager::SoundManager() - : mDecoder{std::make_unique<SoundDecoder>(this, kDecoderThreads)} + : mDecoder{std::make_unique<SoundDecoder>(this, kDecoderThreads, ANDROID_PRIORITY_NORMAL)} { ALOGV("%s()", __func__); } diff --git a/media/jni/soundpool/StreamManager.cpp b/media/jni/soundpool/StreamManager.cpp index 66fec1c528e7..e11ccbc0448b 100644 --- a/media/jni/soundpool/StreamManager.cpp +++ b/media/jni/soundpool/StreamManager.cpp @@ -126,7 +126,8 @@ StreamManager::StreamManager( mThreadPool = std::make_unique<ThreadPool>( std::min((size_t)streams, // do not make more threads than streams to play std::min(threads, (size_t)std::thread::hardware_concurrency())), - "SoundPool_"); + "SoundPool_", + ANDROID_PRIORITY_AUDIO); } #pragma clang diagnostic pop diff --git a/media/jni/soundpool/StreamManager.h b/media/jni/soundpool/StreamManager.h index 340b49bc6d6c..a4cb28663bda 100644 --- a/media/jni/soundpool/StreamManager.h +++ b/media/jni/soundpool/StreamManager.h @@ -46,9 +46,9 @@ namespace android::soundpool { */ class JavaThread { public: - JavaThread(std::function<void()> f, const char *name) + JavaThread(std::function<void()> f, const char *name, int32_t threadPriority) : mF{std::move(f)} { - createThreadEtc(staticFunction, this, name, ANDROID_PRIORITY_AUDIO); + createThreadEtc(staticFunction, this, name, threadPriority); } JavaThread(JavaThread &&) = delete; // uses "this" ptr, not moveable. @@ -109,9 +109,11 @@ private: */ class ThreadPool { public: - ThreadPool(size_t maxThreadCount, std::string name) + ThreadPool(size_t maxThreadCount, std::string name, + int32_t threadPriority = ANDROID_PRIORITY_NORMAL) : mMaxThreadCount(maxThreadCount) - , mName{std::move(name)} { } + , mName{std::move(name)} + , mThreadPriority(threadPriority) {} ~ThreadPool() { quit(); } @@ -159,7 +161,8 @@ public: const int32_t id = mNextThreadId; mThreads.emplace_back(std::make_unique<JavaThread>( [this, id, mf = std::move(f)] { mf(id); --mActiveThreadCount; }, - (mName + std::to_string(id)).c_str())); + (mName + std::to_string(id)).c_str(), + mThreadPriority)); ++mActiveThreadCount; return id; } @@ -180,6 +183,7 @@ public: private: const size_t mMaxThreadCount; const std::string mName; + const int32_t mThreadPriority; std::atomic_size_t mActiveThreadCount = 0; diff --git a/media/tests/projection/src/android/media/projection/FakeIMediaProjection.java b/media/tests/projection/src/android/media/projection/FakeIMediaProjection.java index 774de5fbbe3e..0df36afddf25 100644 --- a/media/tests/projection/src/android/media/projection/FakeIMediaProjection.java +++ b/media/tests/projection/src/android/media/projection/FakeIMediaProjection.java @@ -19,7 +19,7 @@ package android.media.projection; import static android.Manifest.permission.MANAGE_MEDIA_PROJECTION; import android.annotation.EnforcePermission; -import android.os.IBinder; +import android.app.ActivityOptions.LaunchCookie; import android.os.PermissionEnforcer; import android.os.RemoteException; @@ -29,7 +29,7 @@ import android.os.RemoteException; */ public final class FakeIMediaProjection extends IMediaProjection.Stub { boolean mIsStarted = false; - IBinder mLaunchCookie = null; + LaunchCookie mLaunchCookie = null; IMediaProjectionCallback mIMediaProjectionCallback = null; FakeIMediaProjection(PermissionEnforcer enforcer) { @@ -80,14 +80,14 @@ public final class FakeIMediaProjection extends IMediaProjection.Stub { @Override @EnforcePermission(MANAGE_MEDIA_PROJECTION) - public IBinder getLaunchCookie() throws RemoteException { + public LaunchCookie getLaunchCookie() throws RemoteException { getLaunchCookie_enforcePermission(); return mLaunchCookie; } @Override @EnforcePermission(MANAGE_MEDIA_PROJECTION) - public void setLaunchCookie(IBinder launchCookie) throws RemoteException { + public void setLaunchCookie(LaunchCookie launchCookie) throws RemoteException { setLaunchCookie_enforcePermission(); mLaunchCookie = launchCookie; } diff --git a/media/tests/projection/src/android/media/projection/MediaProjectionTest.java b/media/tests/projection/src/android/media/projection/MediaProjectionTest.java index 2e0396fc6119..1323e89202ee 100644 --- a/media/tests/projection/src/android/media/projection/MediaProjectionTest.java +++ b/media/tests/projection/src/android/media/projection/MediaProjectionTest.java @@ -31,15 +31,14 @@ import static org.junit.Assert.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.mock; import android.annotation.Nullable; +import android.app.ActivityOptions.LaunchCookie; import android.compat.testing.PlatformCompatChangeRule; import android.hardware.display.DisplayManager; import android.hardware.display.VirtualDisplay; import android.hardware.display.VirtualDisplayConfig; import android.os.Handler; -import android.os.IBinder; import android.os.Looper; import android.os.RemoteException; import android.os.test.FakePermissionEnforcer; @@ -117,7 +116,7 @@ public class MediaProjectionTest { permissionEnforcer.grant(MANAGE_MEDIA_PROJECTION); // Support the MediaProjection instance. mFakeIMediaProjection = new FakeIMediaProjection(permissionEnforcer); - mFakeIMediaProjection.setLaunchCookie(mock(IBinder.class)); + mFakeIMediaProjection.setLaunchCookie(new LaunchCookie()); mMediaProjection = new MediaProjection(mTestableContext, mFakeIMediaProjection, mDisplayManager); diff --git a/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml b/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml index 929756cdf9cc..b97e9927e0ed 100644 --- a/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml +++ b/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml @@ -17,6 +17,7 @@ android:id="@android:id/content" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:minWidth="@dimen/dropdown_touch_target_min_width" android:orientation="horizontal" android:layout_marginEnd="@dimen/dropdown_layout_horizontal_margin" android:elevation="3dp"> diff --git a/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml b/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml index 1fe5e0ed41f9..261154fe9792 100644 --- a/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml +++ b/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml @@ -17,6 +17,7 @@ android:id="@android:id/content" android:layout_width="wrap_content" android:layout_height="wrap_content" + android:minWidth="@dimen/dropdown_touch_target_min_width" android:layout_marginEnd="@dimen/dropdown_layout_horizontal_margin" android:elevation="3dp"> @@ -24,7 +25,7 @@ android:id="@android:id/icon1" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:contentDescription="@string/provider_icon_content_description" + android:contentDescription="@string/dropdown_presentation_more_sign_in_options_text" android:layout_centerVertical="true" android:layout_alignParentStart="true" android:background="@null"/> diff --git a/packages/CredentialManager/res/values/dimens.xml b/packages/CredentialManager/res/values/dimens.xml index 3a8c78f6d854..53852cbd0d10 100644 --- a/packages/CredentialManager/res/values/dimens.xml +++ b/packages/CredentialManager/res/values/dimens.xml @@ -27,4 +27,5 @@ <dimen name="autofill_dropdown_textview_max_width">230dp</dimen> <dimen name="dropdown_layout_horizontal_margin">24dp</dimen> <integer name="autofill_max_visible_datasets">3</integer> + <dimen name="dropdown_touch_target_min_width">48dp</dimen> </resources>
\ No newline at end of file diff --git a/packages/PackageInstaller/AndroidManifest.xml b/packages/PackageInstaller/AndroidManifest.xml index 5a21d59c6471..09e0d61010f7 100644 --- a/packages/PackageInstaller/AndroidManifest.xml +++ b/packages/PackageInstaller/AndroidManifest.xml @@ -75,19 +75,15 @@ </intent-filter> </activity> - <!-- NOTE: the workaround to fix the screen flash problem. Remember to check the problem - is resolved for new implementation --> <activity android:name=".InstallStaging" - android:theme="@style/Theme.AlertDialogActivity.NoDim" - android:exported="false" /> + android:exported="false" /> <activity android:name=".DeleteStagedFileOnResult" android:theme="@style/Theme.AlertDialogActivity.NoActionBar" android:exported="false" /> <activity android:name=".PackageInstallerActivity" - android:theme="@style/Theme.AlertDialogActivity.NoAnimation" - android:exported="false" /> + android:exported="false" /> <activity android:name=".InstallInstalling" android:theme="@style/Theme.AlertDialogActivity.NoAnimation" diff --git a/packages/PackageInstaller/res/values/themes.xml b/packages/PackageInstaller/res/values/themes.xml index 811fa7380c48..9a062295608d 100644 --- a/packages/PackageInstaller/res/values/themes.xml +++ b/packages/PackageInstaller/res/values/themes.xml @@ -32,9 +32,4 @@ <item name="android:windowNoTitle">true</item> </style> - <style name="Theme.AlertDialogActivity.NoDim" - parent="@style/Theme.AlertDialogActivity.NoActionBar"> - <item name="android:backgroundDimAmount">0</item> - </style> - </resources> diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java index ceb580d170d0..7dc157fbb523 100644 --- a/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java +++ b/packages/PackageInstaller/src/com/android/packageinstaller/PackageInstallerActivity.java @@ -308,7 +308,6 @@ public class PackageInstallerActivity extends Activity { } private void initiateInstall() { - bindUi(); String pkgName = mPkgInfo.packageName; // Check if there is already a package on the device with this name // but it has been renamed to something else. @@ -448,6 +447,7 @@ public class PackageInstallerActivity extends Activity { if (mAppSnippet != null) { // load placeholder layout with OK button disabled until we override this layout in // startInstallConfirm + bindUi(); checkIfAllowedAndInitiateInstall(); } diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java index 38ec931612f0..4c255a556954 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java @@ -107,6 +107,7 @@ public class SystemSettings { Settings.System.TOUCHPAD_POINTER_SPEED, Settings.System.TOUCHPAD_NATURAL_SCROLLING, Settings.System.TOUCHPAD_TAP_TO_CLICK, + Settings.System.TOUCHPAD_TAP_DRAGGING, Settings.System.TOUCHPAD_RIGHT_CLICK_ZONE, Settings.System.CAMERA_FLASH_NOTIFICATION, Settings.System.SCREEN_FLASH_NOTIFICATION, diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java index 98941c7cc116..011b42f451bc 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java @@ -209,6 +209,7 @@ public class SystemSettingsValidators { VALIDATORS.put(System.TOUCHPAD_POINTER_SPEED, new InclusiveIntegerRangeValidator(-7, 7)); VALIDATORS.put(System.TOUCHPAD_NATURAL_SCROLLING, BOOLEAN_VALIDATOR); VALIDATORS.put(System.TOUCHPAD_TAP_TO_CLICK, BOOLEAN_VALIDATOR); + VALIDATORS.put(System.TOUCHPAD_TAP_DRAGGING, BOOLEAN_VALIDATOR); VALIDATORS.put(System.TOUCHPAD_RIGHT_CLICK_ZONE, BOOLEAN_VALIDATOR); VALIDATORS.put(System.LOCK_TO_APP_ENABLED, BOOLEAN_VALIDATOR); VALIDATORS.put( diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java index 1670a705f939..1e146a51e21e 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProtoDumpUtil.java @@ -3002,6 +3002,9 @@ class SettingsProtoDumpUtil { dumpSetting(s, p, Settings.System.TOUCHPAD_TAP_TO_CLICK, SystemSettingsProto.Touchpad.TAP_TO_CLICK); + dumpSetting(s, p, + Settings.System.TOUCHPAD_TAP_DRAGGING, + SystemSettingsProto.Touchpad.TAP_DRAGGING); p.end(touchpadToken); dumpSetting(s, p, diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 54ab5d1e726f..0050676ace84 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -1083,5 +1083,25 @@ <!-- Allow SystemUI to listen for the capabilities defined in the linked xml --> <property android:name="android.net.PROPERTY_SELF_CERTIFIED_CAPABILITIES" android:value="@xml/self_certified_network_capabilities_both" /> + + + <service + android:name="com.android.systemui.dreams.homecontrols.HomeControlsDreamService" + android:exported="false" + android:enabled="false" + android:label="@string/home_controls_dream_label" + android:description="@string/home_controls_dream_description" + android:permission="android.permission.BIND_DREAM_SERVICE" + android:icon="@drawable/controls_icon" + > + + <intent-filter> + <action android:name="android.service.dreams.DreamService" /> + <category android:name="android.intent.category.DEFAULT" /> + </intent-filter> + <meta-data + android:name="android.service.dream" + android:resource="@xml/home_controls_dream_metadata" /> + </service> </application> </manifest> diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig index a2530d59e2e6..0ea2b1fed968 100644 --- a/packages/SystemUI/aconfig/systemui.aconfig +++ b/packages/SystemUI/aconfig/systemui.aconfig @@ -59,14 +59,6 @@ flag { } flag { - name: "notification_lifetime_extension_refactor" - namespace: "systemui" - description: "Enables moving notification lifetime extension management from SystemUI to " - "Notification Manager Service" - bug: "299448097" -} - -flag { name: "notifications_live_data_store_refactor" namespace: "systemui" description: "Replaces NotifLiveDataStore with ActiveNotificationListRepository, and updates consumers. " @@ -371,3 +363,10 @@ flag { description: "Enables refactored logic for SysUI+WM unlock/occlusion code paths" bug: "278086361" } + +flag { + name: "enable_keyguard_compose" + namespace: "systemui" + description: "Enables the compose version of keyguard." + bug: "301968149" +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt index f9b91cf4c10b..bd539a740e81 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt @@ -20,6 +20,7 @@ package com.android.systemui.bouncer.ui.composable import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.TextField @@ -36,16 +37,21 @@ import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.onInterceptKeyBeforeSoftKeyboard +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.PasswordVisualTransformation import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import com.android.compose.PlatformIconButton import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel +import com.android.systemui.res.R /** UI for the input part of a password-requiring version of the bouncer. */ @Composable @@ -64,6 +70,7 @@ internal fun PasswordBouncer( val password: String by viewModel.password.collectAsState() val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsState() val animateFailure: Boolean by viewModel.animateFailure.collectAsState() + val isImeSwitcherButtonVisible by viewModel.isImeSwitcherButtonVisible.collectAsState() DisposableEffect(Unit) { viewModel.onShown() @@ -116,5 +123,28 @@ internal fun PasswordBouncer( false } }, + trailingIcon = + if (isImeSwitcherButtonVisible) { + { ImeSwitcherButton(viewModel, color) } + } else null + ) +} + +/** Button for changing the password input method (IME). */ +@Composable +private fun ImeSwitcherButton( + viewModel: PasswordBouncerViewModel, + color: Color, +) { + val context = LocalContext.current + PlatformIconButton( + onClick = { viewModel.onImeSwitcherButtonClicked(context.displayId) }, + iconResource = R.drawable.ic_lockscreen_ime, + contentDescription = stringResource(R.string.accessibility_ime_switch_button), + colors = + IconButtonDefaults.filledIconButtonColors( + contentColor = color, + containerColor = Color.Transparent, + ) ) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt index ff5a69801c56..92bc1f18a6c5 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt @@ -124,7 +124,7 @@ fun SceneKey.toCommunalSceneKey(): CommunalSceneKey { // TODO(b/315490861): Remove these conversions once Compose can be used throughout SysUI. fun CommunalSceneKey.toTransitionSceneKey(): SceneKey { - return SceneKey(name = toString(), identity = this) + return SceneKey(debugName = toString(), identity = this) } /** diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt index 42fcd1363f11..5e27d8299c16 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt @@ -21,19 +21,27 @@ import android.view.ViewGroup import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import com.android.compose.animation.scene.SceneScope +import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl import com.android.systemui.notifications.ui.composable.NotificationStack import com.android.systemui.scene.shared.flag.SceneContainerFlags import com.android.systemui.statusbar.notification.stack.AmbientState import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController +import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator import com.android.systemui.statusbar.notification.stack.shared.flexiNotifsEnabled import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackAppearanceViewBinder +import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel +import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +@SysUISingleton class NotificationSection @Inject constructor( @@ -42,22 +50,41 @@ constructor( controller: NotificationStackScrollLayoutController, sceneContainerFlags: SceneContainerFlags, sharedNotificationContainer: SharedNotificationContainer, + sharedNotificationContainerViewModel: SharedNotificationContainerViewModel, stackScrollLayout: NotificationStackScrollLayout, notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel, ambientState: AmbientState, + notificationStackSizeCalculator: NotificationStackSizeCalculator, + @Main mainDispatcher: CoroutineDispatcher, ) { init { - if (sceneContainerFlags.flexiNotifsEnabled()) { + if (!KeyguardShadeMigrationNssl.isUnexpectedlyInLegacyMode()) { + // This scene container section moves the NSSL to the SharedNotificationContainer. This + // also requires that SharedNotificationContainer gets moved to the SceneWindowRootView + // by the SceneWindowRootViewBinder. + // Prior to Scene Container, but when the KeyguardShadeMigrationNssl flag is enabled, + // NSSL is moved into this container by the NotificationStackScrollLayoutSection. (stackScrollLayout.parent as? ViewGroup)?.removeView(stackScrollLayout) sharedNotificationContainer.addNotificationStackScrollLayout(stackScrollLayout) - NotificationStackAppearanceViewBinder.bind( - context, + SharedNotificationContainerBinder.bind( sharedNotificationContainer, - notificationStackAppearanceViewModel, - ambientState, + sharedNotificationContainerViewModel, + sceneContainerFlags, controller, + notificationStackSizeCalculator, + mainDispatcher, ) + + if (sceneContainerFlags.flexiNotifsEnabled()) { + NotificationStackAppearanceViewBinder.bind( + context, + sharedNotificationContainer, + notificationStackAppearanceViewModel, + ambientState, + controller, + ) + } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimNestedScrollConnection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimNestedScrollConnection.kt new file mode 100644 index 000000000000..2ba78cfd7785 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimNestedScrollConnection.kt @@ -0,0 +1,77 @@ +/* + * 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.notifications.ui.composable + +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import com.android.compose.nestedscroll.PriorityNestedScrollConnection + +/** + * A [NestedScrollConnection] that listens for all vertical scroll events and responds in the + * following way: + * - If you **scroll up**, it **first brings the [scrimOffset]** back to the [minScrimOffset] and + * then allows scrolling of the children (usually the content). + * - If you **scroll down**, it **first allows scrolling of the children** (usually the content) and + * then resets the [scrimOffset] to [maxScrimOffset]. + */ +fun NotificationScrimNestedScrollConnection( + scrimOffset: () -> Float, + onScrimOffsetChanged: (Float) -> Unit, + minScrimOffset: () -> Float, + maxScrimOffset: Float, + contentHeight: () -> Float, + minVisibleScrimHeight: () -> Float, +): PriorityNestedScrollConnection { + return PriorityNestedScrollConnection( + orientation = Orientation.Vertical, + // scrolling up and inner content is taller than the scrim, so scrim needs to + // expand; content can scroll once scrim is at the minScrimOffset. + canStartPreScroll = { offsetAvailable, offsetBeforeStart -> + offsetAvailable < 0 && + offsetBeforeStart == 0f && + contentHeight() > minVisibleScrimHeight() && + scrimOffset() > minScrimOffset() + }, + // scrolling down and content is done scrolling to top. After that, the scrim + // needs to collapse; collapse the scrim until it is at the maxScrimOffset. + canStartPostScroll = { offsetAvailable, _ -> + offsetAvailable > 0 && scrimOffset() < maxScrimOffset + }, + canStartPostFling = { false }, + canContinueScroll = { + val currentHeight = scrimOffset() + minScrimOffset() < currentHeight && currentHeight < maxScrimOffset + }, + canScrollOnFling = true, + onStart = { /* do nothing */}, + onScroll = { offsetAvailable -> + val currentHeight = scrimOffset() + val amountConsumed = + if (offsetAvailable > 0) { + val amountLeft = maxScrimOffset - currentHeight + offsetAvailable.coerceAtMost(amountLeft) + } else { + val amountLeft = minScrimOffset() - currentHeight + offsetAvailable.coerceAtLeast(amountLeft) + } + onScrimOffsetChanged(currentHeight + amountConsumed) + amountConsumed + }, + // Don't consume the velocity on pre/post fling + onStop = { 0f }, + ) +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt index e835d3e576d5..0e08a198c71e 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt @@ -20,32 +20,53 @@ package com.android.systemui.notifications.ui.composable import android.util.Log import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBars +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.layout.LayoutCoordinates import androidx.compose.ui.layout.boundsInWindow import androidx.compose.ui.layout.onPlaced import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.positionInWindow +import androidx.compose.ui.platform.LocalConfiguration +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import com.android.compose.animation.scene.ElementKey +import com.android.compose.animation.scene.NestedScrollBehavior import com.android.compose.animation.scene.SceneScope import com.android.compose.modifiers.height import com.android.systemui.notifications.ui.composable.Notifications.Form +import com.android.systemui.scene.ui.composable.Gone +import com.android.systemui.scene.ui.composable.Shade +import com.android.systemui.shade.ui.composable.ShadeHeader import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel import kotlin.math.roundToInt @@ -100,33 +121,109 @@ fun SceneScope.NotificationStack( @Composable fun SceneScope.NotificationScrollingStack( viewModel: NotificationsPlaceholderViewModel, + maxScrimTop: () -> Float, modifier: Modifier = Modifier, ) { + val density = LocalDensity.current val cornerRadius by viewModel.cornerRadiusDp.collectAsState() + val expansionFraction by viewModel.expandFraction.collectAsState(0f) - val contentHeight by viewModel.intrinsicContentHeight.collectAsState() + val navBarHeight = + with(density) { WindowInsets.systemBars.asPaddingValues().calculateBottomPadding().toPx() } + val statusBarHeight = + with(density) { WindowInsets.systemBars.asPaddingValues().calculateTopPadding().toPx() } + val displayCutoutHeight = + with(density) { WindowInsets.displayCutout.asPaddingValues().calculateTopPadding().toPx() } + val screenHeight = + with(density) { LocalConfiguration.current.screenHeightDp.dp.toPx() } + + navBarHeight + + maxOf(statusBarHeight, displayCutoutHeight) - val expansionFraction by viewModel.expandFraction.collectAsState(0f) + val contentHeight = viewModel.intrinsicContentHeight.collectAsState() - Box( - modifier = - modifier - .verticalNestedScrollToScene() - .fillMaxWidth() - .element(Notifications.Elements.NotificationScrim) - .graphicsLayer { - shape = RoundedCornerShape(cornerRadius.dp) - clip = true - alpha = expansionFraction - } - .background(MaterialTheme.colorScheme.surface) - .debugBackground(viewModel, Color(0.5f, 0.5f, 0f, 0.2f)) - ) { - NotificationPlaceholder( - viewModel = viewModel, - form = Form.Stack, - modifier = Modifier.fillMaxWidth().height { contentHeight.roundToInt() } + // the offset for the notifications scrim. Its upper bound is 0, and its lower bound is + // calculated in minScrimOffset. The scrim is the same height as the screen minus the + // height of the Shade Header, and at rest (scrimOffset = 0) its top bound is at maxScrimStartY. + // When fully expanded (scrimOffset = minScrimOffset), its top bound is at minScrimStartY, + // which is equal to the height of the Shade Header. Thus, when the scrim is fully expanded, the + // entire height of the scrim is visible on screen. + val scrimOffset = remember { mutableStateOf(0f) } + + val minScrimTop = with(density) { ShadeHeader.Dimensions.CollapsedHeight.toPx() } + + // The minimum offset for the scrim. The scrim is considered fully expanded when it + // is at this offset. + val minScrimOffset: () -> Float = { minScrimTop - maxScrimTop() } + + // The height of the scrim visible on screen when it is in its resting (collapsed) state. + val minVisibleScrimHeight: () -> Float = { screenHeight - maxScrimTop() } + + // we are not scrolled to the top unless the scrim is at its maximum offset. + LaunchedEffect(viewModel, scrimOffset) { + snapshotFlow { scrimOffset.value >= 0f } + .collect { isScrolledToTop -> viewModel.setScrolledToTop(isScrolledToTop) } + } + + // if contentHeight drops below minimum visible scrim height while scrim is + // expanded, reset scrim offset. + LaunchedEffect(contentHeight, screenHeight, maxScrimTop, scrimOffset) { + snapshotFlow { contentHeight.value < minVisibleScrimHeight() && scrimOffset.value < 0f } + .collect { shouldCollapse -> if (shouldCollapse) scrimOffset.value = 0f } + } + + Box(modifier = modifier.element(Notifications.Elements.NotificationScrim)) { + Spacer( + modifier = + Modifier.fillMaxSize() + .graphicsLayer { + shape = RoundedCornerShape(cornerRadius.dp) + clip = true + } + .drawBehind { drawRect(Color.Black, blendMode = BlendMode.DstOut) } ) + Box( + modifier = + Modifier.fillMaxSize() + .offset { IntOffset(0, scrimOffset.value.roundToInt()) } + .graphicsLayer { + shape = RoundedCornerShape(cornerRadius.dp) + clip = true + alpha = + if (layoutState.isTransitioningBetween(Gone, Shade)) { + (expansionFraction / 0.3f).coerceAtMost(1f) + } else 1f + } + .background(MaterialTheme.colorScheme.surface) + .debugBackground(viewModel, Color(0.5f, 0.5f, 0f, 0.2f)) + ) { + NotificationPlaceholder( + viewModel = viewModel, + form = Form.Stack, + modifier = + Modifier.verticalNestedScrollToScene( + topBehavior = NestedScrollBehavior.EdgeWithPreview, + ) + .nestedScroll( + remember( + scrimOffset, + maxScrimTop, + minScrimTop, + ) { + NotificationScrimNestedScrollConnection( + scrimOffset = { scrimOffset.value }, + onScrimOffsetChanged = { scrimOffset.value = it }, + minScrimOffset = minScrimOffset, + maxScrimOffset = 0f, + contentHeight = { contentHeight.value }, + minVisibleScrimHeight = minVisibleScrimHeight, + ) + } + ) + .verticalScroll(rememberScrollState()) + .fillMaxWidth() + .height { (contentHeight.value + navBarHeight).roundToInt() }, + ) + } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt index bbfe0fda049a..5531f9cc5589 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt @@ -36,6 +36,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.MaterialTheme import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass @@ -46,6 +47,11 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.layout.LayoutCoordinates +import androidx.compose.ui.layout.boundsInWindow +import androidx.compose.ui.layout.onPlaced +import androidx.compose.ui.layout.positionInWindow import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.unit.dp @@ -56,7 +62,7 @@ import com.android.systemui.battery.BatteryMeterViewController import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application -import com.android.systemui.notifications.ui.composable.HeadsUpNotificationSpace +import com.android.systemui.notifications.ui.composable.Notifications import com.android.systemui.qs.footer.ui.compose.FooterActions import com.android.systemui.qs.ui.viewmodel.QuickSettingsSceneViewModel import com.android.systemui.scene.shared.model.SceneKey @@ -116,6 +122,8 @@ private fun SceneScope.QuickSettingsScene( statusBarIconController: StatusBarIconController, modifier: Modifier = Modifier, ) { + val cornerRadius by viewModel.notifications.cornerRadiusDp.collectAsState() + // TODO(b/280887232): implement the real UI. Box(modifier = modifier.fillMaxSize()) { val isCustomizing by viewModel.qsSceneAdapter.isCustomizing.collectAsState() @@ -234,10 +242,32 @@ private fun SceneScope.QuickSettingsScene( } } } - HeadsUpNotificationSpace( - viewModel = viewModel.notifications, - isPeekFromBottom = true, - modifier = Modifier.padding(16.dp).fillMaxSize(), + // Scrim with height 0 aligned to bottom of the screen to facilitate shared element + // transition from Shade scene. + Box( + modifier = + Modifier.element(Notifications.Elements.NotificationScrim) + .fillMaxWidth() + .height(0.dp) + .graphicsLayer { + shape = RoundedCornerShape(cornerRadius.dp) + clip = true + alpha = 1f + } + .background(MaterialTheme.colorScheme.surface) + .align(Alignment.BottomCenter) + .onPlaced { coordinates: LayoutCoordinates -> + viewModel.notifications.onContentTopChanged( + coordinates.positionInWindow().y + ) + val boundsInWindow = coordinates.boundsInWindow() + viewModel.notifications.onBoundsChanged( + left = boundsInWindow.left, + top = boundsInWindow.top, + right = boundsInWindow.right, + bottom = boundsInWindow.bottom, + ) + } ) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt index 747faabe514b..770d654a4c88 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/GoneScene.kt @@ -18,13 +18,10 @@ package com.android.systemui.scene.ui.composable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import com.android.compose.animation.scene.SceneScope import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.notifications.ui.composable.HeadsUpNotificationSpace import com.android.systemui.notifications.ui.composable.Notifications import com.android.systemui.scene.shared.model.Direction import com.android.systemui.scene.shared.model.Edge @@ -66,12 +63,6 @@ constructor( override fun SceneScope.Content( modifier: Modifier, ) { - Box(modifier = modifier) { - Box(modifier = Modifier.fillMaxSize().element(Notifications.Elements.NotificationScrim)) - HeadsUpNotificationSpace( - viewModel = notificationsViewModel, - modifier = Modifier.padding(16.dp).fillMaxSize(), - ) - } + Box(modifier = Modifier.fillMaxSize().element(Notifications.Elements.NotificationScrim)) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/TransitionSceneKeys.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/TransitionSceneKeys.kt index 5336bf646231..0c66701de61c 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/TransitionSceneKeys.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/TransitionSceneKeys.kt @@ -12,5 +12,5 @@ val Communal = SceneKey.Communal.toTransitionSceneKey() // TODO(b/293899074): Remove this file once we can use the scene keys from SceneTransitionLayout. fun SceneKey.toTransitionSceneKey(): SceneTransitionSceneKey { - return SceneTransitionSceneKey(name = toString(), identity = this) + return SceneTransitionSceneKey(debugName = toString(), identity = this) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt index e2beaeea6402..b11edf7b47b7 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt @@ -58,6 +58,8 @@ import com.android.systemui.battery.BatteryMeterViewController import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation import com.android.systemui.common.ui.compose.windowinsets.LocalDisplayCutout import com.android.systemui.res.R +import com.android.systemui.scene.ui.composable.QuickSettings +import com.android.systemui.scene.ui.composable.Shade as ShadeKey import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel import com.android.systemui.statusbar.phone.StatusBarIconController import com.android.systemui.statusbar.phone.StatusBarIconController.TintedIconManager @@ -348,7 +350,7 @@ private fun ShadeCarrierGroup( } @Composable -private fun StatusIcons( +private fun SceneScope.StatusIcons( viewModel: ShadeHeaderViewModel, createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager, statusBarIconController: StatusBarIconController, @@ -358,7 +360,6 @@ private fun StatusIcons( val carrierIconSlots = listOf(stringResource(id = com.android.internal.R.string.status_bar_mobile)) val isSingleCarrier by viewModel.isSingleCarrier.collectAsState() - val isTransitioning by viewModel.isTransitioning.collectAsState() AndroidView( factory = { context -> @@ -373,7 +374,9 @@ private fun StatusIcons( iconContainer }, update = { iconContainer -> - iconContainer.setQsExpansionTransitioning(isTransitioning) + iconContainer.setQsExpansionTransitioning( + layoutState.isTransitioningBetween(ShadeKey, QuickSettings) + ) if (isSingleCarrier || !useExpandedFormat) { iconContainer.removeIgnoredSlots(carrierIconSlots) } else { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt index 1545372686c9..497fe873e87d 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt @@ -32,6 +32,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.layout.Layout import androidx.compose.ui.layout.layout import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.dimensionResource @@ -62,6 +63,7 @@ import com.android.systemui.statusbar.phone.StatusBarLocation import com.android.systemui.util.animation.MeasurementInput import javax.inject.Inject import javax.inject.Named +import kotlin.math.roundToInt import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -154,62 +156,104 @@ private fun SceneScope.ShadeScene( mediaHost: MediaHost, modifier: Modifier = Modifier, ) { - val localDensity = LocalDensity.current + val density = LocalDensity.current val layoutWidth = remember { mutableStateOf(0) } + val maxNotifScrimTop = remember { mutableStateOf(0f) } Box( modifier = modifier.element(Shade.Elements.Scrim).background(MaterialTheme.colorScheme.scrim), ) Box { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - modifier = Modifier.fillMaxWidth().clickable(onClick = { viewModel.onContentClicked() }) - ) { - CollapsedShadeHeader( - viewModel = viewModel.shadeHeaderViewModel, - createTintedIconManager = createTintedIconManager, - createBatteryMeterViewController = createBatteryMeterViewController, - statusBarIconController = statusBarIconController, - modifier = Modifier.padding(horizontal = Shade.Dimensions.HorizontalPadding) - ) - QuickSettings( - modifier = Modifier.height(130.dp), - viewModel.qsSceneAdapter, - ) - - if (viewModel.isMediaVisible()) { - val mediaHeight = dimensionResource(R.dimen.qs_media_session_height_expanded) - MediaCarousel( - modifier = - Modifier.height(mediaHeight).fillMaxWidth().layout { measurable, constraints - -> - val placeable = measurable.measure(constraints) - - // Notify controller to size the carousel for the current space - mediaHost.measurementInput = - MeasurementInput(placeable.width, placeable.height) - mediaCarouselController.setSceneContainerSize( - placeable.width, - placeable.height + Layout( + contents = + listOf( + { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = + Modifier.fillMaxWidth() + .clickable(onClick = { viewModel.onContentClicked() }) + ) { + CollapsedShadeHeader( + viewModel = viewModel.shadeHeaderViewModel, + createTintedIconManager = createTintedIconManager, + createBatteryMeterViewController = createBatteryMeterViewController, + statusBarIconController = statusBarIconController, + modifier = + Modifier.padding( + horizontal = Shade.Dimensions.HorizontalPadding + ) + ) + QuickSettings( + modifier = Modifier.height(130.dp), + viewModel.qsSceneAdapter, ) - layout(placeable.width, placeable.height) { - placeable.placeRelative(0, 0) + if (viewModel.isMediaVisible()) { + val mediaHeight = + dimensionResource(R.dimen.qs_media_session_height_expanded) + MediaCarousel( + modifier = + Modifier.height(mediaHeight).fillMaxWidth().layout { + measurable, + constraints -> + val placeable = measurable.measure(constraints) + + // Notify controller to size the carousel for the + // current space + mediaHost.measurementInput = + MeasurementInput(placeable.width, placeable.height) + mediaCarouselController.setSceneContainerSize( + placeable.width, + placeable.height + ) + + layout(placeable.width, placeable.height) { + placeable.placeRelative(0, 0) + } + }, + mediaHost = mediaHost, + layoutWidth = layoutWidth.value, + layoutHeight = with(density) { mediaHeight.toPx() }.toInt(), + carouselController = mediaCarouselController, + ) } - }, - mediaHost = mediaHost, - layoutWidth = layoutWidth.value, - layoutHeight = with(localDensity) { mediaHeight.toPx() }.toInt(), - carouselController = mediaCarouselController, + + Spacer(modifier = Modifier.height(16.dp)) + } + }, + { + NotificationScrollingStack( + viewModel = viewModel.notifications, + maxScrimTop = { maxNotifScrimTop.value }, + ) + }, ) - } + ) { measurables, constraints -> + check(measurables.size == 2) + check(measurables[0].size == 1) + check(measurables[1].size == 1) - Spacer(modifier = Modifier.height(16.dp)) - NotificationScrollingStack( - viewModel = viewModel.notifications, - modifier = Modifier.fillMaxWidth().weight(1f), - ) + val quickSettingsPlaceable = measurables[0][0].measure(constraints) + + val notificationsMeasurable = measurables[1][0] + val notificationsScrimMaxHeight = + constraints.maxHeight - ShadeHeader.Dimensions.CollapsedHeight.roundToPx() + val notificationsPlaceable = + notificationsMeasurable.measure( + constraints.copy( + minHeight = notificationsScrimMaxHeight, + maxHeight = notificationsScrimMaxHeight + ) + ) + + maxNotifScrimTop.value = quickSettingsPlaceable.height.toFloat() + + layout(constraints.maxWidth, constraints.maxHeight) { + quickSettingsPlaceable.placeRelative(x = 0, y = 0) + notificationsPlaceable.placeRelative(x = 0, y = maxNotifScrimTop.value.roundToInt()) + } } } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt index 7d3b0fbe1725..705754594903 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt @@ -61,7 +61,7 @@ internal fun CoroutineScope.animateToScene( // The transition is already finished (progress ~= 1): no need to animate. We // finish the current transition early to make sure that the current state // change is committed. - layoutState.finishTransition(transitionState, transitionState.currentScene) + layoutState.finishTransition(transitionState, target) null } else { // The transition is in progress: start the canned animation at the same @@ -78,7 +78,7 @@ internal fun CoroutineScope.animateToScene( if (progress.absoluteValue < ProgressVisibilityThreshold) { // The transition is at progress ~= 0: no need to animate.We finish the current // transition early to make sure that the current state change is committed. - layoutState.finishTransition(transitionState, transitionState.currentScene) + layoutState.finishTransition(transitionState, target) null } else { // TODO(b/290184746): Also take the current velocity into account. diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt index 9d4b69c51690..c68519547c7e 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Key.kt @@ -42,16 +42,16 @@ sealed class Key(val debugName: String, val identity: Any) { /** Key for a scene. */ class SceneKey( - name: String, + debugName: String, identity: Any = Object(), -) : Key(name, identity), UserActionResult { +) : Key(debugName, identity), UserActionResult { @VisibleForTesting // TODO(b/240432457): Make internal once PlatformComposeSceneTransitionLayoutTestsUtils can // access internal members. - val testTag: String = "scene:$name" + val testTag: String = "scene:$debugName" /** The unique [ElementKey] identifying this scene's root element. */ - val rootElementKey = ElementKey(name, identity) + val rootElementKey = ElementKey(debugName, identity) // Implementation of [UserActionResult]. override val toScene: SceneKey = this @@ -64,7 +64,7 @@ class SceneKey( /** Key for an element. */ class ElementKey( - name: String, + debugName: String, identity: Any = Object(), /** @@ -72,11 +72,11 @@ class ElementKey( * or compose MovableElements. */ val scenePicker: ElementScenePicker = DefaultElementScenePicker, -) : Key(name, identity), ElementMatcher { +) : Key(debugName, identity), ElementMatcher { @VisibleForTesting // TODO(b/240432457): Make internal once PlatformComposeSceneTransitionLayoutTestsUtils can // access internal members. - val testTag: String = "element:$name" + val testTag: String = "element:$debugName" override fun matches(key: ElementKey, scene: SceneKey): Boolean { return key == this @@ -99,7 +99,7 @@ class ElementKey( } /** Key for a shared value of an element. */ -class ValueKey(name: String, identity: Any = Object()) : Key(name, identity) { +class ValueKey(debugName: String, identity: Any = Object()) : Key(debugName, identity) { override fun toString(): String { return "ValueKey(debugName=$debugName)" } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt index 5f615fdbe239..529fc0327fcb 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MultiPointerDraggable.kt @@ -263,10 +263,10 @@ private suspend fun PointerInputScope.detectDragGestures( val deltaOffset = drag.position - initialDown.position val delta = when (orientation) { - Orientation.Horizontal -> deltaOffset.y + Orientation.Horizontal -> deltaOffset.x Orientation.Vertical -> deltaOffset.y } - check(delta != 0f) + check(delta != 0f) { "delta is equal to 0" } overSlop = delta.sign } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt index 58c3be244725..35754d624ccd 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneGestureHandler.kt @@ -146,9 +146,10 @@ internal class SceneGestureHandler( val fromScene = layoutImpl.scene(transitionState.currentScene) updateSwipes(fromScene, startedPosition, pointersDown) - val (targetScene, distance) = - findTargetSceneAndDistance(fromScene, overSlop, updateSwipesResults = true) ?: return - updateTransition(SwipeTransition(fromScene, targetScene, distance), force = true) + val result = + findUserActionResult(fromScene, directionOffset = overSlop, updateSwipesResults = true) + ?: return + updateTransition(SwipeTransition(fromScene, result), force = true) } private fun updateSwipes(fromScene: Scene, startedPosition: Offset?, pointersDown: Int) { @@ -224,8 +225,8 @@ internal class SceneGestureHandler( computeFromSceneConsideringAcceleratedSwipe(swipeTransition) val isNewFromScene = fromScene.key != swipeTransition.fromScene - val (targetScene, distance) = - findTargetSceneAndDistance( + val result = + findUserActionResult( fromScene, swipeTransition.dragOffset, updateSwipesResults = isNewFromScene, @@ -236,9 +237,9 @@ internal class SceneGestureHandler( } swipeTransition.dragOffset += acceleratedOffset - if (isNewFromScene || targetScene.key != swipeTransition.toScene) { + if (isNewFromScene || result.toScene != swipeTransition.toScene) { updateTransition( - SwipeTransition(fromScene, targetScene, distance).apply { + SwipeTransition(fromScene, result).apply { this.dragOffset = swipeTransition.dragOffset } ) @@ -306,7 +307,7 @@ internal class SceneGestureHandler( } /** - * Returns the target scene and distance from [fromScene] in the direction [directionOffset]. + * Returns the [UserActionResult] from [fromScene] in the direction of [directionOffset]. * * @param fromScene the scene from which we look for the target * @param directionOffset signed float that indicates the direction. Positive is down or right @@ -322,60 +323,45 @@ internal class SceneGestureHandler( * [directionOffset] is 0f and both direction are available, it will default to * [upOrLeftResult]. */ - private inline fun findTargetSceneAndDistance( + private fun findUserActionResult( fromScene: Scene, directionOffset: Float, updateSwipesResults: Boolean, - ): Pair<Scene, Float>? { + ): UserActionResult? { if (updateSwipesResults) updateSwipesResults(fromScene) - // Compute the target scene depending on the current offset. return when { upOrLeftResult == null && downOrRightResult == null -> null (directionOffset < 0f && upOrLeftResult != null) || downOrRightResult == null -> - upOrLeftResult?.let { result -> - Pair( - layoutImpl.scene(result.toScene), - -fromScene.getAbsoluteDistance(result.distance) - ) - } - else -> - downOrRightResult?.let { result -> - Pair( - layoutImpl.scene(result.toScene), - fromScene.getAbsoluteDistance(result.distance) - ) - } + upOrLeftResult + else -> downOrRightResult } } /** - * A strict version of [findTargetSceneAndDistance] that will return null when there is no Scene - * in [directionOffset] direction + * A strict version of [findUserActionResult] that will return null when there is no Scene in + * [directionOffset] direction */ - private inline fun findTargetSceneAndDistanceStrict( - fromScene: Scene, - directionOffset: Float, - ): Pair<Scene, Float>? { + private fun findUserActionResultStrict(directionOffset: Float): UserActionResult? { return when { - directionOffset > 0f -> - upOrLeftResult?.let { result -> - Pair( - layoutImpl.scene(result.toScene), - -fromScene.getAbsoluteDistance(result.distance), - ) - } - directionOffset < 0f -> - downOrRightResult?.let { result -> - Pair( - layoutImpl.scene(result.toScene), - fromScene.getAbsoluteDistance(result.distance), - ) - } + directionOffset > 0f -> upOrLeftResult + directionOffset < 0f -> downOrRightResult else -> null } } + private fun computeAbsoluteDistance( + fromScene: Scene, + result: UserActionResult, + ): Float { + return if (result == upOrLeftResult) { + -fromScene.getAbsoluteDistance(result.distance) + } else { + check(result == downOrRightResult) + fromScene.getAbsoluteDistance(result.distance) + } + } + internal fun onDragStopped(velocity: Float, canChangeScene: Boolean) { // The state was changed since the drag started; don't do anything. if (!isDrivingTransition) { @@ -440,8 +426,8 @@ internal class SceneGestureHandler( if (startFromIdlePosition) { // If there is a target scene, we start the overscroll animation. - val (targetScene, distance) = - findTargetSceneAndDistanceStrict(fromScene, velocity) + val result = + findUserActionResultStrict(velocity) ?: run { // We will not animate layoutState.finishTransition(swipeTransition, idleScene = fromScene.key) @@ -449,7 +435,7 @@ internal class SceneGestureHandler( } updateTransition( - SwipeTransition(fromScene, targetScene, distance).apply { + SwipeTransition(fromScene, result).apply { _currentScene = swipeTransition._currentScene } ) @@ -496,6 +482,14 @@ internal class SceneGestureHandler( } } + private fun SwipeTransition(fromScene: Scene, result: UserActionResult): SwipeTransition { + return SwipeTransition( + fromScene, + layoutImpl.scene(result.toScene), + computeAbsoluteDistance(fromScene, result), + ) + } + internal class SwipeTransition( val _fromScene: Scene, val _toScene: Scene, diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt index 48825fb88096..9a25d43cb62d 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt @@ -91,6 +91,16 @@ class SceneTransitionLayoutStateTest { } @Test + fun setTargetScene_transitionToOriginalScene() = runMonotonicClockTest { + val state = MutableSceneTransitionLayoutState(TestScenes.SceneA) + assertThat(state.setTargetScene(TestScenes.SceneB, coroutineScope = this)).isNotNull() + + // Progress is 0f, so we don't animate at all and directly snap back to A. + assertThat(state.setTargetScene(TestScenes.SceneA, coroutineScope = this)).isNull() + assertThat(state.transitionState).isEqualTo(TransitionState.Idle(TestScenes.SceneA)) + } + + @Test fun setTargetScene_coroutineScopeCancelled() = runMonotonicClockTest { val state = MutableSceneTransitionLayoutState(TestScenes.SceneA) diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt index d81631aad13a..af1a5b8f37b0 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt @@ -442,6 +442,23 @@ class SwipeToSceneTest { transition = layoutState.currentTransition assertThat(transition).isNotNull() assertThat(transition?.toScene).isEqualTo(TestScenes.SceneB) + + // Release the finger, animating back to scene A. + rule.onRoot().performTouchInput { up() } + rule.waitForIdle() + assertThat(layoutState.currentTransition).isNull() + assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA) + + // Swipe left by exactly touchSlop, so that the drag overSlop is 0f. + rule.onRoot().performTouchInput { + down(middle) + moveBy(Offset(-touchSlop, 0f), delayMillis = 1_000) + } + + // We should still correctly compute that we are swiping down to scene B. + transition = layoutState.currentTransition + assertThat(transition).isNotNull() + assertThat(transition?.toScene).isEqualTo(TestScenes.SceneB) } @Test diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderClient.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderClient.kt index e0506046ee54..34c4dfb4bc54 100644 --- a/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderClient.kt +++ b/packages/SystemUI/customization/src/com/android/systemui/shared/customization/data/content/CustomizationProviderClient.kt @@ -33,6 +33,7 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.withContext @@ -518,6 +519,7 @@ class CustomizationProviderClientImpl( awaitClose { context.contentResolver.unregisterContentObserver(observer) } } .onStart { emit(Unit) } + .flowOn(backgroundDispatcher) } private fun String.toIntent( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/SysuiTestCaseSelfTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/SysuiTestCaseSelfTest.kt index be6bb9c39299..107293e801cc 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/SysuiTestCaseSelfTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/SysuiTestCaseSelfTest.kt @@ -24,6 +24,7 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) +@android.platform.test.annotations.EnabledOnRavenwood class SysuiTestCaseSelfTest : SysuiTestCase() { private val contextBeforeSetup = context diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepositoryImplTest.kt index 9287edf4ee51..c2f0c6f71169 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/AccessibilityQsShortcutsRepositoryImplTest.kt @@ -31,6 +31,7 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) +@android.platform.test.annotations.EnabledOnRavenwood class AccessibilityQsShortcutsRepositoryImplTest : SysuiTestCase() { private val testDispatcher = StandardTestDispatcher() private val testScope = TestScope(testDispatcher) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt index c86c7470909b..b6605ed9d007 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorCorrectionRepositoryImplTest.kt @@ -37,6 +37,7 @@ import org.junit.runner.RunWith @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) +@android.platform.test.annotations.EnabledOnRavenwood class ColorCorrectionRepositoryImplTest : SysuiTestCase() { private val testUser1 = UserHandle.of(1)!! diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt index 4853529229fe..30eb782e5938 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/ColorInversionRepositoryImplTest.kt @@ -37,6 +37,7 @@ import org.junit.runner.RunWith @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) +@android.platform.test.annotations.EnabledOnRavenwood class ColorInversionRepositoryImplTest : SysuiTestCase() { private val testUser1 = UserHandle.of(1)!! diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/UserA11yQsShortcutsRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/UserA11yQsShortcutsRepositoryTest.kt index ce22e288e292..ed3b4c0fe322 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/UserA11yQsShortcutsRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/accessibility/data/repository/UserA11yQsShortcutsRepositoryTest.kt @@ -31,6 +31,7 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) +@android.platform.test.annotations.EnabledOnRavenwood class UserA11yQsShortcutsRepositoryTest : SysuiTestCase() { private val secureSettings = FakeSettings() private val testDispatcher = StandardTestDispatcher() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/FaceHelpMessageDeferralTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/FaceHelpMessageDeferralTest.kt index 39f0d570cb26..059620502a49 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/FaceHelpMessageDeferralTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/FaceHelpMessageDeferralTest.kt @@ -34,6 +34,7 @@ import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidJUnit4::class) +@android.platform.test.annotations.EnabledOnRavenwood class FaceHelpMessageDeferralTest : SysuiTestCase() { val threshold = .75f @Mock lateinit var logger: BiometricMessageDeferralLogger diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepositoryImplTest.kt index a67b0931f171..d317aeb08e1e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/data/repository/EmergencyServicesRepositoryImplTest.kt @@ -37,6 +37,7 @@ import org.junit.runner.RunWith @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) +@android.platform.test.annotations.EnabledOnRavenwood class EmergencyServicesRepositoryImplTest : SysuiTestCase() { private val kosmos = testKosmos() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerCallbackInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerCallbackInteractorTest.kt index 4aea4f329858..db83b3ba12b1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerCallbackInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerCallbackInteractorTest.kt @@ -29,6 +29,7 @@ import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidJUnit4::class) +@android.platform.test.annotations.EnabledOnRavenwood class PrimaryBouncerCallbackInteractorTest : SysuiTestCase() { private val mPrimaryBouncerCallbackInteractor = PrimaryBouncerCallbackInteractor() @Mock diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt index c193d14220de..fbb5415402db 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt @@ -16,6 +16,7 @@ package com.android.systemui.bouncer.ui.viewmodel +import android.content.pm.UserInfo import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase @@ -26,18 +27,27 @@ import com.android.systemui.bouncer.domain.interactor.bouncerInteractor import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository +import com.android.systemui.inputmethod.data.model.InputMethodModel +import com.android.systemui.inputmethod.data.repository.fakeInputMethodRepository +import com.android.systemui.inputmethod.domain.interactor.inputMethodInteractor import com.android.systemui.kosmos.testScope import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.shared.model.SceneModel import com.android.systemui.testKosmos +import com.android.systemui.user.data.model.SelectedUserModel +import com.android.systemui.user.data.model.SelectionStatus +import com.android.systemui.user.data.repository.fakeUserRepository +import com.android.systemui.user.domain.interactor.selectedUserInteractor import com.google.common.truth.Truth.assertThat +import java.util.UUID import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -51,19 +61,22 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope - private val authenticationInteractor = kosmos.authenticationInteractor + private val authenticationInteractor by lazy { kosmos.authenticationInteractor } private val sceneInteractor by lazy { kosmos.sceneInteractor } private val bouncerInteractor by lazy { kosmos.bouncerInteractor } + private val selectedUserInteractor by lazy { kosmos.selectedUserInteractor } + private val inputMethodInteractor by lazy { kosmos.inputMethodInteractor } private val bouncerViewModel by lazy { kosmos.bouncerViewModel } private val isInputEnabled = MutableStateFlow(true) - private val underTest by lazy { + private val underTest = PasswordBouncerViewModel( viewModelScope = testScope.backgroundScope, + isInputEnabled = isInputEnabled.asStateFlow(), interactor = bouncerInteractor, - isInputEnabled.asStateFlow(), + inputMethodInteractor = inputMethodInteractor, + selectedUserInteractor = selectedUserInteractor, ) - } @Before fun setUp() { @@ -270,6 +283,52 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { assertThat(isTextFieldFocusRequested).isTrue() } + @Test + fun isImeSwitcherButtonVisible() = + testScope.runTest { + val selectedUserId by collectLastValue(selectedUserInteractor.selectedUser) + selectUser(USER_INFOS.first()) + + enableInputMethodsForUser(checkNotNull(selectedUserId)) + + // Assert initial value, before the UI subscribes. + assertThat(underTest.isImeSwitcherButtonVisible.value).isFalse() + + // Subscription starts; verify a fresh value is fetched. + val isImeSwitcherButtonVisible by collectLastValue(underTest.isImeSwitcherButtonVisible) + assertThat(isImeSwitcherButtonVisible).isTrue() + + // Change the user, verify a fresh value is fetched. + selectUser(USER_INFOS.last()) + + assertThat( + inputMethodInteractor.hasMultipleEnabledImesOrSubtypes( + checkNotNull(selectedUserId) + ) + ) + .isFalse() + assertThat(isImeSwitcherButtonVisible).isFalse() + + // Enable IMEs and add another subscriber; verify a fresh value is fetched. + enableInputMethodsForUser(checkNotNull(selectedUserId)) + val collector2 by collectLastValue(underTest.isImeSwitcherButtonVisible) + assertThat(collector2).isTrue() + } + + @Test + fun onImeSwitcherButtonClicked() = + testScope.runTest { + val displayId = 7 + assertThat(kosmos.fakeInputMethodRepository.inputMethodPickerShownDisplayId) + .isNotEqualTo(displayId) + + underTest.onImeSwitcherButtonClicked(displayId) + runCurrent() + + assertThat(kosmos.fakeInputMethodRepository.inputMethodPickerShownDisplayId) + .isEqualTo(displayId) + } + private fun TestScope.switchToScene(toScene: SceneKey) { val currentScene by collectLastValue(sceneInteractor.desiredScene) val bouncerShown = currentScene?.key != SceneKey.Bouncer && toScene == SceneKey.Bouncer @@ -310,8 +369,45 @@ class PasswordBouncerViewModelTest : SysuiTestCase() { runCurrent() } + private fun TestScope.selectUser(userInfo: UserInfo) { + kosmos.fakeUserRepository.selectedUser.value = + SelectedUserModel( + userInfo = userInfo, + selectionStatus = SelectionStatus.SELECTION_COMPLETE + ) + advanceTimeBy(PasswordBouncerViewModel.DELAY_TO_FETCH_IMES) + } + + private suspend fun enableInputMethodsForUser(userId: Int) { + kosmos.fakeInputMethodRepository.setEnabledInputMethods( + userId, + createInputMethodWithSubtypes(auxiliarySubtypes = 0, nonAuxiliarySubtypes = 0), + createInputMethodWithSubtypes(auxiliarySubtypes = 0, nonAuxiliarySubtypes = 1), + ) + assertThat(inputMethodInteractor.hasMultipleEnabledImesOrSubtypes(userId)).isTrue() + } + + private fun createInputMethodWithSubtypes( + auxiliarySubtypes: Int, + nonAuxiliarySubtypes: Int, + ): InputMethodModel { + return InputMethodModel( + imeId = UUID.randomUUID().toString(), + subtypes = + List(auxiliarySubtypes + nonAuxiliarySubtypes) { + InputMethodModel.Subtype(subtypeId = it, isAuxiliary = it < auxiliarySubtypes) + } + ) + } + companion object { private const val ENTER_YOUR_PASSWORD = "Enter your password" private const val WRONG_PASSWORD = "Wrong password" + + private val USER_INFOS = + listOf( + UserInfo(100, "First user", 0), + UserInfo(101, "Second user", 0), + ) } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt index 55016bb1fc07..25a287c4cfff 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinInputViewModelTest.kt @@ -24,6 +24,7 @@ import org.junit.runner.RunWith */ @SmallTest @RunWith(AndroidJUnit4::class) +@android.platform.test.annotations.EnabledOnRavenwood class PinInputViewModelTest : SysuiTestCase() { @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt new file mode 100644 index 000000000000..a8fe16b12e1b --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/CommunalSceneStartableTest.kt @@ -0,0 +1,252 @@ +/* + * 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.communal + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.domain.interactor.communalInteractor +import com.android.systemui.communal.shared.model.CommunalSceneKey +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.dock.DockManager +import com.android.systemui.dock.dockManager +import com.android.systemui.dock.fakeDockManager +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class CommunalSceneStartableTest : SysuiTestCase() { + private val kosmos = testKosmos() + + private lateinit var underTest: CommunalSceneStartable + + @Before + fun setUp() = + with(kosmos) { + underTest = + CommunalSceneStartable( + dockManager = dockManager, + communalInteractor = communalInteractor, + keyguardTransitionInteractor = keyguardTransitionInteractor, + applicationScope = applicationCoroutineScope, + bgScope = applicationCoroutineScope, + ) + .apply { start() } + } + + @Test + fun keyguardGoesAway_forceBlankScene() = + with(kosmos) { + testScope.runTest { + val scene by collectLastValue(communalInteractor.desiredScene) + + communalInteractor.onSceneChanged(CommunalSceneKey.Communal) + assertThat(scene).isEqualTo(CommunalSceneKey.Communal) + + fakeKeyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.PRIMARY_BOUNCER, + to = KeyguardState.GONE, + testScope = this + ) + + assertThat(scene).isEqualTo(CommunalSceneKey.Blank) + } + } + + @Test + fun deviceDreaming_forceBlankScene() = + with(kosmos) { + testScope.runTest { + val scene by collectLastValue(communalInteractor.desiredScene) + + communalInteractor.onSceneChanged(CommunalSceneKey.Communal) + assertThat(scene).isEqualTo(CommunalSceneKey.Communal) + + fakeKeyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.GLANCEABLE_HUB, + to = KeyguardState.DREAMING, + testScope = this + ) + + assertThat(scene).isEqualTo(CommunalSceneKey.Blank) + } + } + + @Test + fun deviceDocked_forceCommunalScene() = + with(kosmos) { + testScope.runTest { + val scene by collectLastValue(communalInteractor.desiredScene) + assertThat(scene).isEqualTo(CommunalSceneKey.Blank) + + updateDocked(true) + fakeKeyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.GONE, + to = KeyguardState.LOCKSCREEN, + testScope = this + ) + assertThat(scene).isEqualTo(CommunalSceneKey.Communal) + + fakeKeyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.GLANCEABLE_HUB, + to = KeyguardState.DREAMING, + testScope = this + ) + assertThat(scene).isEqualTo(CommunalSceneKey.Blank) + } + } + + @Test + fun deviceDocked_doesNotForceCommunalIfTransitioningFromCommunal() = + with(kosmos) { + testScope.runTest { + val scene by collectLastValue(communalInteractor.desiredScene) + assertThat(scene).isEqualTo(CommunalSceneKey.Blank) + + updateDocked(true) + fakeKeyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.GLANCEABLE_HUB, + to = KeyguardState.LOCKSCREEN, + testScope = this + ) + assertThat(scene).isEqualTo(CommunalSceneKey.Blank) + } + } + + @Test + fun deviceAsleep_forceBlankSceneAfterTimeout() = + with(kosmos) { + testScope.runTest { + val scene by collectLastValue(communalInteractor.desiredScene) + communalInteractor.onSceneChanged(CommunalSceneKey.Communal) + assertThat(scene).isEqualTo(CommunalSceneKey.Communal) + + fakeKeyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.GLANCEABLE_HUB, + to = KeyguardState.OFF, + testScope = this + ) + assertThat(scene).isEqualTo(CommunalSceneKey.Communal) + + advanceTimeBy(CommunalSceneStartable.AWAKE_DEBOUNCE_DELAY) + + assertThat(scene).isEqualTo(CommunalSceneKey.Blank) + } + } + + @Test + fun deviceAsleep_wakesUpBeforeTimeout_noChangeInScene() = + with(kosmos) { + testScope.runTest { + val scene by collectLastValue(communalInteractor.desiredScene) + communalInteractor.onSceneChanged(CommunalSceneKey.Communal) + assertThat(scene).isEqualTo(CommunalSceneKey.Communal) + + fakeKeyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.GLANCEABLE_HUB, + to = KeyguardState.OFF, + testScope = this + ) + assertThat(scene).isEqualTo(CommunalSceneKey.Communal) + advanceTimeBy(CommunalSceneStartable.AWAKE_DEBOUNCE_DELAY / 2) + assertThat(scene).isEqualTo(CommunalSceneKey.Communal) + + fakeKeyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.OFF, + to = KeyguardState.GLANCEABLE_HUB, + testScope = this + ) + + advanceTimeBy(CommunalSceneStartable.AWAKE_DEBOUNCE_DELAY) + assertThat(scene).isEqualTo(CommunalSceneKey.Communal) + } + } + + @Test + fun dockingOnLockscreen_forcesCommunal() = + with(kosmos) { + testScope.runTest { + communalInteractor.onSceneChanged(CommunalSceneKey.Blank) + val scene by collectLastValue(communalInteractor.desiredScene) + + // device is docked while on the lockscreen + fakeKeyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.GLANCEABLE_HUB, + to = KeyguardState.LOCKSCREEN, + testScope = this + ) + updateDocked(true) + + assertThat(scene).isEqualTo(CommunalSceneKey.Blank) + advanceTimeBy(CommunalSceneStartable.DOCK_DEBOUNCE_DELAY) + assertThat(scene).isEqualTo(CommunalSceneKey.Communal) + } + } + + @Test + fun dockingOnLockscreen_doesNotForceCommunalIfDreamStarts() = + with(kosmos) { + testScope.runTest { + communalInteractor.onSceneChanged(CommunalSceneKey.Blank) + val scene by collectLastValue(communalInteractor.desiredScene) + + // device is docked while on the lockscreen + fakeKeyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.GLANCEABLE_HUB, + to = KeyguardState.LOCKSCREEN, + testScope = this + ) + updateDocked(true) + + assertThat(scene).isEqualTo(CommunalSceneKey.Blank) + advanceTimeBy(CommunalSceneStartable.DOCK_DEBOUNCE_DELAY / 2) + assertThat(scene).isEqualTo(CommunalSceneKey.Blank) + + // dream starts shortly after docking + fakeKeyguardTransitionRepository.sendTransitionSteps( + from = KeyguardState.LOCKSCREEN, + to = KeyguardState.DREAMING, + testScope = this + ) + advanceTimeBy(CommunalSceneStartable.DOCK_DEBOUNCE_DELAY) + assertThat(scene).isEqualTo(CommunalSceneKey.Blank) + } + } + + private fun TestScope.updateDocked(docked: Boolean) = + with(kosmos) { + runCurrent() + fakeDockManager.setIsDocked(docked) + fakeDockManager.setDockEvent(DockManager.STATE_DOCKED) + runCurrent() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt index 92b75cb0f47d..76b0d4aaa8ca 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalMediaRepositoryImplTest.kt @@ -40,6 +40,7 @@ import org.mockito.MockitoAnnotations @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) +@android.platform.test.annotations.EnabledOnRavenwood class CommunalMediaRepositoryImplTest : SysuiTestCase() { @Mock private lateinit var mediaDataManager: MediaDataManager @Mock private lateinit var mediaData: MediaData diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt index 820bfbfdf0a2..d15e15e179fb 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt @@ -39,6 +39,7 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) +@android.platform.test.annotations.EnabledOnRavenwood class CommunalPrefsRepositoryImplTest : SysuiTestCase() { private lateinit var underTest: CommunalPrefsRepositoryImpl diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryImplTest.kt index c4a8582d51b5..0c66bbb63439 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalTutorialRepositoryImplTest.kt @@ -38,6 +38,7 @@ import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidJUnit4::class) +@android.platform.test.annotations.EnabledOnRavenwood class CommunalTutorialRepositoryImplTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/widgets/CommunalAppWidgetHostTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/widgets/CommunalAppWidgetHostTest.kt index 3aa99c40bd67..89a4c04015b6 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/widgets/CommunalAppWidgetHostTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/widgets/CommunalAppWidgetHostTest.kt @@ -23,16 +23,27 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.communal.widgets.CommunalAppWidgetHost import com.android.systemui.communal.widgets.CommunalAppWidgetHostView +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testScope +import com.android.systemui.log.logcatLogBuffer +import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWithLooper(setAsMainLooper = true) @RunWith(AndroidJUnit4::class) class CommunalAppWidgetHostTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope private lateinit var testableLooper: TestableLooper private lateinit var underTest: CommunalAppWidgetHost @@ -43,9 +54,11 @@ class CommunalAppWidgetHostTest : SysuiTestCase() { underTest = CommunalAppWidgetHost( context = context, + backgroundScope = kosmos.applicationCoroutineScope, hostId = 116, interactionHandler = mock(), - looper = testableLooper.looper + looper = testableLooper.looper, + logBuffer = logcatLogBuffer("CommunalAppWidgetHostTest"), ) } @@ -64,4 +77,23 @@ class CommunalAppWidgetHostTest : SysuiTestCase() { assertThat(view).isNotNull() assertThat(view.appWidgetId).isEqualTo(appWidgetId) } + + @Test + fun appWidgetIdToRemove_emit() = + testScope.runTest { + val appWidgetIdToRemove by collectLastValue(underTest.appWidgetIdToRemove) + + // Nothing should be emitted yet + assertThat(appWidgetIdToRemove).isNull() + + underTest.onAppWidgetRemoved(appWidgetId = 1) + runCurrent() + + assertThat(appWidgetIdToRemove).isEqualTo(1) + + underTest.onAppWidgetRemoved(appWidgetId = 2) + runCurrent() + + assertThat(appWidgetIdToRemove).isEqualTo(2) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt index a3654b6e8963..032d76f0dceb 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartableTest.kt @@ -21,14 +21,21 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.communal.data.repository.fakeCommunalRepository +import com.android.systemui.communal.data.repository.fakeCommunalWidgetRepository import com.android.systemui.communal.domain.interactor.communalInteractor +import com.android.systemui.communal.shared.model.CommunalWidgetContentModel +import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.testKosmos import com.android.systemui.user.data.repository.fakeUserRepository +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before @@ -47,6 +54,8 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() { @Mock private lateinit var appWidgetHost: CommunalAppWidgetHost + private lateinit var appWidgetIdToRemove: MutableSharedFlow<Int> + private lateinit var underTest: CommunalAppWidgetHostStartable @Before @@ -54,6 +63,9 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() { MockitoAnnotations.initMocks(this) kosmos.fakeUserRepository.setUserInfos(listOf(MAIN_USER_INFO)) + appWidgetIdToRemove = MutableSharedFlow() + whenever(appWidgetHost.appWidgetIdToRemove).thenReturn(appWidgetIdToRemove) + underTest = CommunalAppWidgetHostStartable( appWidgetHost, @@ -120,6 +132,38 @@ class CommunalAppWidgetHostStartableTest : SysuiTestCase() { } } + @Test + fun removeAppWidgetReportedByHost() = + with(kosmos) { + testScope.runTest { + // Set up communal widgets + val widget1 = + mock<CommunalWidgetContentModel> { whenever(this.appWidgetId).thenReturn(1) } + val widget2 = + mock<CommunalWidgetContentModel> { whenever(this.appWidgetId).thenReturn(2) } + val widget3 = + mock<CommunalWidgetContentModel> { whenever(this.appWidgetId).thenReturn(3) } + fakeCommunalWidgetRepository.setCommunalWidgets(listOf(widget1, widget2, widget3)) + + underTest.start() + + // Assert communal widgets has 3 + val communalWidgets by + collectLastValue(fakeCommunalWidgetRepository.communalWidgets) + assertThat(communalWidgets).containsExactly(widget1, widget2, widget3) + + // Report app widget 1 to remove and assert widget removed + appWidgetIdToRemove.emit(1) + runCurrent() + assertThat(communalWidgets).containsExactly(widget2, widget3) + + // Report app widget 3 to remove and assert widget removed + appWidgetIdToRemove.emit(3) + runCurrent() + assertThat(communalWidgets).containsExactly(widget2) + } + } + private suspend fun setCommunalAvailable(available: Boolean) = with(kosmos) { fakeKeyguardRepository.setIsEncryptedOrLockdown(false) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepositoryTest.kt index b54c5bdae004..9536084fdb23 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryRepositoryTest.kt @@ -32,6 +32,7 @@ import org.mockito.MockitoAnnotations @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) +@android.platform.test.annotations.EnabledOnRavenwood class DeviceEntryRepositoryTest : SysuiTestCase() { @Mock private lateinit var lockPatternUtils: LockPatternUtils diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt index 32943a19be28..51db4513bb39 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceUnlockedInteractorTest.kt @@ -33,6 +33,7 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) +@android.platform.test.annotations.EnabledOnRavenwood class DeviceUnlockedInteractorTest : SysuiTestCase() { private val kosmos = testKosmos() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/ui/binder/LiftToRunFaceAuthBinderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/ui/binder/LiftToRunFaceAuthBinderTest.kt new file mode 100644 index 000000000000..e9e85c9f210b --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/ui/binder/LiftToRunFaceAuthBinderTest.kt @@ -0,0 +1,200 @@ +/* + * 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.deviceentry.domain.ui.binder + +import android.content.packageManager +import android.content.pm.PackageManager +import android.hardware.Sensor +import android.hardware.TriggerEventListener +import android.testing.TestableLooper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.bouncer.data.repository.keyguardBouncerRepository +import com.android.systemui.deviceentry.ui.binder.liftToRunFaceAuthBinder +import com.android.systemui.keyguard.data.repository.biometricSettingsRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.kosmos.testScope +import com.android.systemui.power.data.repository.fakePowerRepository +import com.android.systemui.testKosmos +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.capture +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.sensors.asyncSensorManager +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.Mockito.clearInvocations +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@ExperimentalCoroutinesApi +@SmallTest +@RunWith(AndroidJUnit4::class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +class LiftToRunFaceAuthBinderTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val sensorManager = kosmos.asyncSensorManager + private val powerRepository = kosmos.fakePowerRepository + private val keyguardRepository = kosmos.fakeKeyguardRepository + private val bouncerRepository = kosmos.keyguardBouncerRepository + private val biometricSettingsRepository = kosmos.biometricSettingsRepository + private val packageManager = kosmos.packageManager + + @Captor private lateinit var triggerEventListenerCaptor: ArgumentCaptor<TriggerEventListener> + @Mock private lateinit var mockSensor: Sensor + + private val underTest = kosmos.liftToRunFaceAuthBinder + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + whenever(packageManager.hasSystemFeature(PackageManager.FEATURE_FACE)).thenReturn(true) + whenever(sensorManager.getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE)).thenReturn(mockSensor) + } + + @Test + fun doNotListenForGesture() = + testScope.runTest { + start() + verifyNeverRequestsTriggerSensor() + } + + @Test + fun awakeKeyguard_listenForGesture() = + testScope.runTest { + start() + givenAwakeKeyguard(true) + runCurrent() + verifyRequestTriggerSensor() + } + + @Test + fun faceNotEnrolled_listenForGesture() = + testScope.runTest { + start() + givenAwakeKeyguard(true) + biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false) + runCurrent() + verifyNeverRequestsTriggerSensor() + } + + @Test + fun notInteractive_doNotListenForGesture() = + testScope.runTest { + start() + givenAwakeKeyguard(true) + powerRepository.setInteractive(false) + runCurrent() + verifyNeverRequestsTriggerSensor() + } + + @Test + fun primaryBouncer_listenForGesture() = + testScope.runTest { + start() + givenAwakeKeyguard(false) + givenPrimaryBouncerShowing() + runCurrent() + verifyRequestTriggerSensor() + } + + @Test + fun alternateBouncer_listenForGesture() = + testScope.runTest { + start() + givenAwakeKeyguard(false) + givenAlternateBouncerShowing() + runCurrent() + verifyRequestTriggerSensor() + } + + @Test + fun restartListeningForGestureAfterSensorTrigger() = + testScope.runTest { + start() + givenAwakeKeyguard(true) + runCurrent() + verifyRequestTriggerSensor() + clearInvocations(sensorManager) + + triggerEventListenerCaptor.value.onTrigger(null) + runCurrent() + verifyRequestTriggerSensor() + } + + @Test + fun cancelTriggerSensor_keyguardNotAwakeAnymore() = + testScope.runTest { + start() + givenAwakeKeyguard(true) + runCurrent() + verifyRequestTriggerSensor() + + givenAwakeKeyguard(false) + runCurrent() + verifyCancelTriggerSensor() + } + + private fun start() { + underTest.start() + biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true) + givenAwakeKeyguard(false) + givenBouncerNotShowing() + } + + private fun givenAwakeKeyguard(isAwake: Boolean) { + powerRepository.setInteractive(isAwake) + keyguardRepository.setKeyguardShowing(isAwake) + keyguardRepository.setKeyguardOccluded(false) + } + + private fun givenPrimaryBouncerShowing() { + bouncerRepository.setPrimaryShow(true) + bouncerRepository.setAlternateVisible(false) + } + + private fun givenBouncerNotShowing() { + bouncerRepository.setPrimaryShow(false) + bouncerRepository.setAlternateVisible(false) + } + + private fun givenAlternateBouncerShowing() { + bouncerRepository.setPrimaryShow(false) + bouncerRepository.setAlternateVisible(true) + } + + private fun verifyRequestTriggerSensor() { + verify(sensorManager).requestTriggerSensor(capture(triggerEventListenerCaptor), any()) + } + + private fun verifyNeverRequestsTriggerSensor() { + verify(sensorManager, never()).requestTriggerSensor(any(), any()) + } + + private fun verifyCancelTriggerSensor() { + verify(sensorManager).cancelTriggerSensor(any(), any()) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dock/DockManagerFakeKosmos.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dock/DockManagerFakeKosmos.kt new file mode 100644 index 000000000000..06275fa226a5 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dock/DockManagerFakeKosmos.kt @@ -0,0 +1,22 @@ +/* + * 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.dock + +import com.android.systemui.kosmos.Kosmos + +val Kosmos.dockManager: DockManager by Kosmos.Fixture { fakeDockManager } +val Kosmos.fakeDockManager: DockManagerFake by Kosmos.Fixture { DockManagerFake() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayCallbackControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayCallbackControllerTest.kt index 2c6c793c97f5..d9dcfdc7becb 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayCallbackControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayCallbackControllerTest.kt @@ -31,6 +31,7 @@ import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidJUnit4::class) +@android.platform.test.annotations.EnabledOnRavenwood class DreamOverlayCallbackControllerTest : SysuiTestCase() { @Mock private lateinit var callback: DreamOverlayCallbackController.Callback diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayNotificationCountProviderTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayNotificationCountProviderTest.java index d379dc6f3dc1..5ae8595581e5 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayNotificationCountProviderTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayNotificationCountProviderTest.java @@ -39,6 +39,7 @@ import org.mockito.MockitoAnnotations; @SmallTest @RunWith(AndroidJUnit4.class) +@android.platform.test.annotations.EnabledOnRavenwood public class DreamOverlayNotificationCountProviderTest extends SysuiTestCase { @Mock NotificationListener mNotificationListener; diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java index 8bf878c23cde..b46f2aa61518 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayStateControllerTest.java @@ -50,6 +50,7 @@ import java.util.Collection; @SmallTest @RunWith(AndroidJUnit4.class) +@android.platform.test.annotations.EnabledOnRavenwood public class DreamOverlayStateControllerTest extends SysuiTestCase { @Mock DreamOverlayStateController.Callback mCallback; diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayStatusBarItemsProviderTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayStatusBarItemsProviderTest.java index 7ff345cdcf7e..ad353ce39918 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayStatusBarItemsProviderTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayStatusBarItemsProviderTest.java @@ -37,6 +37,7 @@ import java.util.concurrent.Executor; @SmallTest @RunWith(AndroidJUnit4.class) +@android.platform.test.annotations.EnabledOnRavenwood public class DreamOverlayStatusBarItemsProviderTest extends SysuiTestCase { @Mock DreamOverlayStatusBarItemsProvider.Callback mCallback; diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/conditions/AssistantAttentionConditionTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/conditions/AssistantAttentionConditionTest.java index e0c6ab20c6e1..cb5702ada88e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/conditions/AssistantAttentionConditionTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/conditions/AssistantAttentionConditionTest.java @@ -42,6 +42,7 @@ import kotlinx.coroutines.CoroutineScope; @SmallTest @RunWith(AndroidJUnit4.class) +@android.platform.test.annotations.EnabledOnRavenwood public class AssistantAttentionConditionTest extends SysuiTestCase { @Mock Condition.Callback mCallback; diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/conditions/DreamConditionTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/conditions/DreamConditionTest.java index 480754c17616..96d3c9397228 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/conditions/DreamConditionTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/conditions/DreamConditionTest.java @@ -45,6 +45,7 @@ import kotlinx.coroutines.CoroutineScope; @SmallTest @RunWith(AndroidJUnit4.class) +@android.platform.test.annotations.EnabledOnRavenwood public class DreamConditionTest extends SysuiTestCase { @Mock Condition.Callback mCallback; diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorKosmos.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorKosmos.kt new file mode 100644 index 000000000000..efccf7a81ccd --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorKosmos.kt @@ -0,0 +1,41 @@ +/* + * 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.dreams.homecontrols + +import com.android.systemui.controls.dagger.ControlsComponent +import com.android.systemui.controls.management.ControlsListingController +import com.android.systemui.controls.panels.AuthorizedPanelsRepository +import com.android.systemui.controls.panels.selectedComponentRepository +import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.user.data.repository.fakeUserRepository +import com.android.systemui.util.mockito.mock + +val Kosmos.homeControlsComponentInteractor by + Kosmos.Fixture { + HomeControlsComponentInteractor( + selectedComponentRepository = selectedComponentRepository, + controlsComponent, + authorizedPanelsRepository = authorizedPanelsRepository, + userRepository = fakeUserRepository, + bgScope = applicationCoroutineScope, + ) + } + +val Kosmos.controlsComponent by Kosmos.Fixture<ControlsComponent> { mock() } +val Kosmos.controlsListingController by Kosmos.Fixture<ControlsListingController> { mock() } +val Kosmos.authorizedPanelsRepository by Kosmos.Fixture<AuthorizedPanelsRepository> { mock() } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorTest.kt new file mode 100644 index 000000000000..ce74a905ef66 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsComponentInteractorTest.kt @@ -0,0 +1,255 @@ +/* + * 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.dreams.homecontrols + +import android.content.ComponentName +import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.ServiceInfo +import android.content.pm.UserInfo +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.controls.ControlsServiceInfo +import com.android.systemui.controls.dagger.ControlsComponent +import com.android.systemui.controls.management.ControlsListingController +import com.android.systemui.controls.panels.AuthorizedPanelsRepository +import com.android.systemui.controls.panels.FakeSelectedComponentRepository +import com.android.systemui.controls.panels.SelectedComponentRepository +import com.android.systemui.controls.panels.selectedComponentRepository +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.FakeUserRepository +import com.android.systemui.user.data.repository.fakeUserRepository +import com.android.systemui.util.mockito.whenever +import com.android.systemui.util.mockito.withArgCaptor +import com.google.common.truth.Truth.assertThat +import java.util.Optional +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidJUnit4::class) +class HomeControlsComponentInteractorTest : SysuiTestCase() { + + private val kosmos = testKosmos() + + private lateinit var controlsComponent: ControlsComponent + private lateinit var controlsListingController: ControlsListingController + private lateinit var authorizedPanelsRepository: AuthorizedPanelsRepository + private lateinit var underTest: HomeControlsComponentInteractor + private lateinit var userRepository: FakeUserRepository + private lateinit var selectedComponentRepository: FakeSelectedComponentRepository + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + userRepository = kosmos.fakeUserRepository + userRepository.setUserInfos(listOf(PRIMARY_USER, ANOTHER_USER)) + + controlsComponent = kosmos.controlsComponent + authorizedPanelsRepository = kosmos.authorizedPanelsRepository + controlsListingController = kosmos.controlsListingController + selectedComponentRepository = kosmos.selectedComponentRepository + + selectedComponentRepository.setCurrentUserHandle(PRIMARY_USER.userHandle) + whenever(controlsComponent.getControlsListingController()) + .thenReturn(Optional.of(controlsListingController)) + + underTest = + HomeControlsComponentInteractor( + selectedComponentRepository, + controlsComponent, + authorizedPanelsRepository, + userRepository, + kosmos.applicationCoroutineScope, + ) + } + + @Test + fun testPanelComponentReturnsComponentNameForSelectedItemByUser() = + with(kosmos) { + testScope.runTest { + whenever(authorizedPanelsRepository.getAuthorizedPanels()) + .thenReturn(setOf(TEST_PACKAGE_PANEL)) + userRepository.setSelectedUserInfo(PRIMARY_USER) + selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL) + val actualValue by collectLastValue(underTest.panelComponent) + assertThat(actualValue).isNull() + runServicesUpdate() + assertThat(actualValue).isEqualTo(TEST_COMPONENT_PANEL) + } + } + + @Test + fun testPanelComponentReturnsComponentNameAsInitialValueWithoutServiceUpdate() = + with(kosmos) { + testScope.runTest { + whenever(authorizedPanelsRepository.getAuthorizedPanels()) + .thenReturn(setOf(TEST_PACKAGE_PANEL)) + userRepository.setSelectedUserInfo(PRIMARY_USER) + selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL) + whenever(controlsListingController.getCurrentServices()) + .thenReturn( + listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true)) + ) + val actualValue by collectLastValue(underTest.panelComponent) + assertThat(actualValue).isEqualTo(TEST_COMPONENT_PANEL) + } + } + + @Test + fun testPanelComponentReturnsNullForHomeControlsThatDoesNotSupportPanel() = + with(kosmos) { + testScope.runTest { + whenever(authorizedPanelsRepository.getAuthorizedPanels()) + .thenReturn(setOf(TEST_PACKAGE_PANEL)) + userRepository.setSelectedUserInfo(PRIMARY_USER) + selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_NON_PANEL) + val actualValue by collectLastValue(underTest.panelComponent) + assertThat(actualValue).isNull() + runServicesUpdate(false) + assertThat(actualValue).isNull() + } + } + + @Test + fun testPanelComponentReturnsNullWhenPanelIsUnauthorized() = + with(kosmos) { + testScope.runTest { + whenever(authorizedPanelsRepository.getAuthorizedPanels()).thenReturn(setOf()) + userRepository.setSelectedUserInfo(PRIMARY_USER) + selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL) + val actualValue by collectLastValue(underTest.panelComponent) + assertThat(actualValue).isNull() + runServicesUpdate() + assertThat(actualValue).isNull() + } + } + + @Test + fun testPanelComponentReturnsComponentNameForDifferentUsers() = + with(kosmos) { + testScope.runTest { + whenever(authorizedPanelsRepository.getAuthorizedPanels()) + .thenReturn(setOf(TEST_PACKAGE_PANEL)) + userRepository.setSelectedUserInfo(ANOTHER_USER) + selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_NON_PANEL) + selectedComponentRepository.setCurrentUserHandle(ANOTHER_USER.userHandle) + selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL) + + val actualValue by collectLastValue(underTest.panelComponent) + assertThat(actualValue).isNull() + runServicesUpdate() + assertThat(actualValue).isEqualTo(TEST_COMPONENT_PANEL) + } + } + + @Test + fun testPanelComponentReturnsNullWhenControlsComponentReturnsNullForListingController() = + with(kosmos) { + testScope.runTest { + whenever(authorizedPanelsRepository.getAuthorizedPanels()) + .thenReturn(setOf(TEST_PACKAGE_PANEL)) + whenever(controlsComponent.getControlsListingController()) + .thenReturn(Optional.empty()) + userRepository.setSelectedUserInfo(PRIMARY_USER) + selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL) + val actualValue by collectLastValue(underTest.panelComponent) + assertThat(actualValue).isNull() + } + } + + private fun runServicesUpdate(hasPanelBoolean: Boolean = true) { + val listings = + listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = hasPanelBoolean)) + val callback = withArgCaptor { verify(controlsListingController).addCallback(capture()) } + callback.onServicesUpdated(listings) + } + + private fun ControlsServiceInfo( + componentName: ComponentName, + label: CharSequence, + hasPanel: Boolean + ): ControlsServiceInfo { + val serviceInfo = + ServiceInfo().apply { + applicationInfo = ApplicationInfo() + packageName = componentName.packageName + name = componentName.className + } + return FakeControlsServiceInfo(context, serviceInfo, label, hasPanel) + } + + private class FakeControlsServiceInfo( + context: Context, + serviceInfo: ServiceInfo, + private val label: CharSequence, + hasPanel: Boolean + ) : ControlsServiceInfo(context, serviceInfo) { + + init { + if (hasPanel) { + panelActivity = serviceInfo.componentName + } + } + + override fun loadLabel(): CharSequence { + return label + } + } + + companion object { + private const val PRIMARY_USER_ID = 0 + private val PRIMARY_USER = + UserInfo( + /* id= */ PRIMARY_USER_ID, + /* name= */ "primary user", + /* flags= */ UserInfo.FLAG_PRIMARY + ) + + private const val ANOTHER_USER_ID = 1 + private val ANOTHER_USER = + UserInfo( + /* id= */ ANOTHER_USER_ID, + /* name= */ "another user", + /* flags= */ UserInfo.FLAG_PRIMARY + ) + private const val TEST_PACKAGE = "pkg" + private val TEST_COMPONENT = ComponentName(TEST_PACKAGE, "service") + private const val TEST_PACKAGE_PANEL = "pkg.panel" + private val TEST_COMPONENT_PANEL = ComponentName(TEST_PACKAGE_PANEL, "service") + private val TEST_SELECTED_COMPONENT_PANEL = + SelectedComponentRepository.SelectedComponent( + TEST_PACKAGE_PANEL, + TEST_COMPONENT_PANEL, + true + ) + private val TEST_SELECTED_COMPONENT_NON_PANEL = + SelectedComponentRepository.SelectedComponent( + TEST_PACKAGE_PANEL, + TEST_COMPONENT_PANEL, + false + ) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamServiceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamServiceTest.kt new file mode 100644 index 000000000000..d28b6bf39f30 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamServiceTest.kt @@ -0,0 +1,120 @@ +/* + * 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.dreams.homecontrols + +import android.app.Activity +import android.content.ComponentName +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.controls.dagger.ControlsComponent +import com.android.systemui.controls.management.ControlsListingController +import com.android.systemui.controls.settings.FakeControlsSettingsRepository +import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.FakeLogBuffer.Factory.Companion.create +import com.android.systemui.testKosmos +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.whenever +import java.util.Optional +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidJUnit4::class) +class HomeControlsDreamServiceTest : SysuiTestCase() { + + private val kosmos = testKosmos() + + private lateinit var controlsSettingsRepository: FakeControlsSettingsRepository + @Mock private lateinit var taskFragmentComponentFactory: TaskFragmentComponent.Factory + @Mock private lateinit var taskFragmentComponent: TaskFragmentComponent + @Mock private lateinit var activity: Activity + private val logBuffer: LogBuffer = create() + + private lateinit var underTest: HomeControlsDreamService + private lateinit var homeControlsComponentInteractor: HomeControlsComponentInteractor + private lateinit var fakeDreamActivityProvider: DreamActivityProvider + private lateinit var controlsComponent: ControlsComponent + private lateinit var controlsListingController: ControlsListingController + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + whenever(taskFragmentComponentFactory.create(any(), any(), any(), any())) + .thenReturn(taskFragmentComponent) + + controlsSettingsRepository = FakeControlsSettingsRepository() + controlsSettingsRepository.setAllowActionOnTrivialControlsInLockscreen(true) + + controlsComponent = kosmos.controlsComponent + controlsListingController = kosmos.controlsListingController + + whenever(controlsComponent.getControlsListingController()) + .thenReturn(Optional.of(controlsListingController)) + + homeControlsComponentInteractor = kosmos.homeControlsComponentInteractor + + fakeDreamActivityProvider = DreamActivityProvider { activity } + underTest = + HomeControlsDreamService( + controlsSettingsRepository, + taskFragmentComponentFactory, + homeControlsComponentInteractor, + fakeDreamActivityProvider, + logBuffer + ) + } + + @Test + fun testOnAttachedToWindowCreatesTaskFragmentComponent() { + underTest.onAttachedToWindow() + verify(taskFragmentComponentFactory).create(any(), any(), any(), any()) + } + + @Test + fun testOnDetachedFromWindowDestroyTaskFragmentComponent() { + underTest.onAttachedToWindow() + underTest.onDetachedFromWindow() + verify(taskFragmentComponent).destroy() + } + + @Test + fun testNotCreatingTaskFragmentComponentWhenActivityIsNull() { + fakeDreamActivityProvider = DreamActivityProvider { null } + underTest = + HomeControlsDreamService( + controlsSettingsRepository, + taskFragmentComponentFactory, + homeControlsComponentInteractor, + fakeDreamActivityProvider, + logBuffer + ) + + underTest.onAttachedToWindow() + verify(taskFragmentComponentFactory, never()).create(any(), any(), any(), any()) + } + + companion object { + private const val TEST_PACKAGE_PANEL = "pkg.panel" + private val TEST_COMPONENT_PANEL = ComponentName(TEST_PACKAGE_PANEL, "service") + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartableTest.kt new file mode 100644 index 000000000000..6610e7007eb2 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartableTest.kt @@ -0,0 +1,202 @@ +/* + * 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.dreams.homecontrols + +import android.content.ComponentName +import android.content.Context +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.content.pm.ServiceInfo +import android.content.pm.UserInfo +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.service.controls.flags.Flags.FLAG_HOME_PANEL_DREAM +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.controls.ControlsServiceInfo +import com.android.systemui.controls.dagger.ControlsComponent +import com.android.systemui.controls.management.ControlsListingController +import com.android.systemui.controls.panels.AuthorizedPanelsRepository +import com.android.systemui.controls.panels.SelectedComponentRepository +import com.android.systemui.controls.panels.selectedComponentRepository +import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.FakeUserRepository +import com.android.systemui.user.data.repository.fakeUserRepository +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.whenever +import java.util.Optional +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.never +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidJUnit4::class) +class HomeControlsDreamStartableTest : SysuiTestCase() { + + private val kosmos = testKosmos() + + @Mock private lateinit var packageManager: PackageManager + + private lateinit var homeControlsComponentInteractor: HomeControlsComponentInteractor + private lateinit var selectedComponentRepository: SelectedComponentRepository + private lateinit var authorizedPanelsRepository: AuthorizedPanelsRepository + private lateinit var userRepository: FakeUserRepository + private lateinit var controlsComponent: ControlsComponent + private lateinit var controlsListingController: ControlsListingController + + private lateinit var startable: HomeControlsDreamStartable + private val componentName = ComponentName(context, HomeControlsDreamService::class.java) + private val testScope = kosmos.testScope + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + selectedComponentRepository = kosmos.selectedComponentRepository + authorizedPanelsRepository = kosmos.authorizedPanelsRepository + userRepository = kosmos.fakeUserRepository + controlsComponent = kosmos.controlsComponent + controlsListingController = kosmos.controlsListingController + + userRepository.setUserInfos(listOf(PRIMARY_USER)) + + whenever(authorizedPanelsRepository.getAuthorizedPanels()) + .thenReturn(setOf(TEST_PACKAGE_PANEL)) + + whenever(controlsComponent.getControlsListingController()) + .thenReturn(Optional.of(controlsListingController)) + whenever(controlsListingController.getCurrentServices()) + .thenReturn(listOf(ControlsServiceInfo(TEST_COMPONENT_PANEL, "panel", hasPanel = true))) + + homeControlsComponentInteractor = kosmos.homeControlsComponentInteractor + + startable = + HomeControlsDreamStartable( + mContext, + packageManager, + homeControlsComponentInteractor, + kosmos.applicationCoroutineScope + ) + } + + @Test + @EnableFlags(FLAG_HOME_PANEL_DREAM) + fun testStartEnablesHomeControlsDreamServiceWhenPanelComponentIsNotNull() = + testScope.runTest { + userRepository.setSelectedUserInfo(PRIMARY_USER) + selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_PANEL) + startable.start() + runCurrent() + verify(packageManager) + .setComponentEnabledSetting( + eq(componentName), + eq(PackageManager.COMPONENT_ENABLED_STATE_ENABLED), + eq(PackageManager.DONT_KILL_APP) + ) + } + + @Test + @EnableFlags(FLAG_HOME_PANEL_DREAM) + fun testStartDisablesHomeControlsDreamServiceWhenPanelComponentIsNull() = + testScope.runTest { + selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_NON_PANEL) + startable.start() + runCurrent() + verify(packageManager) + .setComponentEnabledSetting( + eq(componentName), + eq(PackageManager.COMPONENT_ENABLED_STATE_DISABLED), + eq(PackageManager.DONT_KILL_APP) + ) + } + + @Test + @DisableFlags(FLAG_HOME_PANEL_DREAM) + fun testStartDoesNotRunDreamServiceWhenFlagIsDisabled() = + testScope.runTest { + selectedComponentRepository.setSelectedComponent(TEST_SELECTED_COMPONENT_NON_PANEL) + startable.start() + runCurrent() + verify(packageManager, never()).setComponentEnabledSetting(any(), any(), any()) + } + + private fun ControlsServiceInfo( + componentName: ComponentName, + label: CharSequence, + hasPanel: Boolean + ): ControlsServiceInfo { + val serviceInfo = + ServiceInfo().apply { + applicationInfo = ApplicationInfo() + packageName = componentName.packageName + name = componentName.className + } + return FakeControlsServiceInfo(context, serviceInfo, label, hasPanel) + } + + private class FakeControlsServiceInfo( + context: Context, + serviceInfo: ServiceInfo, + private val label: CharSequence, + hasPanel: Boolean + ) : ControlsServiceInfo(context, serviceInfo) { + + init { + if (hasPanel) { + panelActivity = serviceInfo.componentName + } + } + + override fun loadLabel(): CharSequence { + return label + } + } + + companion object { + private const val PRIMARY_USER_ID = 0 + private val PRIMARY_USER = + UserInfo( + /* id= */ PRIMARY_USER_ID, + /* name= */ "primary user", + /* flags= */ UserInfo.FLAG_PRIMARY + ) + private const val TEST_PACKAGE_PANEL = "pkg.panel" + private val TEST_COMPONENT_PANEL = ComponentName(TEST_PACKAGE_PANEL, "service") + private val TEST_SELECTED_COMPONENT_PANEL = + SelectedComponentRepository.SelectedComponent( + TEST_PACKAGE_PANEL, + TEST_COMPONENT_PANEL, + true + ) + private val TEST_SELECTED_COMPONENT_NON_PANEL = + SelectedComponentRepository.SelectedComponent( + TEST_PACKAGE_PANEL, + TEST_COMPONENT_PANEL, + false + ) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/scrim/BouncerlessScrimControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/scrim/BouncerlessScrimControllerTest.java index 017fdbe8ac01..97052a84a60f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/scrim/BouncerlessScrimControllerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/scrim/BouncerlessScrimControllerTest.java @@ -39,6 +39,7 @@ import org.mockito.MockitoAnnotations; @SmallTest @RunWith(AndroidJUnit4.class) +@android.platform.test.annotations.EnabledOnRavenwood public class BouncerlessScrimControllerTest extends SysuiTestCase { @Mock BouncerlessScrimController.Callback mCallback; diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/scrim/ScrimManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/scrim/ScrimManagerTest.java index 4ee4a60fbeaf..ebbcf981b762 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/scrim/ScrimManagerTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/touch/scrim/ScrimManagerTest.java @@ -39,6 +39,7 @@ import org.mockito.MockitoAnnotations; @SmallTest @RunWith(AndroidJUnit4.class) +@android.platform.test.annotations.EnabledOnRavenwood public class ScrimManagerTest extends SysuiTestCase { @Mock ScrimController mBouncerlessScrimController; diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/fold/ui/helper/FoldPostureTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/fold/ui/helper/FoldPostureTest.kt index 61b205710873..db52a45b59da 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/fold/ui/helper/FoldPostureTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/fold/ui/helper/FoldPostureTest.kt @@ -28,6 +28,7 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) +@android.platform.test.annotations.EnabledOnRavenwood class FoldPostureTest : SysuiTestCase() { @Test diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/inputmethod/data/repository/InputMethodRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/inputmethod/data/repository/InputMethodRepositoryTest.kt new file mode 100644 index 000000000000..857cdce448ed --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/inputmethod/data/repository/InputMethodRepositoryTest.kt @@ -0,0 +1,124 @@ +/* + * 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.inputmethod.data.repository + +import android.os.UserHandle +import android.view.inputmethod.InputMethodInfo +import android.view.inputmethod.InputMethodManager +import android.view.inputmethod.InputMethodSubtype +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.kosmos.testScope +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.eq +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.flow.count +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidJUnit4::class) +class InputMethodRepositoryTest : SysuiTestCase() { + + @Mock private lateinit var inputMethodManager: InputMethodManager + + private val kosmos = Kosmos() + private val testScope = kosmos.testScope + + private lateinit var underTest: InputMethodRepository + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + whenever(inputMethodManager.getEnabledInputMethodSubtypeList(eq(null), anyBoolean())) + .thenReturn(listOf()) + + underTest = + InputMethodRepositoryImpl( + backgroundDispatcher = kosmos.testDispatcher, + inputMethodManager = inputMethodManager, + ) + } + + @Test + fun enabledInputMethods_noImes_emptyFlow() = + testScope.runTest { + whenever(inputMethodManager.getEnabledInputMethodListAsUser(eq(USER_HANDLE))) + .thenReturn(listOf()) + whenever(inputMethodManager.getEnabledInputMethodSubtypeList(any(), anyBoolean())) + .thenReturn(listOf()) + + assertThat(underTest.enabledInputMethods(USER_ID, fetchSubtypes = true).count()) + .isEqualTo(0) + } + + @Test + fun selectedInputMethodSubtypes_returnsSubtypeList() = + testScope.runTest { + val subtypeId = 123 + val isAuxiliary = true + whenever(inputMethodManager.getEnabledInputMethodListAsUser(eq(USER_HANDLE))) + .thenReturn(listOf(mock<InputMethodInfo>())) + whenever(inputMethodManager.getEnabledInputMethodSubtypeList(any(), anyBoolean())) + .thenReturn(listOf()) + whenever(inputMethodManager.getEnabledInputMethodSubtypeList(eq(null), anyBoolean())) + .thenReturn( + listOf( + InputMethodSubtype.InputMethodSubtypeBuilder() + .setSubtypeId(subtypeId) + .setIsAuxiliary(isAuxiliary) + .build() + ) + ) + + val result = underTest.selectedInputMethodSubtypes() + assertThat(result).hasSize(1) + assertThat(result.first().subtypeId).isEqualTo(subtypeId) + assertThat(result.first().isAuxiliary).isEqualTo(isAuxiliary) + } + + @Test + fun showImePicker_forwardsDisplayId() = + testScope.runTest { + val displayId = 7 + + underTest.showInputMethodPicker(displayId, /* showAuxiliarySubtypes = */ true) + + verify(inputMethodManager) + .showInputMethodPickerFromSystem( + /* showAuxiliarySubtypes = */ eq(true), + /* displayId = */ eq(displayId) + ) + } + + companion object { + private const val USER_ID = 100 + private val USER_HANDLE = UserHandle.of(USER_ID) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/inputmethod/domain/interactor/InputMethodInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/inputmethod/domain/interactor/InputMethodInteractorTest.kt new file mode 100644 index 000000000000..d23ff2a817e9 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/inputmethod/domain/interactor/InputMethodInteractorTest.kt @@ -0,0 +1,157 @@ +/* + * 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.inputmethod.domain.interactor + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.inputmethod.data.model.InputMethodModel +import com.android.systemui.inputmethod.data.repository.fakeInputMethodRepository +import com.android.systemui.inputmethod.data.repository.inputMethodRepository +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testScope +import com.google.common.truth.Truth.assertThat +import java.util.UUID +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class InputMethodInteractorTest : SysuiTestCase() { + + private val kosmos = Kosmos() + private val testScope = kosmos.testScope + private val fakeInputMethodRepository = kosmos.fakeInputMethodRepository + + private val underTest = InputMethodInteractor(repository = kosmos.inputMethodRepository) + + @Test + fun hasMultipleEnabledImesOrSubtypes_noImes_returnsFalse() = + testScope.runTest { + fakeInputMethodRepository.setEnabledInputMethods(USER_ID) + + assertThat(underTest.hasMultipleEnabledImesOrSubtypes(USER_ID)).isFalse() + } + + @Test + fun hasMultipleEnabledImesOrSubtypes_noMatches_returnsFalse() = + testScope.runTest { + fakeInputMethodRepository.setEnabledInputMethods( + USER_ID, + createInputMethodWithSubtypes(auxiliarySubtypes = 1, nonAuxiliarySubtypes = 0), + createInputMethodWithSubtypes(auxiliarySubtypes = 3, nonAuxiliarySubtypes = 0), + ) + + assertThat(underTest.hasMultipleEnabledImesOrSubtypes(USER_ID)).isFalse() + } + + @Test + fun hasMultipleEnabledImesOrSubtypes_oneMatch_returnsFalse() = + testScope.runTest { + fakeInputMethodRepository.setEnabledInputMethods( + USER_ID, + createInputMethodWithSubtypes(auxiliarySubtypes = 0, nonAuxiliarySubtypes = 0), + ) + + assertThat(underTest.hasMultipleEnabledImesOrSubtypes(USER_ID)).isFalse() + } + + @Test + fun hasMultipleEnabledImesOrSubtypes_twoMatches_returnsTrue() = + testScope.runTest { + fakeInputMethodRepository.setEnabledInputMethods( + USER_ID, + createInputMethodWithSubtypes(auxiliarySubtypes = 0, nonAuxiliarySubtypes = 1), + createInputMethodWithSubtypes(auxiliarySubtypes = 0, nonAuxiliarySubtypes = 0), + ) + + assertThat(underTest.hasMultipleEnabledImesOrSubtypes(USER_ID)).isTrue() + } + + @Test + fun hasMultipleEnabledImesOrSubtypes_oneWithNonAux_returnsFalse() = + testScope.runTest { + fakeInputMethodRepository.setEnabledInputMethods( + USER_ID, + createInputMethodWithSubtypes(auxiliarySubtypes = 0, nonAuxiliarySubtypes = 2), + ) + + assertThat(underTest.hasMultipleEnabledImesOrSubtypes(USER_ID)).isFalse() + } + + @Test + fun hasMultipleEnabledImesOrSubtypes_twoWithAux_returnsFalse() = + testScope.runTest { + fakeInputMethodRepository.setEnabledInputMethods( + USER_ID, + createInputMethodWithSubtypes(auxiliarySubtypes = 3, nonAuxiliarySubtypes = 0), + createInputMethodWithSubtypes(auxiliarySubtypes = 5, nonAuxiliarySubtypes = 0), + ) + + assertThat(underTest.hasMultipleEnabledImesOrSubtypes(USER_ID)).isFalse() + } + + @Test + fun hasMultipleEnabledImesOrSubtypes_selectedHasOneSubtype_returnsFalse() = + testScope.runTest { + fakeInputMethodRepository.selectedInputMethodSubtypes = + listOf(InputMethodModel.Subtype(1, isAuxiliary = false)) + + assertThat(underTest.hasMultipleEnabledImesOrSubtypes(USER_ID)).isFalse() + } + + @Test + fun hasMultipleEnabledImesOrSubtypes_selectedHasTwoSubtypes_returnsTrue() = + testScope.runTest { + fakeInputMethodRepository.selectedInputMethodSubtypes = + listOf( + InputMethodModel.Subtype(subtypeId = 1, isAuxiliary = false), + InputMethodModel.Subtype(subtypeId = 2, isAuxiliary = false), + ) + + assertThat(underTest.hasMultipleEnabledImesOrSubtypes(USER_ID)).isTrue() + } + + @Test + fun showImePicker_shownOnCorrectId() = + testScope.runTest { + val displayId = 7 + + underTest.showInputMethodPicker(displayId, showAuxiliarySubtypes = false) + + assertThat(fakeInputMethodRepository.inputMethodPickerShownDisplayId) + .isEqualTo(displayId) + } + + private fun createInputMethodWithSubtypes( + auxiliarySubtypes: Int, + nonAuxiliarySubtypes: Int, + ): InputMethodModel { + return InputMethodModel( + imeId = UUID.randomUUID().toString(), + subtypes = + List(auxiliarySubtypes + nonAuxiliarySubtypes) { + InputMethodModel.Subtype(subtypeId = it, isAuxiliary = it < auxiliarySubtypes) + } + ) + } + + companion object { + private const val USER_ID = 100 + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt index e20d3afaca53..ee3e241b5754 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/CameraQuickAffordanceConfigTest.kt @@ -43,6 +43,7 @@ import org.mockito.MockitoAnnotations @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) +@android.platform.test.annotations.EnabledOnRavenwood class CameraQuickAffordanceConfigTest : SysuiTestCase() { @Mock private lateinit var cameraGestureHelper: CameraGestureHelper diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfigTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfigTest.kt index 4ae144c03314..77e0f4ef2c5f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfigTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/FlashlightQuickAffordanceConfigTest.kt @@ -42,6 +42,7 @@ import org.mockito.MockitoAnnotations @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) +@android.platform.test.annotations.EnabledOnRavenwood class FlashlightQuickAffordanceConfigTest : LeakCheckedTest() { @Mock private lateinit var context: Context diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt index 521dea34513e..ca64cec98b2e 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/QrCodeScannerKeyguardQuickAffordanceConfigTest.kt @@ -40,6 +40,7 @@ import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidJUnit4::class) +@android.platform.test.annotations.EnabledOnRavenwood class QrCodeScannerKeyguardQuickAffordanceConfigTest : SysuiTestCase() { @Mock private lateinit var controller: QRCodeScannerController diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt index 6b7d2635ffa4..558e7e6564ae 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/DeviceEntryFingerprintAuthRepositoryTest.kt @@ -50,6 +50,7 @@ import org.mockito.MockitoAnnotations @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) +@android.platform.test.annotations.EnabledOnRavenwood class DeviceEntryFingerprintAuthRepositoryTest : SysuiTestCase() { @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor @Mock private lateinit var authController: AuthController diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/DevicePostureRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/DevicePostureRepositoryTest.kt index ae6c5b7b36b0..a0b85423b9f2 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/DevicePostureRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/DevicePostureRepositoryTest.kt @@ -40,6 +40,7 @@ import org.mockito.MockitoAnnotations @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) +@android.platform.test.annotations.EnabledOnRavenwood class DevicePostureRepositoryTest : SysuiTestCase() { private lateinit var underTest: DevicePostureRepository private lateinit var testScope: TestScope diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt index c01c79dd2601..128b46533a8b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/KeyguardRepositoryImplTest.kt @@ -63,6 +63,7 @@ import org.mockito.MockitoAnnotations @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) +@android.platform.test.annotations.EnabledOnRavenwood class KeyguardRepositoryImplTest : SysuiTestCase() { @Mock private lateinit var statusBarStateController: StatusBarStateController diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/TrustRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/TrustRepositoryTest.kt index ee47c58f4002..5f0f24dcca86 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/TrustRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/repository/TrustRepositoryTest.kt @@ -46,6 +46,7 @@ import org.mockito.MockitoAnnotations @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) +@android.platform.test.annotations.EnabledOnRavenwood class TrustRepositoryTest : SysuiTestCase() { @Mock private lateinit var trustManager: TrustManager @Captor private lateinit var listener: ArgumentCaptor<TrustManager.TrustListener> diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt index 34f703bc0ca7..db414b724b63 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractorTest.kt @@ -27,6 +27,7 @@ import com.android.systemui.animation.DialogLaunchAnimator import com.android.systemui.common.shared.model.ContentDescription import com.android.systemui.common.shared.model.Icon import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.coroutines.collectValues import com.android.systemui.dock.DockManager import com.android.systemui.dock.DockManagerFake import com.android.systemui.flags.FakeFeatureFlags @@ -49,16 +50,17 @@ import com.android.systemui.plugins.ActivityStarter import com.android.systemui.res.R import com.android.systemui.settings.UserFileManager import com.android.systemui.settings.UserTracker +import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots import com.android.systemui.statusbar.policy.KeyguardStateController -import com.android.systemui.testKosmos import com.android.systemui.util.FakeSharedPreferences import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.systemui.util.settings.FakeSettings import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent @@ -82,6 +84,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { @Mock private lateinit var activityStarter: ActivityStarter @Mock private lateinit var launchAnimator: DialogLaunchAnimator @Mock private lateinit var devicePolicyManager: DevicePolicyManager + @Mock private lateinit var shadeInteractor: ShadeInteractor @Mock private lateinit var logger: KeyguardQuickAffordancesMetricsLogger private lateinit var underTest: KeyguardQuickAffordanceInteractor @@ -95,8 +98,6 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { private lateinit var dockManager: DockManagerFake private lateinit var biometricSettingsRepository: FakeBiometricSettingsRepository - private val kosmos = testKosmos() - @Before fun setUp() { MockitoAnnotations.initMocks(this) @@ -183,7 +184,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { underTest = KeyguardQuickAffordanceInteractor( keyguardInteractor = withDeps.keyguardInteractor, - shadeInteractor = kosmos.shadeInteractor, + shadeInteractor = shadeInteractor, lockPatternUtils = lockPatternUtils, keyguardStateController = keyguardStateController, userTracker = userTracker, @@ -198,6 +199,8 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { backgroundDispatcher = testDispatcher, appContext = context, ) + + whenever(shadeInteractor.anyExpansion).thenReturn(MutableStateFlow(0f)) } @Test @@ -344,6 +347,25 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() { } @Test + fun quickAffordance_updateOncePerShadeExpansion() = + testScope.runTest { + val shadeExpansion = MutableStateFlow(0f) + whenever(shadeInteractor.anyExpansion).thenReturn(shadeExpansion) + + val collectedValue by + collectValues( + underTest.quickAffordance(KeyguardQuickAffordancePosition.BOTTOM_START) + ) + + val initialSize = collectedValue.size + for (i in 0..10) { + shadeExpansion.value = i / 10f + } + + assertThat(collectedValue.size).isEqualTo(initialSize + 1) + } + + @Test fun quickAffordanceAlwaysVisible_notVisible_restrictedByPolicyManager() = testScope.runTest { whenever(devicePolicyManager.getKeyguardDisabledFeatures(null, userTracker.userId)) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt index 6828041eff5a..9368097c0ac9 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorTest.kt @@ -45,6 +45,7 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) @kotlinx.coroutines.ExperimentalCoroutinesApi +@android.platform.test.annotations.EnabledOnRavenwood class KeyguardTransitionInteractorTest : SysuiTestCase() { val kosmos = testKosmos() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManagerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManagerTest.kt index e8504563b4ae..4695ea404425 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManagerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/preview/KeyguardRemotePreviewManagerTest.kt @@ -13,6 +13,7 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) +@android.platform.test.annotations.EnabledOnRavenwood class KeyguardRemotePreviewManagerTest : SysuiTestCase() { private val testDispatcher = StandardTestDispatcher() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt index d4dd2ac78e2a..0b80ff8d6ca4 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenContentViewModelTest.kt @@ -36,6 +36,7 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) +@android.platform.test.annotations.EnabledOnRavenwood class LockscreenContentViewModelTest : SysuiTestCase() { private val kosmos: Kosmos = testKosmos() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt index 30ac34402ffd..6fc5be1e376d 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelTest.kt @@ -19,7 +19,7 @@ package com.android.systemui.keyguard.ui.viewmodel import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor +import com.android.systemui.bouncer.domain.interactor.mockPrimaryBouncerInteractor import com.android.systemui.coroutines.collectValues import com.android.systemui.flags.Flags import com.android.systemui.flags.fakeFeatureFlagsClassic @@ -52,11 +52,9 @@ class PrimaryBouncerToGoneTransitionViewModelTest : SysuiTestCase() { val testScope = kosmos.testScope val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository - val primaryBouncerInteractor = kosmos.primaryBouncerInteractor + val primaryBouncerInteractor = kosmos.mockPrimaryBouncerInteractor val sysuiStatusBarStateController = kosmos.sysuiStatusBarStateController - val underTest by lazy { - kosmos.primaryBouncerToGoneTransitionViewModel - } + val underTest by lazy { kosmos.primaryBouncerToGoneTransitionViewModel } @Before fun setUp() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt index b267720971cd..2ad872ca6023 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/SceneContainerRepositoryTest.kt @@ -40,6 +40,7 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) +@android.platform.test.annotations.EnabledOnRavenwood class SceneContainerRepositoryTest : SysuiTestCase() { private val kosmos = testKosmos().apply { fakeSceneContainerFlags.enabled = true } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/WindowRootViewVisibilityRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/WindowRootViewVisibilityRepositoryTest.kt index 7ae501d05fcd..13b5b54510a5 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/WindowRootViewVisibilityRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/data/repository/WindowRootViewVisibilityRepositoryTest.kt @@ -30,6 +30,7 @@ import org.mockito.Mockito.verify @SmallTest @RunWith(AndroidJUnit4::class) +@android.platform.test.annotations.EnabledOnRavenwood class WindowRootViewVisibilityRepositoryTest : SysuiTestCase() { private val iStatusBarService = mock<IStatusBarService>() private val executor = FakeExecutor(FakeSystemClock()) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt index bf99a8687aa4..942fbc229e8a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/SceneInteractorTest.kt @@ -174,105 +174,6 @@ class SceneInteractorTest : SysuiTestCase() { } @Test - fun transitioning_idle_false() = - testScope.runTest { - val transitionState = - MutableStateFlow<ObservableTransitionState>( - ObservableTransitionState.Idle(SceneKey.Shade) - ) - val transitioning by - collectLastValue(underTest.transitioning(SceneKey.Shade, SceneKey.Lockscreen)) - underTest.setTransitionState(transitionState) - - assertThat(transitioning).isFalse() - } - - @Test - fun transitioning_wrongFromScene_false() = - testScope.runTest { - val transitionState = - MutableStateFlow<ObservableTransitionState>( - ObservableTransitionState.Transition( - fromScene = SceneKey.Gone, - toScene = SceneKey.Lockscreen, - progress = flowOf(0.5f), - isInitiatedByUserInput = false, - isUserInputOngoing = flowOf(false), - ) - ) - val transitioning by - collectLastValue(underTest.transitioning(SceneKey.Shade, SceneKey.Lockscreen)) - underTest.setTransitionState(transitionState) - - assertThat(transitioning).isFalse() - } - - @Test - fun transitioning_wrongToScene_false() = - testScope.runTest { - val transitionState = - MutableStateFlow<ObservableTransitionState>( - ObservableTransitionState.Transition( - fromScene = SceneKey.Shade, - toScene = SceneKey.QuickSettings, - progress = flowOf(0.5f), - isInitiatedByUserInput = false, - isUserInputOngoing = flowOf(false), - ) - ) - underTest.setTransitionState(transitionState) - - assertThat(underTest.transitioning(SceneKey.Shade, SceneKey.Lockscreen).value).isFalse() - } - - @Test - fun transitioning_correctFromAndToScenes_true() = - testScope.runTest { - val transitionState = - MutableStateFlow<ObservableTransitionState>( - ObservableTransitionState.Transition( - fromScene = SceneKey.Shade, - toScene = SceneKey.Lockscreen, - progress = flowOf(0.5f), - isInitiatedByUserInput = false, - isUserInputOngoing = flowOf(false), - ) - ) - val transitioning by - collectLastValue(underTest.transitioning(SceneKey.Shade, SceneKey.Lockscreen)) - underTest.setTransitionState(transitionState) - - assertThat(transitioning).isTrue() - } - - @Test - fun transitioning_updates() = - testScope.runTest { - val transitionState = - MutableStateFlow<ObservableTransitionState>( - ObservableTransitionState.Idle(SceneKey.Shade) - ) - val transitioning by - collectLastValue(underTest.transitioning(SceneKey.Shade, SceneKey.Lockscreen)) - underTest.setTransitionState(transitionState) - - assertThat(transitioning).isFalse() - - transitionState.value = - ObservableTransitionState.Transition( - fromScene = SceneKey.Shade, - toScene = SceneKey.Lockscreen, - progress = flowOf(0.5f), - isInitiatedByUserInput = false, - isUserInputOngoing = flowOf(false), - ) - assertThat(transitioning).isTrue() - - transitionState.value = ObservableTransitionState.Idle(SceneKey.Lockscreen) - assertThat(transitioning).isFalse() - } - - @Test fun isTransitionUserInputOngoing_idle_false() = testScope.runTest { val transitionState = diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt index e9a2a3befb03..c0aaab3ad6e1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt @@ -9,8 +9,6 @@ import com.android.systemui.flags.FakeFeatureFlagsClassic import com.android.systemui.flags.Flags import com.android.systemui.kosmos.testScope import com.android.systemui.scene.domain.interactor.sceneInteractor -import com.android.systemui.scene.shared.model.ObservableTransitionState -import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel @@ -22,8 +20,6 @@ import com.android.systemui.statusbar.pipeline.shared.data.repository.FakeConnec import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -74,74 +70,6 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { } @Test - fun isTransitioning_idle_false() = - testScope.runTest { - val isTransitioning by collectLastValue(underTest.isTransitioning) - sceneInteractor.setTransitionState( - MutableStateFlow(ObservableTransitionState.Idle(SceneKey.Shade)) - ) - - assertThat(isTransitioning).isFalse() - } - - @Test - fun isTransitioning_shadeToQs_true() = - testScope.runTest { - val isTransitioning by collectLastValue(underTest.isTransitioning) - sceneInteractor.setTransitionState( - MutableStateFlow( - ObservableTransitionState.Transition( - fromScene = SceneKey.Shade, - toScene = SceneKey.QuickSettings, - progress = MutableStateFlow(0.5f), - isInitiatedByUserInput = false, - isUserInputOngoing = flowOf(false), - ) - ) - ) - - assertThat(isTransitioning).isTrue() - } - - @Test - fun isTransitioning_qsToShade_true() = - testScope.runTest { - val isTransitioning by collectLastValue(underTest.isTransitioning) - sceneInteractor.setTransitionState( - MutableStateFlow( - ObservableTransitionState.Transition( - fromScene = SceneKey.QuickSettings, - toScene = SceneKey.Shade, - progress = MutableStateFlow(0.5f), - isInitiatedByUserInput = false, - isUserInputOngoing = flowOf(false), - ) - ) - ) - - assertThat(isTransitioning).isTrue() - } - - @Test - fun isTransitioning_otherTransition_false() = - testScope.runTest { - val isTransitioning by collectLastValue(underTest.isTransitioning) - sceneInteractor.setTransitionState( - MutableStateFlow( - ObservableTransitionState.Transition( - fromScene = SceneKey.Gone, - toScene = SceneKey.Shade, - progress = MutableStateFlow(0.5f), - isInitiatedByUserInput = false, - isUserInputOngoing = flowOf(false), - ) - ) - ) - - assertThat(isTransitioning).isFalse() - } - - @Test fun mobileSubIds_update() = testScope.runTest { val mobileSubIds by collectLastValue(underTest.mobileSubIds) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/BcSmartspaceConfigProviderTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/BcSmartspaceConfigProviderTest.kt index cb83e7c7adbc..bbbee90f2706 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/BcSmartspaceConfigProviderTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/BcSmartspaceConfigProviderTest.kt @@ -30,6 +30,7 @@ import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidJUnit4::class) +@android.platform.test.annotations.EnabledOnRavenwood class BcSmartspaceConfigProviderTest : SysuiTestCase() { @Mock private lateinit var featureFlags: FeatureFlags diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/LockscreenPreconditionTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/LockscreenPreconditionTest.kt index 0b5aea7d8683..089eb43e70a1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/LockscreenPreconditionTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/LockscreenPreconditionTest.kt @@ -37,6 +37,7 @@ import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidJUnit4::class) @TestableLooper.RunWithLooper +@android.platform.test.annotations.EnabledOnRavenwood class LockscreenPreconditionTest : SysuiTestCase() { @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/LockscreenTargetFilterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/LockscreenTargetFilterTest.kt index bf33010a055b..6616786a3b6c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/LockscreenTargetFilterTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/smartspace/LockscreenTargetFilterTest.kt @@ -52,6 +52,7 @@ import org.mockito.MockitoAnnotations @SmallTest @TestableLooper.RunWithLooper @RunWith(AndroidJUnit4::class) +@android.platform.test.annotations.EnabledOnRavenwood class LockscreenTargetFilterTest : SysuiTestCase() { @Mock private lateinit var secureSettings: SecureSettings diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/RemoteInputRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/RemoteInputRepositoryImplTest.kt index 8a0400d092c3..f7a8858a2741 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/RemoteInputRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/data/repository/RemoteInputRepositoryImplTest.kt @@ -38,6 +38,7 @@ import org.mockito.MockitoAnnotations @SmallTest @RunWith(AndroidJUnit4::class) +@android.platform.test.annotations.EnabledOnRavenwood class RemoteInputRepositoryImplTest : SysuiTestCase() { @Mock private lateinit var remoteInputManager: NotificationRemoteInputManager diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/domain/interactor/RemoteInputInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/domain/interactor/RemoteInputInteractorTest.kt index 12469ddcafc2..60da53cebc80 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/domain/interactor/RemoteInputInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/domain/interactor/RemoteInputInteractorTest.kt @@ -34,6 +34,7 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) +@android.platform.test.annotations.EnabledOnRavenwood class RemoteInputInteractorTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt index 6a2e31739c77..4d7d5d3fa664 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt @@ -87,21 +87,21 @@ class NotificationStackAppearanceIntegrationTest : SysuiTestCase() { } @Test - fun updateShadeExpansion() = + fun shadeExpansion_goneToShade() = testScope.runTest { - val expandFraction by collectLastValue(appearanceViewModel.expandFraction) - assertThat(expandFraction).isEqualTo(0f) - val transitionState = MutableStateFlow<ObservableTransitionState>( - ObservableTransitionState.Idle(scene = SceneKey.Lockscreen) + ObservableTransitionState.Idle(scene = SceneKey.Gone) ) sceneInteractor.setTransitionState(transitionState) + val expandFraction by collectLastValue(appearanceViewModel.expandFraction) + assertThat(expandFraction).isEqualTo(0f) + sceneInteractor.changeScene(SceneModel(SceneKey.Shade), "reason") val transitionProgress = MutableStateFlow(0f) transitionState.value = ObservableTransitionState.Transition( - fromScene = SceneKey.Lockscreen, + fromScene = SceneKey.Gone, toScene = SceneKey.Shade, progress = transitionProgress, isInitiatedByUserInput = false, @@ -118,4 +118,49 @@ class NotificationStackAppearanceIntegrationTest : SysuiTestCase() { sceneInteractor.onSceneChanged(SceneModel(SceneKey.Shade), "reason") assertThat(expandFraction).isWithin(0.01f).of(1f) } + + @Test + fun shadeExpansion_idleOnLockscreen() = + testScope.runTest { + val transitionState = + MutableStateFlow<ObservableTransitionState>( + ObservableTransitionState.Idle(scene = SceneKey.Lockscreen) + ) + sceneInteractor.setTransitionState(transitionState) + val expandFraction by collectLastValue(appearanceViewModel.expandFraction) + assertThat(expandFraction).isEqualTo(1f) + } + + @Test + fun shadeExpansion_shadeToQs() = + testScope.runTest { + val transitionState = + MutableStateFlow<ObservableTransitionState>( + ObservableTransitionState.Idle(scene = SceneKey.Shade) + ) + sceneInteractor.setTransitionState(transitionState) + val expandFraction by collectLastValue(appearanceViewModel.expandFraction) + assertThat(expandFraction).isEqualTo(1f) + + sceneInteractor.changeScene(SceneModel(SceneKey.QuickSettings), "reason") + val transitionProgress = MutableStateFlow(0f) + transitionState.value = + ObservableTransitionState.Transition( + fromScene = SceneKey.Shade, + toScene = SceneKey.QuickSettings, + progress = transitionProgress, + isInitiatedByUserInput = false, + isUserInputOngoing = flowOf(false), + ) + val steps = 10 + repeat(steps) { repetition -> + val progress = (1f / steps) * (repetition + 1) + transitionProgress.value = progress + runCurrent() + assertThat(expandFraction).isEqualTo(1f) + } + + sceneInteractor.onSceneChanged(SceneModel(SceneKey.QuickSettings), "reason") + assertThat(expandFraction).isEqualTo(1f) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt index c7411cd78b78..ffe6e6df6b48 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt @@ -30,6 +30,7 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) +@android.platform.test.annotations.EnabledOnRavenwood class NotificationStackAppearanceInteractorTest : SysuiTestCase() { private val kosmos = Kosmos() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt index ce00250467f6..18825a29c729 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/data/repository/prod/DisabledWifiRepositoryTest.kt @@ -28,6 +28,7 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) +@android.platform.test.annotations.EnabledOnRavenwood class DisabledWifiRepositoryTest : SysuiTestCase() { private lateinit var underTest: DisabledWifiRepository diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt index 7fbbfc77300e..84c728cd9412 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/wifi/domain/interactor/WifiInteractorImplTest.kt @@ -43,6 +43,7 @@ import org.junit.runner.RunWith @Suppress("EXPERIMENTAL_IS_NOT_ENABLED") @SmallTest @RunWith(AndroidJUnit4::class) +@android.platform.test.annotations.EnabledOnRavenwood class WifiInteractorImplTest : SysuiTestCase() { private lateinit var underTest: WifiInteractor diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/data/repository/UserSetupRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/data/repository/UserSetupRepositoryTest.kt index ebc81be6d4b6..4c8bbe09a688 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/data/repository/UserSetupRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/data/repository/UserSetupRepositoryTest.kt @@ -45,6 +45,7 @@ import org.mockito.Mockito.verify @SmallTest @RunWith(AndroidJUnit4::class) +@android.platform.test.annotations.EnabledOnRavenwood class UserSetupRepositoryTest : SysuiTestCase() { private val kosmos = testKosmos() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/UserSetupInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/UserSetupInteractorTest.kt index 26c0f80c53de..7ec0a610b103 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/UserSetupInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/domain/interactor/UserSetupInteractorTest.kt @@ -30,6 +30,7 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) +@android.platform.test.annotations.EnabledOnRavenwood class UserSetupInteractorTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/BooleanFlowOperatorsTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/BooleanFlowOperatorsTest.kt index b4a0a37ec9ef..96d1c0de9b66 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/BooleanFlowOperatorsTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/BooleanFlowOperatorsTest.kt @@ -34,6 +34,7 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) +@android.platform.test.annotations.EnabledOnRavenwood class BooleanFlowOperatorsTest : SysuiTestCase() { val kosmos = testKosmos() diff --git a/packages/SystemUI/res/drawable-nodpi/homecontrols_sq.png b/packages/SystemUI/res/drawable-nodpi/homecontrols_sq.png Binary files differnew file mode 100644 index 000000000000..00b461b1ef8f --- /dev/null +++ b/packages/SystemUI/res/drawable-nodpi/homecontrols_sq.png diff --git a/packages/SystemUI/res/drawable/accessibility_window_magnification_drag_handle_background_change_inset.xml b/packages/SystemUI/res/drawable/accessibility_window_magnification_drag_handle_background_change_inset.xml new file mode 100644 index 000000000000..02486bfae9b5 --- /dev/null +++ b/packages/SystemUI/res/drawable/accessibility_window_magnification_drag_handle_background_change_inset.xml @@ -0,0 +1,22 @@ +<?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. +--> +<inset xmlns:android="http://schemas.android.com/apk/res/android" + android:drawable="@drawable/accessibility_window_magnification_drag_handle_background_change" + android:insetBottom="@dimen/magnification_inner_border_margin" + android:insetLeft="@dimen/magnification_inner_border_margin" + android:insetRight="@dimen/magnification_inner_border_margin" + android:insetTop="@dimen/magnification_inner_border_margin" /> diff --git a/packages/SystemUI/res/drawable/accessibility_window_magnification_drag_handle_background_inset.xml b/packages/SystemUI/res/drawable/accessibility_window_magnification_drag_handle_background_inset.xml new file mode 100644 index 000000000000..bfb7c47cb130 --- /dev/null +++ b/packages/SystemUI/res/drawable/accessibility_window_magnification_drag_handle_background_inset.xml @@ -0,0 +1,22 @@ +<?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. +--> +<inset xmlns:android="http://schemas.android.com/apk/res/android" + android:drawable="@drawable/accessibility_window_magnification_drag_handle_background" + android:insetBottom="@dimen/magnification_inner_border_margin" + android:insetLeft="@dimen/magnification_inner_border_margin" + android:insetRight="@dimen/magnification_inner_border_margin" + android:insetTop="@dimen/magnification_inner_border_margin" /> diff --git a/packages/SystemUI/res/layout/window_magnifier_view.xml b/packages/SystemUI/res/layout/window_magnifier_view.xml index a8a048d8c2f6..6286f343aeff 100644 --- a/packages/SystemUI/res/layout/window_magnifier_view.xml +++ b/packages/SystemUI/res/layout/window_magnifier_view.xml @@ -117,12 +117,11 @@ android:id="@+id/drag_handle" android:layout_width="@dimen/magnification_drag_view_size" android:layout_height="@dimen/magnification_drag_view_size" - android:layout_margin="@dimen/magnification_inner_border_margin" android:layout_gravity="right|bottom" android:padding="@dimen/magnifier_drag_handle_padding" android:scaleType="center" android:src="@drawable/ic_move_magnification" - android:background="@drawable/accessibility_window_magnification_drag_handle_background"/> + android:background="@drawable/accessibility_window_magnification_drag_handle_background_inset"/> <ImageView android:id="@+id/close_button" diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 17719d11345b..7a83070d1806 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -991,7 +991,7 @@ <!-- Component name for Home Panel Dream --> <string name="config_homePanelDreamComponent" translatable="false"> - @null + com.android.systemui/com.android.systemui.dreams.homecontrols.HomeControlsDreamService </string> <!-- diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 4209c1f6a732..fa89fcd17797 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1249,7 +1249,7 @@ <dimen name="magnification_drag_corner_margin">8dp</dimen> <dimen name="magnification_frame_move_short">5dp</dimen> <dimen name="magnification_frame_move_long">25dp</dimen> - <dimen name="magnification_drag_view_size">36dp</dimen> + <dimen name="magnification_drag_view_size">70dp</dimen> <dimen name="magnification_controls_size">90dp</dimen> <dimen name="magnification_switch_button_size">56dp</dimen> <dimen name="magnification_switch_button_padding">6dp</dimen> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 79435885f410..8971859256e5 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -3314,4 +3314,8 @@ <string name="keyboard_backlight_dialog_title">Keyboard backlight</string> <!-- Content description for keyboard backlight brightness value [CHAR LIMIT=NONE] --> <string name="keyboard_backlight_value">Level %1$d of %2$d</string> + <!-- Label for home control panel [CHAR LIMIT=30] --> + <string name="home_controls_dream_label">Home Controls</string> + <!-- Description for home control panel [CHAR LIMIT=50] --> + <string name="home_controls_dream_description">Quickly access your home controls as a screensaver</string> </resources> diff --git a/packages/SystemUI/res/xml/home_controls_dream_metadata.xml b/packages/SystemUI/res/xml/home_controls_dream_metadata.xml new file mode 100644 index 000000000000..eb7c79e24b04 --- /dev/null +++ b/packages/SystemUI/res/xml/home_controls_dream_metadata.xml @@ -0,0 +1,19 @@ +<!-- + ~ 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. + --> +<dream xmlns:android="http://schemas.android.com/apk/res/android" + android:showClockAndComplications="false" + android:previewImage="@drawable/homecontrols_sq" + />
\ No newline at end of file diff --git a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt index bddf3b07dbb5..d2ad096c1207 100644 --- a/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt +++ b/packages/SystemUI/src/com/android/keyguard/logging/KeyguardLogger.kt @@ -97,6 +97,26 @@ constructor( ) } + fun logUpdateLockScreenUserLockedMsg( + userId: Int, + userUnlocked: Boolean, + encryptedOrLockdown: Boolean, + ) { + buffer.log( + KeyguardIndicationController.TAG, + LogLevel.DEBUG, + { + int1 = userId + bool1 = userUnlocked + bool2 = encryptedOrLockdown + }, + { + "updateLockScreenUserLockedMsg userId=$int1 " + + "userUnlocked:$bool1 encryptedOrLockdown:$bool2" + } + ) + } + fun logUpdateBatteryIndication( powerIndication: String, pluggedIn: Boolean, diff --git a/packages/SystemUI/src/com/android/systemui/GuestResumeSessionReceiver.java b/packages/SystemUI/src/com/android/systemui/GuestResumeSessionReceiver.java index 43728260248a..0f5f869cba5d 100644 --- a/packages/SystemUI/src/com/android/systemui/GuestResumeSessionReceiver.java +++ b/packages/SystemUI/src/com/android/systemui/GuestResumeSessionReceiver.java @@ -20,12 +20,11 @@ import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.pm.UserInfo; -import android.os.HandlerExecutor; -import android.os.HandlerThread; import android.os.UserHandle; import androidx.annotation.NonNull; +import com.android.systemui.res.R; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.logging.UiEventLogger; import com.android.systemui.GuestResetOrExitSessionReceiver.ResetSessionDialogFactory; @@ -33,7 +32,6 @@ import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.qs.QSUserSwitcherEvent; -import com.android.systemui.res.R; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.phone.SystemUIDialog; import com.android.systemui.statusbar.policy.UserSwitcherController; @@ -63,7 +61,6 @@ public class GuestResumeSessionReceiver { private final SecureSettings mSecureSettings; private final ResetSessionDialogFactory mResetSessionDialogFactory; private final GuestSessionNotification mGuestSessionNotification; - private final HandlerThread mHandlerThread; @VisibleForTesting public final UserTracker.Callback mUserChangedCallback = @@ -114,16 +111,13 @@ public class GuestResumeSessionReceiver { mSecureSettings = secureSettings; mGuestSessionNotification = guestSessionNotification; mResetSessionDialogFactory = resetSessionDialogFactory; - mHandlerThread = new HandlerThread("GuestResumeSessionReceiver"); - mHandlerThread.start(); } /** * Register this receiver with the {@link BroadcastDispatcher} */ public void register() { - mUserTracker.addCallback(mUserChangedCallback, - new HandlerExecutor(mHandlerThread.getThreadHandler())); + mUserTracker.addCallback(mUserChangedCallback, mMainExecutor); } private void cancelDialog() { diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/Magnification.java b/packages/SystemUI/src/com/android/systemui/accessibility/Magnification.java index 3ca95e11d789..5171a1f22791 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/Magnification.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/Magnification.java @@ -19,6 +19,7 @@ package com.android.systemui.accessibility; import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_FULLSCREEN; import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_MODE_WINDOW; import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY; +import static android.view.WindowManager.LayoutParams.TYPE_ACCESSIBILITY_OVERLAY; import static com.android.systemui.accessibility.AccessibilityLogger.MagnificationSettingsEvent; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_MAGNIFICATION_OVERLAP; @@ -28,10 +29,12 @@ import android.annotation.Nullable; import android.content.Context; import android.graphics.Rect; import android.hardware.display.DisplayManager; +import android.os.Binder; import android.os.Handler; import android.util.SparseArray; import android.view.Display; import android.view.SurfaceControl; +import android.view.SurfaceControlViewHost; import android.view.WindowManagerGlobal; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.IMagnificationConnection; @@ -40,6 +43,7 @@ import android.view.accessibility.IRemoteMagnificationAnimationCallback; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; import com.android.systemui.CoreStartable; +import com.android.systemui.Flags; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.model.SysUiState; @@ -49,6 +53,7 @@ import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.util.settings.SecureSettings; import java.io.PrintWriter; +import java.util.function.Supplier; import javax.inject.Inject; @@ -101,19 +106,28 @@ public class Magnification implements CoreStartable, CommandQueue.Callbacks { @Override protected WindowMagnificationController createInstance(Display display) { final Context windowContext = mContext.createWindowContext(display, - TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY, /* options */ null); + Flags.createWindowlessWindowMagnifier() + ? TYPE_ACCESSIBILITY_OVERLAY + : TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY, + /* options */ null); windowContext.setTheme(com.android.systemui.res.R.style.Theme_SystemUI); + + Supplier<SurfaceControlViewHost> scvhSupplier = () -> + Flags.createWindowlessWindowMagnifier() ? new SurfaceControlViewHost(mContext, + mContext.getDisplay(), new Binder(), TAG) : null; + return new WindowMagnificationController( windowContext, mHandler, new WindowMagnificationAnimationController(windowContext), - new SfVsyncFrameCallbackProvider(), - null, + /* mirrorWindowControl= */ null, new SurfaceControl.Transaction(), mWindowMagnifierCallback, mSysUiState, - WindowManagerGlobal::getWindowSession, - mSecureSettings); + mSecureSettings, + scvhSupplier, + new SfVsyncFrameCallbackProvider(), + WindowManagerGlobal::getWindowSession); } } @@ -140,7 +154,7 @@ public class Magnification implements CoreStartable, CommandQueue.Callbacks { @Override protected MagnificationSettingsController createInstance(Display display) { final Context windowContext = mContext.createWindowContext(display, - TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY, /* options */ null); + TYPE_ACCESSIBILITY_OVERLAY, /* options */ null); windowContext.setTheme(com.android.systemui.res.R.style.Theme_SystemUI); return new MagnificationSettingsController( windowContext, diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java index dde9f48424ea..d65cd5c09dcf 100644 --- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java +++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationController.java @@ -63,23 +63,27 @@ import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.Surface; import android.view.SurfaceControl; +import android.view.SurfaceControlViewHost; import android.view.SurfaceHolder; import android.view.SurfaceView; import android.view.View; import android.view.WindowManager; import android.view.WindowManagerGlobal; import android.view.WindowMetrics; +import android.view.accessibility.AccessibilityManager; import android.view.accessibility.AccessibilityNodeInfo; import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; import android.view.accessibility.IRemoteMagnificationAnimationCallback; import android.widget.FrameLayout; import android.widget.ImageView; +import androidx.annotation.UiThread; import androidx.core.math.MathUtils; import com.android.internal.accessibility.common.MagnificationConstants; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; +import com.android.systemui.Flags; import com.android.systemui.model.SysUiState; import com.android.systemui.res.R; import com.android.systemui.util.settings.SecureSettings; @@ -158,6 +162,15 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold private int mMagnificationFrameOffsetX = 0; private int mMagnificationFrameOffsetY = 0; + @Nullable private Supplier<SurfaceControlViewHost> mScvhSupplier; + + /** + * SurfaceControlViewHost is used to control the position of the window containing + * {@link #mMirrorView}. Using SurfaceControlViewHost instead of a regular window enables + * changing the window's position and setting {@link #mMirrorSurface}'s geometry atomically. + */ + private SurfaceControlViewHost mSurfaceControlViewHost; + // The root of the mirrored content private SurfaceControl mMirrorSurface; @@ -236,21 +249,21 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold @UiContext Context context, @NonNull Handler handler, @NonNull WindowMagnificationAnimationController animationController, - SfVsyncFrameCallbackProvider sfVsyncFrameProvider, MirrorWindowControl mirrorWindowControl, SurfaceControl.Transaction transaction, @NonNull WindowMagnifierCallback callback, SysUiState sysUiState, - @NonNull Supplier<IWindowSession> globalWindowSessionSupplier, - SecureSettings secureSettings) { + SecureSettings secureSettings, + Supplier<SurfaceControlViewHost> scvhSupplier, + SfVsyncFrameCallbackProvider sfVsyncFrameProvider, + Supplier<IWindowSession> globalWindowSessionSupplier) { mContext = context; mHandler = handler; mAnimationController = animationController; - mGlobalWindowSessionSupplier = globalWindowSessionSupplier; mAnimationController.setWindowMagnificationController(this); - mSfVsyncFrameProvider = sfVsyncFrameProvider; mWindowMagnifierCallback = callback; mSysUiState = sysUiState; + mScvhSupplier = scvhSupplier; mConfiguration = new Configuration(context.getResources().getConfiguration()); mWindowMagnificationSizePrefs = new WindowMagnificationSizePrefs(mContext); @@ -288,22 +301,78 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold mTransaction = transaction; mGestureDetector = new MagnificationGestureDetector(mContext, handler, this); + mWindowInsetChangeRunnable = this::onWindowInsetChanged; + mGlobalWindowSessionSupplier = globalWindowSessionSupplier; + mSfVsyncFrameProvider = sfVsyncFrameProvider; // Initialize listeners. - mMirrorViewRunnable = () -> { - if (mMirrorView != null) { - final Rect oldViewBounds = new Rect(mMirrorViewBounds); - mMirrorView.getBoundsOnScreen(mMirrorViewBounds); - if (oldViewBounds.width() != mMirrorViewBounds.width() - || oldViewBounds.height() != mMirrorViewBounds.height()) { - mMirrorView.setSystemGestureExclusionRects(Collections.singletonList( - new Rect(0, 0, mMirrorViewBounds.width(), mMirrorViewBounds.height()))); + if (Flags.createWindowlessWindowMagnifier()) { + mMirrorViewRunnable = new Runnable() { + final Rect mPreviousBounds = new Rect(); + + @Override + public void run() { + if (mMirrorView != null) { + if (mPreviousBounds.width() != mMirrorViewBounds.width() + || mPreviousBounds.height() != mMirrorViewBounds.height()) { + mMirrorView.setSystemGestureExclusionRects(Collections.singletonList( + new Rect(0, 0, mMirrorViewBounds.width(), + mMirrorViewBounds.height()))); + mPreviousBounds.set(mMirrorViewBounds); + } + updateSystemUIStateIfNeeded(); + mWindowMagnifierCallback.onWindowMagnifierBoundsChanged( + mDisplayId, mMirrorViewBounds); + } } - updateSystemUIStateIfNeeded(); - mWindowMagnifierCallback.onWindowMagnifierBoundsChanged( - mDisplayId, mMirrorViewBounds); - } - }; + }; + + mMirrorSurfaceViewLayoutChangeListener = + (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> + mMirrorView.post(this::applyTapExcludeRegion); + + mMirrorViewGeometryVsyncCallback = null; + } else { + mMirrorViewRunnable = () -> { + if (mMirrorView != null) { + final Rect oldViewBounds = new Rect(mMirrorViewBounds); + mMirrorView.getBoundsOnScreen(mMirrorViewBounds); + if (oldViewBounds.width() != mMirrorViewBounds.width() + || oldViewBounds.height() != mMirrorViewBounds.height()) { + mMirrorView.setSystemGestureExclusionRects(Collections.singletonList( + new Rect(0, 0, + mMirrorViewBounds.width(), mMirrorViewBounds.height()))); + } + updateSystemUIStateIfNeeded(); + mWindowMagnifierCallback.onWindowMagnifierBoundsChanged( + mDisplayId, mMirrorViewBounds); + } + }; + + mMirrorSurfaceViewLayoutChangeListener = + (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> + mMirrorView.post(this::applyTapExcludeRegion); + + mMirrorViewGeometryVsyncCallback = + l -> { + if (isActivated() && mMirrorSurface != null && calculateSourceBounds( + mMagnificationFrame, mScale)) { + // The final destination for the magnification surface should be at 0,0 + // since the ViewRootImpl's position will change + mTmpRect.set(0, 0, mMagnificationFrame.width(), + mMagnificationFrame.height()); + mTransaction.setGeometry(mMirrorSurface, mSourceBounds, mTmpRect, + Surface.ROTATION_0).apply(); + + // Notify source bounds change when the magnifier is not animating. + if (!mAnimationController.isAnimating()) { + mWindowMagnifierCallback.onSourceBoundsChanged(mDisplayId, + mSourceBounds); + } + } + }; + } + mMirrorViewLayoutChangeListener = (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> { if (!mHandler.hasCallbacks(mMirrorViewRunnable)) { @@ -311,34 +380,11 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold } }; - mMirrorSurfaceViewLayoutChangeListener = - (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> - mMirrorView.post(this::applyTapExcludeRegion); - - mMirrorViewGeometryVsyncCallback = - l -> { - if (isActivated() && mMirrorSurface != null && calculateSourceBounds( - mMagnificationFrame, mScale)) { - // The final destination for the magnification surface should be at 0,0 - // since the ViewRootImpl's position will change - mTmpRect.set(0, 0, mMagnificationFrame.width(), - mMagnificationFrame.height()); - mTransaction.setGeometry(mMirrorSurface, mSourceBounds, mTmpRect, - Surface.ROTATION_0).apply(); - - // Notify source bounds change when the magnifier is not animating. - if (!mAnimationController.isAnimating()) { - mWindowMagnifierCallback.onSourceBoundsChanged(mDisplayId, - mSourceBounds); - } - } - }; mUpdateStateDescriptionRunnable = () -> { if (isActivated()) { mMirrorView.setStateDescription(formatStateDescription(mScale)); } }; - mWindowInsetChangeRunnable = this::onWindowInsetChanged; } private void setupMagnificationSizeScaleOptions() { @@ -448,13 +494,21 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold if (mMirrorView != null) { mHandler.removeCallbacks(mMirrorViewRunnable); mMirrorView.removeOnLayoutChangeListener(mMirrorViewLayoutChangeListener); - mWm.removeView(mMirrorView); + if (!Flags.createWindowlessWindowMagnifier()) { + mWm.removeView(mMirrorView); + } mMirrorView = null; } if (mMirrorWindowControl != null) { mMirrorWindowControl.destroyControl(); } + + if (mSurfaceControlViewHost != null) { + mSurfaceControlViewHost.release(); + mSurfaceControlViewHost = null; + } + mMirrorViewBounds.setEmpty(); mSourceBounds.setEmpty(); updateSystemUIStateIfNeeded(); @@ -551,7 +605,11 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold if (!isActivated()) return; LayoutParams params = (LayoutParams) mMirrorView.getLayoutParams(); params.accessibilityTitle = getAccessibilityWindowTitle(); - mWm.updateViewLayout(mMirrorView, params); + if (Flags.createWindowlessWindowMagnifier()) { + mSurfaceControlViewHost.relayout(params); + } else { + mWm.updateViewLayout(mMirrorView, params); + } } /** @@ -602,6 +660,11 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold } private void createMirrorWindow() { + if (Flags.createWindowlessWindowMagnifier()) { + createWindowlessMirrorWindow(); + return; + } + // The window should be the size the mirrored surface will be but also add room for the // border and the drag handle. int windowWidth = mMagnificationFrame.width() + 2 * mMirrorSurfaceMargin; @@ -652,6 +715,68 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold addDragTouchListeners(); } + private void createWindowlessMirrorWindow() { + // The window should be the size the mirrored surface will be but also add room for the + // border and the drag handle. + int windowWidth = mMagnificationFrame.width() + 2 * mMirrorSurfaceMargin; + int windowHeight = mMagnificationFrame.height() + 2 * mMirrorSurfaceMargin; + + // TODO delete TYPE_ACCESSIBILITY_MAGNIFICATION_OVERLAY, it shouldn't be needed anymore + + LayoutParams params = new LayoutParams( + windowWidth, windowHeight, + LayoutParams.TYPE_ACCESSIBILITY_OVERLAY, + LayoutParams.FLAG_NOT_TOUCH_MODAL + | LayoutParams.FLAG_NOT_FOCUSABLE, + PixelFormat.TRANSPARENT); + params.receiveInsetsIgnoringZOrder = true; + params.setTitle(mContext.getString(R.string.magnification_window_title)); + params.accessibilityTitle = getAccessibilityWindowTitle(); + params.setTrustedOverlay(); + + mMirrorView = LayoutInflater.from(mContext).inflate(R.layout.window_magnifier_view, null); + mMirrorSurfaceView = mMirrorView.findViewById(R.id.surface_view); + + mMirrorBorderView = mMirrorView.findViewById(R.id.magnification_inner_border); + + // Allow taps to go through to the mirror SurfaceView below. + mMirrorSurfaceView.addOnLayoutChangeListener(mMirrorSurfaceViewLayoutChangeListener); + + mMirrorView.addOnLayoutChangeListener(mMirrorViewLayoutChangeListener); + mMirrorView.setAccessibilityDelegate(new MirrorWindowA11yDelegate()); + mMirrorView.setOnApplyWindowInsetsListener((v, insets) -> { + if (!mHandler.hasCallbacks(mWindowInsetChangeRunnable)) { + mHandler.post(mWindowInsetChangeRunnable); + } + return v.onApplyWindowInsets(insets); + }); + + mSurfaceControlViewHost = mScvhSupplier.get(); + mSurfaceControlViewHost.setView(mMirrorView, params); + SurfaceControl surfaceControl = mSurfaceControlViewHost + .getSurfacePackage().getSurfaceControl(); + + int x = mMagnificationFrame.left - mMirrorSurfaceMargin; + int y = mMagnificationFrame.top - mMirrorSurfaceMargin; + mTransaction + .setCrop(surfaceControl, new Rect(0, 0, windowWidth, windowHeight)) + .setPosition(surfaceControl, x, y) + .setLayer(surfaceControl, Integer.MAX_VALUE) + .show(surfaceControl) + .apply(); + + mMirrorViewBounds.set(x, y, x + windowWidth, y + windowHeight); + + AccessibilityManager accessibilityManager = mContext + .getSystemService(AccessibilityManager.class); + accessibilityManager.attachAccessibilityOverlayToDisplay(mDisplayId, surfaceControl); + + SurfaceHolder holder = mMirrorSurfaceView.getHolder(); + holder.addCallback(this); + holder.setFormat(PixelFormat.RGBA_8888); + addDragTouchListeners(); + } + private void onWindowInsetChanged() { if (updateSystemGestureInsetsTop()) { updateSystemUIStateIfNeeded(); @@ -659,6 +784,11 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold } private void applyTapExcludeRegion() { + if (Flags.createWindowlessWindowMagnifier()) { + applyTouchableRegion(); + return; + } + // Sometimes this can get posted and run after deleteWindowMagnification() is called. if (mMirrorView == null) return; @@ -709,6 +839,51 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold return regionInsideDragBorder; } + private void applyTouchableRegion() { + // Sometimes this can get posted and run after deleteWindowMagnification() is called. + if (mMirrorView == null) return; + + var surfaceControl = mSurfaceControlViewHost.getRootSurfaceControl(); + surfaceControl.setTouchableRegion(calculateTouchableRegion()); + } + + private Region calculateTouchableRegion() { + Region touchableRegion = new Region(0, 0, mMirrorView.getWidth(), mMirrorView.getHeight()); + + Region regionInsideDragBorder = new Region(mBorderDragSize, mBorderDragSize, + mMirrorView.getWidth() - mBorderDragSize, + mMirrorView.getHeight() - mBorderDragSize); + touchableRegion.op(regionInsideDragBorder, Region.Op.DIFFERENCE); + + Rect dragArea = new Rect(); + mDragView.getHitRect(dragArea); + + Rect topLeftArea = new Rect(); + mTopLeftCornerView.getHitRect(topLeftArea); + + Rect topRightArea = new Rect(); + mTopRightCornerView.getHitRect(topRightArea); + + Rect bottomLeftArea = new Rect(); + mBottomLeftCornerView.getHitRect(bottomLeftArea); + + Rect bottomRightArea = new Rect(); + mBottomRightCornerView.getHitRect(bottomRightArea); + + Rect closeArea = new Rect(); + mCloseView.getHitRect(closeArea); + + // Add touchable regions for drag and close + touchableRegion.op(dragArea, Region.Op.UNION); + touchableRegion.op(topLeftArea, Region.Op.UNION); + touchableRegion.op(topRightArea, Region.Op.UNION); + touchableRegion.op(bottomLeftArea, Region.Op.UNION); + touchableRegion.op(bottomRightArea, Region.Op.UNION); + touchableRegion.op(closeArea, Region.Op.UNION); + + return touchableRegion; + } + private String getAccessibilityWindowTitle() { return mResources.getString(com.android.internal.R.string.android_system_label); } @@ -852,8 +1027,84 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold * {@link #mMagnificationFrame}. */ private void modifyWindowMagnification(boolean computeWindowSize) { - mSfVsyncFrameProvider.postFrameCallback(mMirrorViewGeometryVsyncCallback); - updateMirrorViewLayout(computeWindowSize); + if (Flags.createWindowlessWindowMagnifier()) { + updateMirrorSurfaceGeometry(); + updateWindowlessMirrorViewLayout(computeWindowSize); + } else { + mSfVsyncFrameProvider.postFrameCallback(mMirrorViewGeometryVsyncCallback); + updateMirrorViewLayout(computeWindowSize); + } + } + + /** + * Updates {@link #mMirrorSurface}'s geometry. This modifies {@link #mTransaction} but does not + * apply it. + */ + @UiThread + private void updateMirrorSurfaceGeometry() { + if (isActivated() && mMirrorSurface != null + && calculateSourceBounds(mMagnificationFrame, mScale)) { + // The final destination for the magnification surface should be at 0,0 + // since the ViewRootImpl's position will change + mTmpRect.set(0, 0, mMagnificationFrame.width(), mMagnificationFrame.height()); + mTransaction.setGeometry(mMirrorSurface, mSourceBounds, mTmpRect, Surface.ROTATION_0); + + // Notify source bounds change when the magnifier is not animating. + if (!mAnimationController.isAnimating()) { + mWindowMagnifierCallback.onSourceBoundsChanged(mDisplayId, mSourceBounds); + } + } + } + + /** + * Updates the position of {@link mSurfaceControlViewHost} and layout params of MirrorView based + * on the position and size of {@link #mMagnificationFrame}. + * + * @param computeWindowSize set to {@code true} to compute window size with + * {@link #mMagnificationFrame}. + */ + @UiThread + private void updateWindowlessMirrorViewLayout(boolean computeWindowSize) { + if (!isActivated()) { + return; + } + + final int width = mMagnificationFrame.width() + 2 * mMirrorSurfaceMargin; + final int height = mMagnificationFrame.height() + 2 * mMirrorSurfaceMargin; + + final int minX = -mOuterBorderSize; + final int maxX = mWindowBounds.right - width + mOuterBorderSize; + final int x = MathUtils.clamp(mMagnificationFrame.left - mMirrorSurfaceMargin, minX, maxX); + + final int minY = -mOuterBorderSize; + final int maxY = mWindowBounds.bottom - height + mOuterBorderSize; + final int y = MathUtils.clamp(mMagnificationFrame.top - mMirrorSurfaceMargin, minY, maxY); + + if (computeWindowSize) { + LayoutParams params = (LayoutParams) mMirrorView.getLayoutParams(); + params.width = width; + params.height = height; + mSurfaceControlViewHost.relayout(params); + mTransaction.setCrop(mSurfaceControlViewHost.getSurfacePackage().getSurfaceControl(), + new Rect(0, 0, width, height)); + } + + mMirrorViewBounds.set(x, y, x + width, y + height); + mTransaction.setPosition( + mSurfaceControlViewHost.getSurfacePackage().getSurfaceControl(), x, y); + if (computeWindowSize) { + mSurfaceControlViewHost.getRootSurfaceControl().applyTransactionOnDraw(mTransaction); + } else { + mTransaction.apply(); + } + + // If they are not dragging the handle, we can move the drag handle immediately without + // disruption. But if they are dragging it, we avoid moving until the end of the drag. + if (!mIsDragging) { + mMirrorView.post(this::maybeRepositionButton); + } + + mMirrorViewRunnable.run(); } /** @@ -1094,7 +1345,7 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold /** * Enables window magnification with specified parameters. If the given scale is <strong>less - * than or equal to 1.0f<strong>, then + * than or equal to 1.0f</strong>, then * {@link WindowMagnificationController#deleteWindowMagnification()} will be called instead to * be consistent with the behavior of display magnification. * @@ -1110,7 +1361,7 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold /** * Enables window magnification with specified parameters. If the given scale is <strong>less - * than 1.0f<strong>, then + * than 1.0f</strong>, then * {@link WindowMagnificationController#deleteWindowMagnification()} will be called instead to * be consistent with the behavior of display magnification. * @@ -1426,10 +1677,8 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold final FrameLayout.LayoutParams layoutParams = (FrameLayout.LayoutParams) mDragView.getLayoutParams(); - mMirrorView.getBoundsOnScreen(mTmpRect); - final int newGravity; - if (mTmpRect.right >= screenEdgeX) { + if (mMirrorViewBounds.right >= screenEdgeX) { newGravity = Gravity.BOTTOM | Gravity.LEFT; } else { newGravity = Gravity.BOTTOM | Gravity.RIGHT; @@ -1449,8 +1698,8 @@ class WindowMagnificationController implements View.OnTouchListener, SurfaceHold mSettingsPanelVisibility = settingsPanelIsShown; mDragView.setBackground(mContext.getResources().getDrawable(settingsPanelIsShown - ? R.drawable.accessibility_window_magnification_drag_handle_background_change - : R.drawable.accessibility_window_magnification_drag_handle_background)); + ? R.drawable.accessibility_window_magnification_drag_handle_background_change_inset + : R.drawable.accessibility_window_magnification_drag_handle_background_inset)); PorterDuffColorFilter filter = new PorterDuffColorFilter( mContext.getColor(settingsPanelIsShown diff --git a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt index 49f34f18b06e..454ed27161a2 100644 --- a/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/authentication/data/repository/AuthenticationRepository.kt @@ -39,7 +39,7 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.scene.shared.flag.SceneContainerFlags import com.android.systemui.statusbar.pipeline.mobile.data.repository.MobileConnectionsRepository import com.android.systemui.user.data.repository.UserRepository -import com.android.systemui.util.kotlin.pairwise +import com.android.systemui.util.kotlin.onSubscriberAdded import com.android.systemui.util.time.SystemClock import dagger.Binds import dagger.Module @@ -54,7 +54,6 @@ import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged -import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart @@ -355,10 +354,7 @@ constructor( userRepository.selectedUserInfo.map { it.id }.distinctUntilChanged(), // Emits a value only when the number of downstream subscribers of this flow // increases. - flow.subscriptionCount.pairwise(initialValue = 0).filter { (previous, current) - -> - current > previous - }, + flow.onSubscriberAdded(), ) { selectedUserId, _ -> selectedUserId } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt index 7265c0c1cb94..d849b3a44519 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt @@ -21,8 +21,6 @@ import com.android.systemui.flags.FeatureFlagsClassic import com.android.systemui.flags.Flags import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow /** Provides access to bouncer-related application state. */ @SysUISingleton @@ -31,15 +29,10 @@ class BouncerRepository constructor( private val flags: FeatureFlagsClassic, ) { - private val _message = MutableStateFlow<String?>(null) /** The user-facing message to show in the bouncer. */ - val message: StateFlow<String?> = _message.asStateFlow() + val message = MutableStateFlow<String?>(null) /** Whether the user switcher should be displayed within the bouncer UI on large screens. */ val isUserSwitcherVisible: Boolean get() = flags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER) - - fun setMessage(message: String?) { - _message.value = message - } } diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt index c8ce245e48bf..d8be1afc4dd6 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt @@ -120,7 +120,7 @@ constructor( } fun setMessage(message: String?) { - repository.setMessage(message) + repository.message.value = message } /** @@ -129,13 +129,13 @@ constructor( */ fun resetMessage() { applicationScope.launch { - repository.setMessage(promptMessage(authenticationInteractor.getAuthenticationMethod())) + setMessage(promptMessage(authenticationInteractor.getAuthenticationMethod())) } } /** Removes the user-facing message. */ fun clearMessage() { - repository.setMessage(null) + setMessage(null) } /** @@ -196,7 +196,7 @@ constructor( * message without having the attempt trigger lockout. */ private suspend fun showWrongInputMessage() { - repository.setMessage(wrongInputMessage(authenticationInteractor.getAuthenticationMethod())) + setMessage(wrongInputMessage(authenticationInteractor.getAuthenticationMethod())) } /** Notifies that the input method editor (software keyboard) has been hidden by the user. */ diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt index 4d686a1ba0d4..4466cbbe05be 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt @@ -34,7 +34,9 @@ import com.android.systemui.common.shared.model.Text import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.inputmethod.domain.interactor.InputMethodInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlags +import com.android.systemui.user.domain.interactor.SelectedUserInteractor import com.android.systemui.user.ui.viewmodel.UserActionViewModel import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel import com.android.systemui.user.ui.viewmodel.UserViewModel @@ -66,8 +68,10 @@ class BouncerViewModel( @Application private val applicationScope: CoroutineScope, @Main private val mainDispatcher: CoroutineDispatcher, private val bouncerInteractor: BouncerInteractor, + private val inputMethodInteractor: InputMethodInteractor, private val simBouncerInteractor: SimBouncerInteractor, private val authenticationInteractor: AuthenticationInteractor, + private val selectedUserInteractor: SelectedUserInteractor, flags: SceneContainerFlags, selectedUser: Flow<UserViewModel>, users: Flow<List<UserViewModel>>, @@ -346,8 +350,10 @@ class BouncerViewModel( is AuthenticationMethodModel.Password -> PasswordBouncerViewModel( viewModelScope = newViewModelScope, - interactor = bouncerInteractor, isInputEnabled = isInputEnabled, + interactor = bouncerInteractor, + inputMethodInteractor = inputMethodInteractor, + selectedUserInteractor = selectedUserInteractor, ) is AuthenticationMethodModel.Pattern -> PatternBouncerViewModel( @@ -467,11 +473,13 @@ object BouncerViewModelModule { @Application applicationScope: CoroutineScope, @Main mainDispatcher: CoroutineDispatcher, bouncerInteractor: BouncerInteractor, + imeInteractor: InputMethodInteractor, simBouncerInteractor: SimBouncerInteractor, + actionButtonInteractor: BouncerActionButtonInteractor, authenticationInteractor: AuthenticationInteractor, + selectedUserInteractor: SelectedUserInteractor, flags: SceneContainerFlags, userSwitcherViewModel: UserSwitcherViewModel, - actionButtonInteractor: BouncerActionButtonInteractor, clock: SystemClock, devicePolicyManager: DevicePolicyManager, ): BouncerViewModel { @@ -480,8 +488,10 @@ object BouncerViewModelModule { applicationScope = applicationScope, mainDispatcher = mainDispatcher, bouncerInteractor = bouncerInteractor, + inputMethodInteractor = imeInteractor, simBouncerInteractor = simBouncerInteractor, authenticationInteractor = authenticationInteractor, + selectedUserInteractor = selectedUserInteractor, flags = flags, selectedUser = userSwitcherViewModel.selectedUser, users = userSwitcherViewModel.users, diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt index 5c9c997db7e4..1c8b84d82a56 100644 --- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt @@ -16,23 +16,32 @@ package com.android.systemui.bouncer.ui.viewmodel +import androidx.annotation.VisibleForTesting import com.android.systemui.authentication.shared.model.AuthenticationMethodModel import com.android.systemui.bouncer.domain.interactor.BouncerInteractor +import com.android.systemui.inputmethod.domain.interactor.InputMethodInteractor import com.android.systemui.res.R +import com.android.systemui.user.domain.interactor.SelectedUserInteractor +import com.android.systemui.util.kotlin.onSubscriberAdded +import kotlin.time.Duration.Companion.milliseconds import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.delay import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch /** Holds UI state and handles user input for the password bouncer UI. */ class PasswordBouncerViewModel( viewModelScope: CoroutineScope, - interactor: BouncerInteractor, isInputEnabled: StateFlow<Boolean>, + interactor: BouncerInteractor, + private val inputMethodInteractor: InputMethodInteractor, + private val selectedUserInteractor: SelectedUserInteractor, ) : AuthMethodBouncerViewModel( viewModelScope = viewModelScope, @@ -49,6 +58,9 @@ class PasswordBouncerViewModel( override val lockoutMessageId = R.string.kg_too_many_failed_password_attempts_dialog_message + /** Informs the UI whether the input method switcher button should be visible. */ + val isImeSwitcherButtonVisible: StateFlow<Boolean> = imeSwitcherRefreshingFlow() + /** Whether the text field element currently has focus. */ private val isTextFieldFocused = MutableStateFlow(false) @@ -87,6 +99,13 @@ class PasswordBouncerViewModel( _password.value = newPassword } + /** Notifies that the user clicked the button to change the input method. */ + fun onImeSwitcherButtonClicked(displayId: Int) { + viewModelScope.launch { + inputMethodInteractor.showInputMethodPicker(displayId, showAuxiliarySubtypes = false) + } + } + /** Notifies that the user has pressed the key for attempting to authenticate the password. */ fun onAuthenticateKeyPressed() { if (_password.value.isNotEmpty()) { @@ -103,4 +122,35 @@ class PasswordBouncerViewModel( fun onTextFieldFocusChanged(isFocused: Boolean) { isTextFieldFocused.value = isFocused } + + /** + * Whether the input method switcher button should be displayed in the password bouncer UI. The + * value may be stale at the moment of subscription to this flow, but it is guaranteed to be + * shortly updated with a fresh value. + * + * Note: Each added subscription triggers an IPC call in the background, so this should only be + * subscribed to by the UI once in its lifecycle (i.e. when the bouncer is shown). + */ + private fun imeSwitcherRefreshingFlow(): StateFlow<Boolean> { + val isImeSwitcherButtonVisible = MutableStateFlow(value = false) + viewModelScope.launch { + // Re-fetch the currently-enabled IMEs whenever the selected user changes, and whenever + // the UI subscribes to the `isImeSwitcherButtonVisible` flow. + combine( + // InputMethodManagerService sometimes takes some time to update its internal + // state when the selected user changes. As a workaround, delay fetching the IME + // info. + selectedUserInteractor.selectedUser.onEach { delay(DELAY_TO_FETCH_IMES) }, + isImeSwitcherButtonVisible.onSubscriberAdded() + ) { selectedUserId, _ -> + inputMethodInteractor.hasMultipleEnabledImesOrSubtypes(selectedUserId) + } + .collect { isImeSwitcherButtonVisible.value = it } + } + return isImeSwitcherButtonVisible.asStateFlow() + } + + companion object { + @VisibleForTesting val DELAY_TO_FETCH_IMES = 300.milliseconds + } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt new file mode 100644 index 000000000000..f7ba5a44f4c9 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/CommunalSceneStartable.kt @@ -0,0 +1,110 @@ +/* + * 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.communal + +import com.android.systemui.CoreStartable +import com.android.systemui.communal.domain.interactor.CommunalInteractor +import com.android.systemui.communal.shared.model.CommunalSceneKey +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dock.DockManager +import com.android.systemui.dock.retrieveIsDocked +import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.TransitionStep +import com.android.systemui.util.kotlin.sample +import javax.inject.Inject +import kotlin.time.Duration.Companion.seconds +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.FlowPreview +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.onEach + +/** + * A [CoreStartable] responsible for automatically navigating between communal scenes when certain + * conditions are met. + */ +@OptIn(ExperimentalCoroutinesApi::class, FlowPreview::class) +@SysUISingleton +class CommunalSceneStartable +@Inject +constructor( + private val dockManager: DockManager, + private val communalInteractor: CommunalInteractor, + private val keyguardTransitionInteractor: KeyguardTransitionInteractor, + @Application private val applicationScope: CoroutineScope, + @Background private val bgScope: CoroutineScope, +) : CoreStartable { + override fun start() { + // Handle automatically switching based on keyguard state. + keyguardTransitionInteractor.startedKeyguardTransitionStep + .mapLatest(::determineSceneAfterTransition) + .filterNotNull() + // TODO(b/322787129): Also set a custom transition animation here to avoid the regular + // slide-in animation when setting the scene programmatically + .onEach { nextScene -> communalInteractor.onSceneChanged(nextScene) } + .launchIn(applicationScope) + + // Handle automatically switching to communal when docked. + dockManager + .retrieveIsDocked() + // Allow some time after docking to ensure the dream doesn't start. If the dream + // starts, then we don't want to automatically transition to glanceable hub. + .debounce(DOCK_DEBOUNCE_DELAY) + .sample(keyguardTransitionInteractor.startedKeyguardState, ::Pair) + .onEach { (docked, lastStartedState) -> + if (docked && lastStartedState == KeyguardState.LOCKSCREEN) { + communalInteractor.onSceneChanged(CommunalSceneKey.Communal) + } + } + .launchIn(bgScope) + } + + private suspend fun determineSceneAfterTransition( + lastStartedTransition: TransitionStep, + ): CommunalSceneKey? { + val to = lastStartedTransition.to + val from = lastStartedTransition.from + val docked = dockManager.isDocked + + return when { + to == KeyguardState.DREAMING -> CommunalSceneKey.Blank + docked && to == KeyguardState.LOCKSCREEN && from != KeyguardState.GLANCEABLE_HUB -> { + CommunalSceneKey.Communal + } + to == KeyguardState.GONE -> CommunalSceneKey.Blank + !docked && !KeyguardState.deviceIsAwakeInState(to) -> { + // If the user taps the screen and wakes the device within this timeout, we don't + // want to dismiss the hub + delay(AWAKE_DEBOUNCE_DELAY) + CommunalSceneKey.Blank + } + else -> null + } + } + + companion object { + val AWAKE_DEBOUNCE_DELAY = 5.seconds + val DOCK_DEBOUNCE_DELAY = 1.seconds + } +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryModule.kt b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryModule.kt index ab0a2d0d232f..d7f163bad2e9 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryModule.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/data/repository/CommunalWidgetRepositoryModule.kt @@ -26,6 +26,7 @@ import com.android.systemui.communal.widgets.CommunalAppWidgetHost import com.android.systemui.communal.widgets.WidgetInteractionHandler import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.log.LogBuffer import com.android.systemui.log.dagger.CommunalLog @@ -35,6 +36,7 @@ import dagger.Module import dagger.Provides import java.util.Optional import javax.inject.Named +import kotlinx.coroutines.CoroutineScope @Module interface CommunalWidgetRepositoryModule { @@ -52,10 +54,19 @@ interface CommunalWidgetRepositoryModule { @Provides fun provideCommunalAppWidgetHost( @Application context: Context, + @Background backgroundScope: CoroutineScope, interactionHandler: WidgetInteractionHandler, @Main looper: Looper, + @CommunalLog logBuffer: LogBuffer, ): CommunalAppWidgetHost { - return CommunalAppWidgetHost(context, APP_WIDGET_HOST_ID, interactionHandler, looper) + return CommunalAppWidgetHost( + context, + backgroundScope, + APP_WIDGET_HOST_ID, + interactionHandler, + looper, + logBuffer, + ) } @SysUISingleton diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt index 61db02639788..5f1d89e079a7 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHost.kt @@ -22,14 +22,31 @@ import android.appwidget.AppWidgetProviderInfo import android.content.Context import android.os.Looper import android.widget.RemoteViews +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.core.Logger +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.launch /** Communal app widget host that creates a [CommunalAppWidgetHostView]. */ class CommunalAppWidgetHost( context: Context, + private val backgroundScope: CoroutineScope, hostId: Int, interactionHandler: RemoteViews.InteractionHandler, - looper: Looper + looper: Looper, + logBuffer: LogBuffer, ) : AppWidgetHost(context, hostId, interactionHandler, looper) { + + private val logger = Logger(logBuffer, TAG) + + private val _appWidgetIdToRemove = MutableSharedFlow<Int>() + + /** App widget ids that have been removed and no longer available. */ + val appWidgetIdToRemove: SharedFlow<Int> = _appWidgetIdToRemove.asSharedFlow() + override fun onCreateView( context: Context, appWidgetId: Int, @@ -52,4 +69,15 @@ class CommunalAppWidgetHost( // `createView`, but we are sure that the hostView is `CommunalAppWidgetHostView` return createView(context, appWidgetId, appWidget) as CommunalAppWidgetHostView } + + override fun onAppWidgetRemoved(appWidgetId: Int) { + backgroundScope.launch { + logger.i({ "App widget removed from system: $int1" }) { int1 = appWidgetId } + _appWidgetIdToRemove.emit(appWidgetId) + } + } + + companion object { + private const val TAG = "CommunalAppWidgetHost" + } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt index 586df32e6561..fb9abeb4fbcf 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/CommunalAppWidgetHostStartable.kt @@ -40,6 +40,7 @@ constructor( @Background private val bgScope: CoroutineScope, @Main private val uiDispatcher: CoroutineDispatcher ) : CoreStartable { + override fun start() { or(communalInteractor.isCommunalAvailable, communalInteractor.editModeOpen) // Only trigger updates on state changes, ignoring the initial false value. @@ -47,6 +48,10 @@ constructor( .filter { (previous, new) -> previous != new } .onEach { (_, shouldListen) -> updateAppWidgetHostActive(shouldListen) } .launchIn(bgScope) + + appWidgetHost.appWidgetIdToRemove + .onEach { appWidgetId -> communalInteractor.deleteWidget(appWidgetId) } + .launchIn(bgScope) } private suspend fun updateAppWidgetHostActive(active: Boolean) = diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt index ad1327e90710..54c709d4f053 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt @@ -29,6 +29,7 @@ import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult import com.android.internal.logging.UiEventLogger import com.android.systemui.communal.shared.log.CommunalUiEvent +import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel import com.android.systemui.compose.ComposeFacade.setCommunalEditWidgetActivityContent import javax.inject.Inject @@ -126,6 +127,7 @@ constructor( }, onEditDone = { try { + communalViewModel.onSceneChanged(CommunalSceneKey.Communal) checkNotNull(windowManagerService).lockNow(/* options */ null) finish() } catch (e: RemoteException) { diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt index 95233f701bbb..5ee2045865a6 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -24,12 +24,14 @@ import com.android.systemui.accessibility.Magnification import com.android.systemui.back.domain.interactor.BackActionInteractor import com.android.systemui.biometrics.BiometricNotificationService import com.android.systemui.clipboardoverlay.ClipboardListener +import com.android.systemui.communal.CommunalSceneStartable import com.android.systemui.communal.log.CommunalLoggerStartable import com.android.systemui.communal.widgets.CommunalAppWidgetHostStartable import com.android.systemui.controls.dagger.StartControlsStartableModule import com.android.systemui.dagger.qualifiers.PerUser import com.android.systemui.dreams.AssistantAttentionMonitor import com.android.systemui.dreams.DreamMonitor +import com.android.systemui.dreams.homecontrols.HomeControlsDreamStartable import com.android.systemui.globalactions.GlobalActionsComponent import com.android.systemui.keyboard.KeyboardUI import com.android.systemui.keyboard.PhysicalKeyboardCoreStartable @@ -50,7 +52,6 @@ import com.android.systemui.shortcut.ShortcutKeyDispatcher import com.android.systemui.statusbar.ImmersiveModeConfirmation import com.android.systemui.statusbar.gesture.GesturePointerEventListener import com.android.systemui.statusbar.notification.InstantAppNotifier -import com.android.systemui.statusbar.phone.KeyguardLiftController import com.android.systemui.statusbar.phone.ScrimController import com.android.systemui.statusbar.phone.StatusBarHeadsUpChangeListener import com.android.systemui.stylus.StylusUsiPowerStartable @@ -224,12 +225,6 @@ abstract class SystemUICoreStartableModule { @ClassKey(WMShell::class) abstract fun bindWMShell(sysui: WMShell): CoreStartable - /** Inject into KeyguardLiftController. */ - @Binds - @IntoMap - @ClassKey(KeyguardLiftController::class) - abstract fun bindKeyguardLiftController(sysui: KeyguardLiftController): CoreStartable - /** Inject into MediaTttSenderCoordinator. */ @Binds @IntoMap @@ -328,8 +323,18 @@ abstract class SystemUICoreStartableModule { @Binds @IntoMap + @ClassKey(CommunalSceneStartable::class) + abstract fun bindCommunalSceneStartable(impl: CommunalSceneStartable): CoreStartable + + @Binds + @IntoMap @ClassKey(CommunalAppWidgetHostStartable::class) abstract fun bindCommunalAppWidgetHostStartable( impl: CommunalAppWidgetHostStartable ): CoreStartable + + @Binds + @IntoMap + @ClassKey(HomeControlsDreamStartable::class) + abstract fun bindHomeControlsDreamStartable(impl: HomeControlsDreamStartable): CoreStartable } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 2587e2da2172..efcbd47b67b4 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -57,6 +57,7 @@ import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.flags.FlagDependenciesModule; import com.android.systemui.flags.FlagsModule; +import com.android.systemui.inputmethod.InputMethodModule; import com.android.systemui.keyboard.KeyboardModule; import com.android.systemui.keyevent.data.repository.KeyEventRepositoryModule; import com.android.systemui.keyguard.ui.view.layout.blueprints.KeyguardBlueprintModule; @@ -193,6 +194,7 @@ import javax.inject.Named; FlagsModule.class, FlagDependenciesModule.class, FooterActionsModule.class, + InputMethodModule.class, KeyEventRepositoryModule.class, KeyboardModule.class, KeyguardBlueprintModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/ui/binder/LiftToRunFaceAuthBinder.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/binder/LiftToRunFaceAuthBinder.kt new file mode 100644 index 000000000000..1fd7d009cee4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/ui/binder/LiftToRunFaceAuthBinder.kt @@ -0,0 +1,143 @@ +/* + * 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.deviceentry.ui.binder + +import android.content.pm.PackageManager +import android.hardware.Sensor +import android.hardware.TriggerEvent +import android.hardware.TriggerEventListener +import com.android.keyguard.ActiveUnlockConfig +import com.android.keyguard.KeyguardUpdateMonitor +import com.android.systemui.CoreStartable +import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor +import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor +import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor +import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.util.Assert +import com.android.systemui.util.sensors.AsyncSensorManager +import java.io.PrintWriter +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.filterNot +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch + +/** + * Triggers face auth and active unlock on lift when the device is showing the lock screen or + * bouncer. Only initialized if face auth is supported on the device. Not to be confused with the + * lift to wake gesture which is handled by {@link com.android.server.policy.PhoneWindowManager}. + */ +@SysUISingleton +class LiftToRunFaceAuthBinder +@Inject +constructor( + @Application private val scope: CoroutineScope, + private val packageManager: PackageManager, + private val asyncSensorManager: AsyncSensorManager, + private val keyguardUpdateMonitor: KeyguardUpdateMonitor, + keyguardInteractor: KeyguardInteractor, + primaryBouncerInteractor: PrimaryBouncerInteractor, + alternateBouncerInteractor: AlternateBouncerInteractor, + private val deviceEntryFaceAuthInteractor: DeviceEntryFaceAuthInteractor, + powerInteractor: PowerInteractor, +) : CoreStartable { + + private var pickupSensor: Sensor? = null + private val isListening: MutableStateFlow<Boolean> = MutableStateFlow(false) + private val stoppedListening: Flow<Unit> = isListening.filterNot { it }.map {} // map to Unit + + private val onAwakeKeyguard: Flow<Boolean> = + combine( + powerInteractor.isInteractive, + keyguardInteractor.isKeyguardVisible, + ) { isInteractive, isKeyguardVisible -> + isInteractive && isKeyguardVisible + } + private val bouncerShowing: Flow<Boolean> = + combine( + primaryBouncerInteractor.isShowing, + alternateBouncerInteractor.isVisible, + ) { primaryBouncerShowing, alternateBouncerShowing -> + primaryBouncerShowing || alternateBouncerShowing + } + private val listenForPickupSensor: Flow<Boolean> = + combine( + stoppedListening, + bouncerShowing, + onAwakeKeyguard, + ) { _, bouncerShowing, onAwakeKeyguard -> + (onAwakeKeyguard || bouncerShowing) && + deviceEntryFaceAuthInteractor.isFaceAuthEnabledAndEnrolled() + } + + override fun start() { + if (packageManager.hasSystemFeature(PackageManager.FEATURE_FACE)) { + init() + } + } + + private fun init() { + pickupSensor = asyncSensorManager.getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE) + scope.launch { + listenForPickupSensor.collect { listenForPickupSensor -> + updateListeningState(listenForPickupSensor) + } + } + } + + private val listener: TriggerEventListener = + object : TriggerEventListener() { + override fun onTrigger(event: TriggerEvent?) { + Assert.isMainThread() + deviceEntryFaceAuthInteractor.onDeviceLifted() + keyguardUpdateMonitor.requestActiveUnlock( + ActiveUnlockConfig.ActiveUnlockRequestOrigin.WAKE, + "KeyguardLiftController" + ) + + // Not listening anymore since trigger events unregister themselves + isListening.value = false + } + } + + override fun dump(pw: PrintWriter, args: Array<out String>) { + pw.println("LiftToRunFaceAuthBinder:") + pw.println(" pickupSensor: $pickupSensor") + pw.println(" isListening: ${isListening.value}") + } + + private fun updateListeningState(shouldListen: Boolean) { + if (pickupSensor == null) { + return + } + if (shouldListen != isListening.value) { + isListening.value = shouldListen + + if (shouldListen) { + asyncSensorManager.requestTriggerSensor(listener, pickupSensor) + } else { + asyncSensorManager.cancelTriggerSensor(listener, pickupSensor) + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java index 0656933804f3..ba74742fc47f 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/dagger/DreamModule.java @@ -17,6 +17,7 @@ package com.android.systemui.dreams.dagger; import android.annotation.Nullable; +import android.app.Service; import android.content.ComponentName; import android.content.Context; import android.content.pm.PackageManager; @@ -30,12 +31,18 @@ import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dreams.DreamOverlayNotificationCountProvider; import com.android.systemui.dreams.DreamOverlayService; import com.android.systemui.dreams.complication.dagger.ComplicationComponent; +import com.android.systemui.dreams.homecontrols.DreamActivityProvider; +import com.android.systemui.dreams.homecontrols.DreamActivityProviderImpl; +import com.android.systemui.dreams.homecontrols.HomeControlsDreamService; import com.android.systemui.dreams.touch.scrim.dagger.ScrimModule; import com.android.systemui.res.R; import com.android.systemui.touch.TouchInsetManager; +import dagger.Binds; import dagger.Module; import dagger.Provides; +import dagger.multibindings.ClassKey; +import dagger.multibindings.IntoMap; import java.util.Optional; import java.util.concurrent.Executor; @@ -88,6 +95,15 @@ public interface DreamModule { } /** + * Provides Home Controls Dream Service + */ + @Binds + @IntoMap + @ClassKey(HomeControlsDreamService.class) + Service bindHomeControlsDreamService( + HomeControlsDreamService service); + + /** * Provides a touch inset manager for dreams. */ @Provides @@ -151,4 +167,9 @@ public interface DreamModule { static String providesDreamOverlayWindowTitle(@Main Resources resources) { return resources.getString(R.string.app_label); } + + /** Provides activity for dream service */ + @Binds + DreamActivityProvider bindActivityProvider(DreamActivityProviderImpl impl); + } diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamActivityProvider.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamActivityProvider.kt new file mode 100644 index 000000000000..b35b7f5debb3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamActivityProvider.kt @@ -0,0 +1,27 @@ +/* + * 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.dreams.homecontrols + +import android.app.Activity +import android.service.dreams.DreamService + +fun interface DreamActivityProvider { + /** + * Provides abstraction for getting the activity associated with a dream service, so that the + * activity can be mocked in tests. + */ + fun getActivity(dreamService: DreamService): Activity? +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamActivityProviderImpl.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamActivityProviderImpl.kt new file mode 100644 index 000000000000..0854e939645b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/DreamActivityProviderImpl.kt @@ -0,0 +1,26 @@ +/* + * 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.dreams.homecontrols + +import android.app.Activity +import android.service.dreams.DreamService +import javax.inject.Inject + +class DreamActivityProviderImpl @Inject constructor() : DreamActivityProvider { + override fun getActivity(dreamService: DreamService): Activity { + return dreamService.activity + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamService.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamService.kt new file mode 100644 index 000000000000..e04a5052199c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamService.kt @@ -0,0 +1,88 @@ +/* + * 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.dreams.homecontrols + +import android.content.Intent +import android.service.controls.ControlsProviderService +import android.service.dreams.DreamService +import android.window.TaskFragmentInfo +import com.android.systemui.controls.settings.ControlsSettingsRepository +import com.android.systemui.dreams.DreamLogger +import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor +import com.android.systemui.log.LogBuffer +import com.android.systemui.log.dagger.DreamLog +import javax.inject.Inject + +class HomeControlsDreamService +@Inject +constructor( + private val controlsSettingsRepository: ControlsSettingsRepository, + private val taskFragmentFactory: TaskFragmentComponent.Factory, + private val homeControlsComponentInteractor: HomeControlsComponentInteractor, + private val dreamActivityProvider: DreamActivityProvider, + @DreamLog logBuffer: LogBuffer +) : DreamService() { + private lateinit var taskFragmentComponent: TaskFragmentComponent + + private val logger = DreamLogger(logBuffer, "HomeControlsDreamService") + + override fun onAttachedToWindow() { + super.onAttachedToWindow() + val activity = dreamActivityProvider.getActivity(this) + if (activity == null) { + finish() + return + } + taskFragmentComponent = + taskFragmentFactory + .create( + activity = activity, + onCreateCallback = this::onTaskFragmentCreated, + onInfoChangedCallback = this::onTaskFragmentInfoChanged, + hide = { finish() } + ) + .apply { createTaskFragment() } + } + + private fun onTaskFragmentInfoChanged(taskFragmentInfo: TaskFragmentInfo) { + if (taskFragmentInfo.isEmpty) { + logger.d("Finishing dream due to TaskFragment being empty") + finish() + } + } + + private fun onTaskFragmentCreated(taskFragmentInfo: TaskFragmentInfo) { + val setting = controlsSettingsRepository.allowActionOnTrivialControlsInLockscreen.value + val componentName = homeControlsComponentInteractor.panelComponent.value + logger.d("Starting embedding $componentName") + val intent = + Intent().apply { + component = componentName + putExtra(ControlsProviderService.EXTRA_LOCKSCREEN_ALLOW_TRIVIAL_CONTROLS, setting) + putExtra( + ControlsProviderService.EXTRA_CONTROLS_SURFACE, + ControlsProviderService.CONTROLS_SURFACE_DREAM + ) + } + taskFragmentComponent.startActivityInTaskFragment(intent) + } + + override fun onDetachedFromWindow() { + super.onDetachedFromWindow() + taskFragmentComponent.destroy() + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartable.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartable.kt new file mode 100644 index 000000000000..6cd94c623ff7 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/HomeControlsDreamStartable.kt @@ -0,0 +1,63 @@ +/* + * 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.dreams.homecontrols + +import android.content.ComponentName +import android.content.Context +import android.content.pm.PackageManager +import android.service.controls.flags.Flags.homePanelDream +import com.android.systemui.CoreStartable +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.dreams.homecontrols.domain.interactor.HomeControlsComponentInteractor +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +class HomeControlsDreamStartable +@Inject +constructor( + private val context: Context, + private val packageManager: PackageManager, + private val homeControlsComponentInteractor: HomeControlsComponentInteractor, + @Background private val bgScope: CoroutineScope, +) : CoreStartable { + + private val componentName = ComponentName(context, HomeControlsDreamService::class.java) + + override fun start() { + if (!homePanelDream()) return + bgScope.launch { + homeControlsComponentInteractor.panelComponent.collect { selectedPanelComponent -> + setEnableHomeControlPanel(selectedPanelComponent != null) + } + } + } + + private fun setEnableHomeControlPanel(enabled: Boolean) { + val packageState = + if (enabled) { + PackageManager.COMPONENT_ENABLED_STATE_ENABLED + } else { + PackageManager.COMPONENT_ENABLED_STATE_DISABLED + } + packageManager.setComponentEnabledSetting( + componentName, + packageState, + PackageManager.DONT_KILL_APP + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/TaskFragmentComponent.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/TaskFragmentComponent.kt new file mode 100644 index 000000000000..6f7dcb173156 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/TaskFragmentComponent.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.systemui.dreams.homecontrols + +import android.app.Activity +import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW +import android.content.Intent +import android.graphics.Rect +import android.os.Binder +import android.window.TaskFragmentCreationParams +import android.window.TaskFragmentInfo +import android.window.TaskFragmentOperation +import android.window.TaskFragmentOperation.OP_TYPE_DELETE_TASK_FRAGMENT +import android.window.TaskFragmentOperation.OP_TYPE_REORDER_TO_TOP_OF_TASK +import android.window.TaskFragmentOrganizer +import android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_CHANGE +import android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_CLOSE +import android.window.TaskFragmentOrganizer.TASK_FRAGMENT_TRANSIT_OPEN +import android.window.TaskFragmentTransaction +import android.window.TaskFragmentTransaction.TYPE_ACTIVITY_REPARENTED_TO_TASK +import android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_APPEARED +import android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_ERROR +import android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_INFO_CHANGED +import android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED +import android.window.TaskFragmentTransaction.TYPE_TASK_FRAGMENT_VANISHED +import android.window.WindowContainerTransaction +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.util.concurrency.DelayableExecutor +import dagger.assisted.Assisted +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +typealias FragmentInfoCallback = (TaskFragmentInfo) -> Unit + +/** Wrapper around TaskFragmentOrganizer for managing a task fragment within an activity */ +class TaskFragmentComponent +@AssistedInject +constructor( + @Assisted private val activity: Activity, + @Assisted("onCreateCallback") private val onCreateCallback: FragmentInfoCallback, + @Assisted("onInfoChangedCallback") private val onInfoChangedCallback: FragmentInfoCallback, + @Assisted private val hide: () -> Unit, + @Main private val executor: DelayableExecutor, +) { + + @AssistedFactory + fun interface Factory { + fun create( + activity: Activity, + @Assisted("onCreateCallback") onCreateCallback: FragmentInfoCallback, + @Assisted("onInfoChangedCallback") onInfoChangedCallback: FragmentInfoCallback, + hide: () -> Unit + ): TaskFragmentComponent + } + + private val fragmentToken = Binder() + private val organizer: TaskFragmentOrganizer = + object : TaskFragmentOrganizer(executor) { + + override fun onTransactionReady(transaction: TaskFragmentTransaction) { + handleTransactionReady(transaction) + } + } + .apply { registerOrganizer(true /* isSystemOrganizer */) } + + private fun handleTransactionReady(transaction: TaskFragmentTransaction) { + val resultT = WindowContainerTransaction() + + for (change in transaction.changes) { + change.taskFragmentInfo?.let { taskFragmentInfo -> + if (taskFragmentInfo.fragmentToken == fragmentToken) { + when (change.type) { + TYPE_TASK_FRAGMENT_APPEARED -> { + resultT.addTaskFragmentOperation( + fragmentToken, + TaskFragmentOperation.Builder(OP_TYPE_REORDER_TO_TOP_OF_TASK) + .build() + ) + + onCreateCallback(taskFragmentInfo) + } + TYPE_TASK_FRAGMENT_INFO_CHANGED -> { + onInfoChangedCallback(taskFragmentInfo) + } + TYPE_TASK_FRAGMENT_VANISHED -> { + hide() + } + TYPE_TASK_FRAGMENT_PARENT_INFO_CHANGED -> {} + TYPE_TASK_FRAGMENT_ERROR -> { + hide() + } + TYPE_ACTIVITY_REPARENTED_TO_TASK -> {} + else -> + throw IllegalArgumentException( + "Unknown TaskFragmentEvent=" + change.type + ) + } + } + } + } + organizer.onTransactionHandled( + transaction.transactionToken, + resultT, + TASK_FRAGMENT_TRANSIT_CHANGE, + false + ) + } + + /** Creates the task fragment */ + fun createTaskFragment() { + val taskBounds = Rect(activity.resources.configuration.windowConfiguration.bounds) + val fragmentOptions = + TaskFragmentCreationParams.Builder( + organizer.organizerToken, + fragmentToken, + activity.activityToken!! + ) + .setInitialRelativeBounds(taskBounds) + .setWindowingMode(WINDOWING_MODE_MULTI_WINDOW) + .build() + organizer.applyTransaction( + WindowContainerTransaction().createTaskFragment(fragmentOptions), + TASK_FRAGMENT_TRANSIT_CHANGE, + false + ) + } + + private fun WindowContainerTransaction.startActivity(intent: Intent) = + this.startActivityInTaskFragment(fragmentToken, activity.activityToken!!, intent, null) + + /** Starts the provided activity in the fragment and move it to the background */ + fun startActivityInTaskFragment(intent: Intent) { + organizer.applyTransaction( + WindowContainerTransaction().startActivity(intent), + TASK_FRAGMENT_TRANSIT_OPEN, + false + ) + } + + /** Destroys the task fragment */ + fun destroy() { + organizer.applyTransaction( + WindowContainerTransaction() + .addTaskFragmentOperation( + fragmentToken, + TaskFragmentOperation.Builder(OP_TYPE_DELETE_TASK_FRAGMENT).build() + ), + TASK_FRAGMENT_TRANSIT_CLOSE, + false + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/domain/interactor/HomeControlsComponentInteractor.kt b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/domain/interactor/HomeControlsComponentInteractor.kt new file mode 100644 index 000000000000..91e0547ff93d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/dreams/homecontrols/domain/interactor/HomeControlsComponentInteractor.kt @@ -0,0 +1,104 @@ +/* + * 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.dreams.homecontrols.domain.interactor + +import android.content.ComponentName +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow +import com.android.systemui.controls.ControlsServiceInfo +import com.android.systemui.controls.dagger.ControlsComponent +import com.android.systemui.controls.management.ControlsListingController +import com.android.systemui.controls.panels.AuthorizedPanelsRepository +import com.android.systemui.controls.panels.SelectedComponentRepository +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.user.data.repository.UserRepository +import com.android.systemui.util.kotlin.getOrNull +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn + +@SysUISingleton +@OptIn(ExperimentalCoroutinesApi::class) +class HomeControlsComponentInteractor +@Inject +constructor( + private val selectedComponentRepository: SelectedComponentRepository, + private val controlsComponent: ControlsComponent, + private val authorizedPanelsRepository: AuthorizedPanelsRepository, + userRepository: UserRepository, + @Background private val bgScope: CoroutineScope +) { + private val controlsListingController = + controlsComponent.getControlsListingController().getOrNull() + + /** Gets the current user's selected panel, or null if there isn't one */ + private val selectedItem: Flow<SelectedComponentRepository.SelectedComponent?> = + userRepository.selectedUserInfo + .flatMapLatest { user -> + selectedComponentRepository.selectedComponentFlow(user.userHandle) + } + .map { if (it?.isPanel == true) it else null } + + /** Gets all the available panels which are authorized by the user */ + private fun allPanelItem(): Flow<List<PanelComponent>> { + if (controlsListingController == null) { + return emptyFlow() + } + return conflatedCallbackFlow { + val listener = + object : ControlsListingController.ControlsListingCallback { + override fun onServicesUpdated(serviceInfos: List<ControlsServiceInfo>) { + trySend(serviceInfos) + } + } + controlsListingController.addCallback(listener) + awaitClose { controlsListingController.removeCallback(listener) } + } + .onStart { emit(controlsListingController.getCurrentServices()) } + .map { serviceInfos -> + val authorizedPanels = authorizedPanelsRepository.getAuthorizedPanels() + serviceInfos.mapNotNull { + val panelActivity = it.panelActivity + if (it.componentName.packageName in authorizedPanels && panelActivity != null) { + PanelComponent(it.componentName, panelActivity) + } else { + null + } + } + } + } + val panelComponent: StateFlow<ComponentName?> = + combine(allPanelItem(), selectedItem) { items, selected -> + val item = + items.firstOrNull { it.componentName == selected?.componentName } + ?: items.firstOrNull() + item?.panelActivity + } + .stateIn(bgScope, SharingStarted.WhileSubscribed(), null) + + data class PanelComponent(val componentName: ComponentName, val panelActivity: ComponentName) +} diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt index 15404238099d..df0566e246a8 100644 --- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt +++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt @@ -29,6 +29,8 @@ import com.android.systemui.flags.Flags.MIGRATE_KEYGUARD_STATUS_BAR_VIEW import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor +import com.android.systemui.statusbar.notification.interruption.VisualInterruptionRefactor +import com.android.systemui.statusbar.notification.shared.NotificationAvalancheSuppression import com.android.systemui.statusbar.notification.shared.NotificationIconContainerRefactor import com.android.systemui.statusbar.notification.shared.NotificationsLiveDataStoreRefactor import javax.inject.Inject @@ -45,6 +47,7 @@ class FlagDependencies @Inject constructor(featureFlags: FeatureFlagsClassic, ha // Internal notification frontend dependencies NotificationsLiveDataStoreRefactor.token dependsOn NotificationIconContainerRefactor.token FooterViewRefactor.token dependsOn NotificationIconContainerRefactor.token + NotificationAvalancheSuppression.token dependsOn VisualInterruptionRefactor.token // Internal keyguard dependencies KeyguardShadeMigrationNssl.token dependsOn keyguardBottomAreaRefactor diff --git a/packages/SystemUI/src/com/android/systemui/inputmethod/InputMethodModule.kt b/packages/SystemUI/src/com/android/systemui/inputmethod/InputMethodModule.kt new file mode 100644 index 000000000000..bac48f176712 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/inputmethod/InputMethodModule.kt @@ -0,0 +1,29 @@ +/* + * 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.inputmethod + +import com.android.systemui.inputmethod.data.repository.InputMethodRepositoryModule +import dagger.Module + +/** Module for providing objects exposed by the input method package. */ +@Module( + includes = + [ + InputMethodRepositoryModule::class, + ], +) +object InputMethodModule diff --git a/packages/SystemUI/src/com/android/systemui/inputmethod/data/model/InputMethodModel.kt b/packages/SystemUI/src/com/android/systemui/inputmethod/data/model/InputMethodModel.kt new file mode 100644 index 000000000000..bdc18b322ac0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/inputmethod/data/model/InputMethodModel.kt @@ -0,0 +1,51 @@ +/* + * 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.inputmethod.data.model + +/** + * Models an input method editor (IME). + * + * @see android.view.inputmethod.InputMethodInfo + */ +data class InputMethodModel( + /** A unique ID for this input method. */ + val imeId: String, + /** The subtypes of this IME (may be empty). */ + val subtypes: List<Subtype>, +) { + /** + * A Subtype can describe locale (e.g. en_US, fr_FR...) and mode (e.g. voice, keyboard), and is + * used for IME switch and settings. + * + * @see android.view.inputmethod.InputMethodSubtype + */ + data class Subtype( + /** A unique ID for this IME subtype. */ + val subtypeId: Int, + /** + * Whether this subtype is auxiliary. An auxiliary subtype will not be shown in the list of + * enabled IMEs for choosing the current IME in Settings, but it will be shown in the list + * of IMEs in the IME switcher to allow the user to tentatively switch to this subtype while + * an IME is shown. + * + * The intent of this flag is to allow for IMEs that are invoked in a one-shot way as + * auxiliary input mode, and return to the previous IME once it is finished (e.g. voice + * input). + */ + val isAuxiliary: Boolean, + ) +} diff --git a/packages/SystemUI/src/com/android/systemui/inputmethod/data/repository/InputMethodRepository.kt b/packages/SystemUI/src/com/android/systemui/inputmethod/data/repository/InputMethodRepository.kt new file mode 100644 index 000000000000..5f316c4495ec --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/inputmethod/data/repository/InputMethodRepository.kt @@ -0,0 +1,139 @@ +/* + * 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.inputmethod.data.repository + +import android.annotation.SuppressLint +import android.os.UserHandle +import android.view.inputmethod.InputMethodInfo +import android.view.inputmethod.InputMethodManager +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.inputmethod.data.model.InputMethodModel +import dagger.Binds +import dagger.Module +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.withContext + +/** Provides access to input-method related application state in the bouncer. */ +interface InputMethodRepository { + /** + * Creates and returns a new `Flow` of installed input methods that are enabled for the + * specified user. + * + * @param fetchSubtypes Whether to fetch the IME Subtypes as well (requires an additional IPC + * call for each IME, avoid if not needed). + * @see InputMethodManager.getEnabledInputMethodListAsUser + */ + suspend fun enabledInputMethods(userId: Int, fetchSubtypes: Boolean): Flow<InputMethodModel> + + /** Returns enabled subtypes for the currently selected input method. */ + suspend fun selectedInputMethodSubtypes(): List<InputMethodModel.Subtype> + + /** + * Shows the system's input method picker dialog. + * + * @param displayId The display ID on which to show the dialog. + * @param showAuxiliarySubtypes Whether to show auxiliary input method subtypes in the list of + * enabled IMEs. + * @see InputMethodManager.showInputMethodPickerFromSystem + */ + suspend fun showInputMethodPicker(displayId: Int, showAuxiliarySubtypes: Boolean) +} + +@SysUISingleton +class InputMethodRepositoryImpl +@Inject +constructor( + @Background private val backgroundDispatcher: CoroutineDispatcher, + private val inputMethodManager: InputMethodManager, +) : InputMethodRepository { + + override suspend fun enabledInputMethods( + userId: Int, + fetchSubtypes: Boolean + ): Flow<InputMethodModel> { + return withContext(backgroundDispatcher) { + inputMethodManager.getEnabledInputMethodListAsUser(UserHandle.of(userId)) + } + .asFlow() + .map { inputMethodInfo -> + InputMethodModel( + imeId = inputMethodInfo.id, + subtypes = + if (fetchSubtypes) { + enabledInputMethodSubtypes( + inputMethodInfo, + allowsImplicitlyEnabledSubtypes = true + ) + } else { + listOf() + } + ) + } + } + + override suspend fun selectedInputMethodSubtypes(): List<InputMethodModel.Subtype> { + return enabledInputMethodSubtypes( + inputMethodInfo = null, // Fetch subtypes for the currently-selected IME. + allowsImplicitlyEnabledSubtypes = false + ) + } + + @SuppressLint("MissingPermission") + override suspend fun showInputMethodPicker(displayId: Int, showAuxiliarySubtypes: Boolean) { + withContext(backgroundDispatcher) { + inputMethodManager.showInputMethodPickerFromSystem(showAuxiliarySubtypes, displayId) + } + } + + /** + * Returns a list of enabled input method subtypes for the specified input method info. + * + * @param inputMethodInfo The [InputMethodInfo] whose subtypes list will be returned. If `null`, + * returns enabled subtypes for the currently selected [InputMethodInfo]. + * @param allowsImplicitlyEnabledSubtypes Whether to allow to return the implicitly enabled + * subtypes. If an input method info doesn't have enabled subtypes, the framework will + * implicitly enable subtypes according to the current system language. + * @see InputMethodManager.getEnabledInputMethodSubtypeList + */ + private suspend fun enabledInputMethodSubtypes( + inputMethodInfo: InputMethodInfo?, + allowsImplicitlyEnabledSubtypes: Boolean + ): List<InputMethodModel.Subtype> { + return withContext(backgroundDispatcher) { + inputMethodManager.getEnabledInputMethodSubtypeList( + inputMethodInfo, + allowsImplicitlyEnabledSubtypes + ) + } + .map { + InputMethodModel.Subtype( + subtypeId = it.subtypeId, + isAuxiliary = it.isAuxiliary, + ) + } + } +} + +@Module +interface InputMethodRepositoryModule { + @Binds fun repository(impl: InputMethodRepositoryImpl): InputMethodRepository +} diff --git a/packages/SystemUI/src/com/android/systemui/inputmethod/domain/interactor/InputMethodInteractor.kt b/packages/SystemUI/src/com/android/systemui/inputmethod/domain/interactor/InputMethodInteractor.kt new file mode 100644 index 000000000000..c54aa7f2c6a5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/inputmethod/domain/interactor/InputMethodInteractor.kt @@ -0,0 +1,53 @@ +/* + * 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.inputmethod.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.inputmethod.data.repository.InputMethodRepository +import javax.inject.Inject +import kotlinx.coroutines.flow.count +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.take + +/** Hosts application business logic related to input methods (e.g. software keyboard). */ +@SysUISingleton +class InputMethodInteractor +@Inject +constructor( + private val repository: InputMethodRepository, +) { + /** + * Returns whether there are multiple enabled input methods to choose from for password input. + * + * Method adapted from `com.android.inputmethod.latin.Utils`. + */ + suspend fun hasMultipleEnabledImesOrSubtypes(userId: Int): Boolean { + // Count IMEs that either have no subtypes, or have at least one non-auxiliary subtype. + val matchingInputMethods = + repository + .enabledInputMethods(userId, fetchSubtypes = true) + .filter { ime -> ime.subtypes.isEmpty() || ime.subtypes.any { !it.isAuxiliary } } + .take(2) // Short-circuit if we find at least 2 matching IMEs. + + return matchingInputMethods.count() > 1 || repository.selectedInputMethodSubtypes().size > 1 + } + + /** Shows the system's input method picker dialog. */ + suspend fun showInputMethodPicker(displayId: Int, showAuxiliarySubtypes: Boolean) { + repository.showInputMethodPicker(displayId, showAuxiliarySubtypes) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java index 13e38358477a..e16f8dcbb00e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java +++ b/packages/SystemUI/src/com/android/systemui/keyguard/dagger/KeyguardModule.java @@ -51,7 +51,7 @@ import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.keyguard.WindowManagerLockscreenVisibilityManager; import com.android.systemui.keyguard.data.quickaffordance.KeyguardDataQuickAffordanceModule; -import com.android.systemui.keyguard.data.repository.KeyguardFaceAuthModule; +import com.android.systemui.keyguard.data.repository.DeviceEntryFaceAuthModule; import com.android.systemui.keyguard.data.repository.KeyguardRepositoryModule; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; import com.android.systemui.keyguard.domain.interactor.StartKeyguardTransitionModule; @@ -104,7 +104,7 @@ import kotlinx.coroutines.CoroutineDispatcher; FalsingModule.class, KeyguardDataQuickAffordanceModule.class, KeyguardRepositoryModule.class, - KeyguardFaceAuthModule.class, + DeviceEntryFaceAuthModule.class, KeyguardDisplayModule.class, StartKeyguardTransitionModule.class, ResourceTrimmerModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthModule.kt index fede47957a7b..4cd544ff658e 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardFaceAuthModule.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/DeviceEntryFaceAuthModule.kt @@ -23,6 +23,7 @@ import com.android.systemui.deviceentry.data.repository.DeviceEntryFaceAuthRepos import com.android.systemui.deviceentry.data.repository.DeviceEntryFaceAuthRepositoryImpl import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor import com.android.systemui.deviceentry.domain.interactor.SystemUIDeviceEntryFaceAuthInteractor +import com.android.systemui.deviceentry.ui.binder.LiftToRunFaceAuthBinder import com.android.systemui.log.table.TableLogBuffer import com.android.systemui.log.table.TableLogBufferFactory import dagger.Binds @@ -32,7 +33,7 @@ import dagger.multibindings.ClassKey import dagger.multibindings.IntoMap @Module -interface KeyguardFaceAuthModule { +interface DeviceEntryFaceAuthModule { @Binds fun deviceEntryFaceAuthRepository( impl: DeviceEntryFaceAuthRepositoryImpl @@ -41,13 +42,20 @@ interface KeyguardFaceAuthModule { @Binds @IntoMap @ClassKey(SystemUIDeviceEntryFaceAuthInteractor::class) - fun bind(impl: SystemUIDeviceEntryFaceAuthInteractor): CoreStartable + fun bindSystemUIDeviceEntryFaceAuthInteractor( + impl: SystemUIDeviceEntryFaceAuthInteractor + ): CoreStartable @Binds fun keyguardFaceAuthInteractor( impl: SystemUIDeviceEntryFaceAuthInteractor ): DeviceEntryFaceAuthInteractor + @Binds + @IntoMap + @ClassKey(LiftToRunFaceAuthBinder::class) + fun bindLiftToRunFaceAuthBinder(impl: LiftToRunFaceAuthBinder): CoreStartable + companion object { @Provides @SysUISingleton diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt index 9d38be941077..71d941ad8d22 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGlanceableHubTransitionInteractor.kt @@ -28,6 +28,7 @@ import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepositor import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionModeOnCanceled import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.util.kotlin.Utils.Companion.sample as sampleMultiple import com.android.systemui.util.kotlin.sample import javax.inject.Inject import kotlin.time.Duration.Companion.milliseconds @@ -64,6 +65,7 @@ constructor( listenForHubToAlternateBouncer() listenForHubToOccluded() listenForHubToGone() + listenForHubToDreaming() } override fun getDefaultAnimatorForTransitionsToState(toState: KeyguardState): ValueAnimator { @@ -131,6 +133,23 @@ constructor( } } + private fun listenForHubToDreaming() { + val invalidFromStates = setOf(KeyguardState.AOD, KeyguardState.DOZING) + scope.launch("$TAG#listenForHubToDreaming") { + keyguardInteractor.isAbleToDream + .sampleMultiple(startedKeyguardTransitionStep, finishedKeyguardState) + .collect { (isAbleToDream, lastStartedTransition, finishedKeyguardState) -> + val isOnHub = finishedKeyguardState == KeyguardState.GLANCEABLE_HUB + val isTransitionInterruptible = + lastStartedTransition.to == KeyguardState.GLANCEABLE_HUB && + !invalidFromStates.contains(lastStartedTransition.from) + if (isAbleToDream && (isOnHub || isTransitionInterruptible)) { + startTransitionTo(KeyguardState.DREAMING) + } + } + } + } + private fun listenForHubToOccluded() { scope.launch { keyguardInteractor.isKeyguardOccluded.sample(startedKeyguardState, ::Pair).collect { diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt index 3965648bd224..deb70b7f1086 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt @@ -405,7 +405,9 @@ constructor( interpolator = Interpolators.LINEAR duration = when (toState) { - KeyguardState.DREAMING -> TO_DREAMING_DURATION + // Adds 100ms to the overall delay to workaround legacy setOccluded calls + // being delayed in KeyguardViewMediator + KeyguardState.DREAMING -> TO_DREAMING_DURATION + 100.milliseconds KeyguardState.OCCLUDED -> TO_OCCLUDED_DURATION KeyguardState.AOD -> TO_AOD_DURATION KeyguardState.DOZING -> TO_DOZING_DURATION diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt index 388834597f60..a1f94250e149 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceInteractor.kt @@ -56,6 +56,7 @@ import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map @@ -102,10 +103,10 @@ constructor( quickAffordanceAlwaysVisible(position), keyguardInteractor.isDozing, keyguardInteractor.isKeyguardShowing, - shadeInteractor.anyExpansion, + shadeInteractor.anyExpansion.map { it < 1.0f }.distinctUntilChanged(), biometricSettingsRepository.isCurrentUserInLockdown, - ) { affordance, isDozing, isKeyguardShowing, qsExpansion, isUserInLockdown -> - if (!isDozing && isKeyguardShowing && (qsExpansion < 1.0f) && !isUserInLockdown) { + ) { affordance, isDozing, isKeyguardShowing, isQuickSettingsVisible, isUserInLockdown -> + if (!isDozing && isKeyguardShowing && isQuickSettingsVisible && !isUserInLockdown) { affordance } else { KeyguardQuickAffordanceModel.Hidden diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/Edge.kt b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/Edge.kt new file mode 100644 index 000000000000..a0f9be629132 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyguard/shared/model/Edge.kt @@ -0,0 +1,22 @@ +/* + * 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.keyguard.shared.model + +/** FROM -> TO keyguard transition. null values are allowed to signify FROM -> *, or * -> TO */ +data class Edge( + val from: KeyguardState?, + val to: KeyguardState?, +) diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt index 4abda741d495..00b798901352 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/KeyguardTransitionAnimationFlow.kt @@ -22,6 +22,7 @@ import com.android.keyguard.logging.KeyguardTransitionAnimationLogger import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.Edge import com.android.systemui.keyguard.shared.model.KeyguardState import com.android.systemui.keyguard.shared.model.TransitionState import com.android.systemui.keyguard.shared.model.TransitionState.CANCELED @@ -174,11 +175,6 @@ constructor( } } - data class Edge( - val from: KeyguardState?, - val to: KeyguardState?, - ) - data class StateToValue( val transitionState: TransitionState, val value: Float?, diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt index 703bb879533c..873cc847fa60 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt @@ -19,6 +19,7 @@ package com.android.systemui.keyguard.ui.binder import android.annotation.SuppressLint import android.content.res.ColorStateList +import android.util.StateSet import android.view.HapticFeedbackConstants import android.view.View import androidx.lifecycle.Lifecycle @@ -113,6 +114,8 @@ object DeviceEntryIconViewBinder { fgIconView.repeatWhenAttached { repeatOnLifecycle(Lifecycle.State.STARTED) { + // Start with an empty state + fgIconView.setImageState(StateSet.NOTHING, /* merge */ false) launch { fgViewModel.viewModel.collect { viewModel -> fgIconView.setImageState( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/PreviewKeyguardBlueprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/PreviewKeyguardBlueprintViewBinder.kt deleted file mode 100644 index 2feaa2e81c0f..000000000000 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/PreviewKeyguardBlueprintViewBinder.kt +++ /dev/null @@ -1,82 +0,0 @@ -/* - * 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.systemui.keyguard.ui.binder - -import android.os.Trace -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.constraintlayout.widget.ConstraintSet -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.repeatOnLifecycle -import com.android.systemui.keyguard.ui.viewmodel.KeyguardBlueprintViewModel -import com.android.systemui.lifecycle.repeatWhenAttached -import kotlinx.coroutines.DisposableHandle -import kotlinx.coroutines.launch - -/** - * Binds the existing blueprint to the constraint layout that previews keyguard. - * - * This view binder should only inflate and add relevant views and apply the constraints. Actual - * data binding should be done in {@link KeyguardPreviewRenderer} - */ -class PreviewKeyguardBlueprintViewBinder { - companion object { - - /** - * Binds the existing blueprint to the constraint layout that previews keyguard. - * - * @param constraintLayout The root view to bind to - * @param viewModel The instance of the view model that contains flows we collect on. - * @param finishedAddViewCallback Called when we have finished inflating the views. - */ - fun bind( - constraintLayout: ConstraintLayout, - viewModel: KeyguardBlueprintViewModel, - finishedAddViewCallback: () -> Unit - ): DisposableHandle { - return constraintLayout.repeatWhenAttached { - repeatOnLifecycle(Lifecycle.State.CREATED) { - launch { - viewModel.blueprint.collect { blueprint -> - val prevBluePrint = viewModel.currentBluePrint - Trace.beginSection("PreviewKeyguardBlueprint#applyBlueprint") - - ConstraintSet().apply { - clone(constraintLayout) - val emptyLayout = ConstraintSet.Layout() - knownIds.forEach { getConstraint(it).layout.copyFrom(emptyLayout) } - blueprint.applyConstraints(this) - // Add and remove views of sections that are not contained by the - // other. - blueprint.replaceViews( - prevBluePrint, - constraintLayout, - bindData = false - ) - applyTo(constraintLayout) - } - - viewModel.currentBluePrint = blueprint - finishedAddViewCallback.invoke() - Trace.endSection() - } - } - } - } - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt index 841bad4c15cc..a0c0095b34a2 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt @@ -41,6 +41,7 @@ import android.view.WindowManager import android.widget.FrameLayout import android.widget.TextView import androidx.constraintlayout.widget.ConstraintLayout +import androidx.constraintlayout.widget.ConstraintSet import androidx.core.view.isInvisible import com.android.keyguard.ClockEventController import com.android.keyguard.KeyguardClockSwitch @@ -54,15 +55,12 @@ import com.android.systemui.communal.ui.viewmodel.CommunalTutorialIndicatorViewM import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.flags.FeatureFlagsClassic -import com.android.systemui.keyguard.shared.KeyguardShadeMigrationNssl import com.android.systemui.keyguard.ui.binder.KeyguardPreviewClockViewBinder import com.android.systemui.keyguard.ui.binder.KeyguardPreviewSmartspaceViewBinder import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder import com.android.systemui.keyguard.ui.binder.KeyguardRootViewBinder -import com.android.systemui.keyguard.ui.binder.PreviewKeyguardBlueprintViewBinder import com.android.systemui.keyguard.ui.view.KeyguardRootView -import com.android.systemui.keyguard.ui.viewmodel.KeyguardBlueprintViewModel +import com.android.systemui.keyguard.ui.view.layout.sections.DefaultShortcutsSection import com.android.systemui.keyguard.ui.viewmodel.KeyguardBottomAreaViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardPreviewClockViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardPreviewSmartspaceViewModel @@ -124,19 +122,18 @@ constructor( private val broadcastDispatcher: BroadcastDispatcher, private val lockscreenSmartspaceController: LockscreenSmartspaceController, private val udfpsOverlayInteractor: UdfpsOverlayInteractor, - private val featureFlags: FeatureFlagsClassic, private val falsingManager: FalsingManager, private val vibratorHelper: VibratorHelper, private val indicationController: KeyguardIndicationController, private val keyguardRootViewModel: KeyguardRootViewModel, @Assisted bundle: Bundle, - private val keyguardBlueprintViewModel: KeyguardBlueprintViewModel, private val occludingAppDeviceEntryMessageViewModel: OccludingAppDeviceEntryMessageViewModel, private val chipbarCoordinator: ChipbarCoordinator, private val screenOffAnimationController: ScreenOffAnimationController, private val shadeInteractor: ShadeInteractor, private val secureSettings: SecureSettings, private val communalTutorialViewModel: CommunalTutorialIndicatorViewModel, + private val defaultShortcutsSection: DefaultShortcutsSection, ) { val hostToken: IBinder? = bundle.getBinder(KEY_HOST_TOKEN) private val width: Int = bundle.getInt(KEY_VIEW_WIDTH) @@ -393,32 +390,32 @@ constructor( setUpUdfps(previewContext, rootView) - disposables.add( - PreviewKeyguardBlueprintViewBinder.bind(keyguardRootView, keyguardBlueprintViewModel) { - if (keyguardBottomAreaRefactor()) { - setupShortcuts(keyguardRootView) - } - - if (!shouldHideClock) { - setUpClock(previewContext, rootView) - KeyguardPreviewClockViewBinder.bind( - largeClockHostView, - smallClockHostView, - clockViewModel, - ) - } + if (keyguardBottomAreaRefactor()) { + setupShortcuts(keyguardRootView) + } - setUpSmartspace(previewContext, rootView) - smartSpaceView?.let { - KeyguardPreviewSmartspaceViewBinder.bind(it, smartspaceViewModel) - } + if (!shouldHideClock) { + setUpClock(previewContext, rootView) + KeyguardPreviewClockViewBinder.bind( + largeClockHostView, + smallClockHostView, + clockViewModel, + ) + } - setupCommunalTutorialIndicator(keyguardRootView) - } - ) + setUpSmartspace(previewContext, rootView) + smartSpaceView?.let { KeyguardPreviewSmartspaceViewBinder.bind(it, smartspaceViewModel) } + setupCommunalTutorialIndicator(keyguardRootView) } private fun setupShortcuts(keyguardRootView: ConstraintLayout) { + // Add shortcuts + val cs = ConstraintSet() + cs.clone(keyguardRootView) + defaultShortcutsSection.addViews(keyguardRootView) + defaultShortcutsSection.applyConstraints(cs) + cs.applyTo(keyguardRootView) + keyguardRootView.findViewById<LaunchableImageView?>(R.id.start_button)?.let { imageView -> shortcutsBindings.add( KeyguardQuickAffordanceViewBinder.bind( @@ -476,53 +473,40 @@ constructor( } private fun setUpClock(previewContext: Context, parentView: ViewGroup) { - largeClockHostView = - if (KeyguardShadeMigrationNssl.isEnabled) { - parentView.requireViewById<FrameLayout>(R.id.lockscreen_clock_view_large) - } else { - val hostView = FrameLayout(previewContext) - hostView.layoutParams = - FrameLayout.LayoutParams( - FrameLayout.LayoutParams.MATCH_PARENT, - FrameLayout.LayoutParams.MATCH_PARENT, - ) - parentView.addView(hostView) - hostView - } + val resources = parentView.resources + largeClockHostView = FrameLayout(previewContext) + largeClockHostView.layoutParams = + FrameLayout.LayoutParams( + FrameLayout.LayoutParams.MATCH_PARENT, + FrameLayout.LayoutParams.MATCH_PARENT, + ) + parentView.addView(largeClockHostView) largeClockHostView.isInvisible = true - smallClockHostView = - if (KeyguardShadeMigrationNssl.isEnabled) { - parentView.requireViewById<FrameLayout>(R.id.lockscreen_clock_view) - } else { - val resources = parentView.resources - val hostView = FrameLayout(previewContext) - val layoutParams = - FrameLayout.LayoutParams( - FrameLayout.LayoutParams.WRAP_CONTENT, - resources.getDimensionPixelSize( - com.android.systemui.customization.R.dimen.small_clock_height - ) - ) - layoutParams.topMargin = - KeyguardPreviewSmartspaceViewModel.getStatusBarHeight(resources) + - resources.getDimensionPixelSize( - com.android.systemui.customization.R.dimen.small_clock_padding_top - ) - hostView.layoutParams = layoutParams - - hostView.setPaddingRelative( - resources.getDimensionPixelSize( - com.android.systemui.customization.R.dimen.clock_padding_start - ), - 0, - 0, - 0 + smallClockHostView = FrameLayout(previewContext) + val layoutParams = + FrameLayout.LayoutParams( + FrameLayout.LayoutParams.WRAP_CONTENT, + resources.getDimensionPixelSize( + com.android.systemui.customization.R.dimen.small_clock_height ) - hostView.clipChildren = false - parentView.addView(hostView) - hostView - } + ) + layoutParams.topMargin = + KeyguardPreviewSmartspaceViewModel.getStatusBarHeight(resources) + + resources.getDimensionPixelSize( + com.android.systemui.customization.R.dimen.small_clock_padding_top + ) + smallClockHostView.layoutParams = layoutParams + smallClockHostView.setPaddingRelative( + resources.getDimensionPixelSize( + com.android.systemui.customization.R.dimen.clock_padding_start + ), + 0, + 0, + 0 + ) + smallClockHostView.clipChildren = false + parentView.addView(smallClockHostView) smallClockHostView.isInvisible = true // TODO (b/283465254): Move the listeners to KeyguardClockRepository diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt index 400d0dc2b242..a651c10d1c35 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt @@ -87,7 +87,8 @@ constructor( return } // This moves the existing NSSL view to a different parent, as the controller is a - // singleton and recreating it has other bad side effects + // singleton and recreating it has other bad side effects. + // In the SceneContainer, this is done by the NotificationSection composable. notificationPanelView.findViewById<View?>(R.id.notification_stack_scroller)?.let { (it.parent as ViewGroup).removeView(it) sharedNotificationContainer.addNotificationStackScrollLayout(it) diff --git a/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt b/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt index a3029b284934..23ee00d88fdc 100644 --- a/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt +++ b/packages/SystemUI/src/com/android/systemui/media/controls/resume/MediaResumeListener.kt @@ -146,7 +146,7 @@ constructor( null, UserHandle.ALL ) - userTracker.addCallback(userTrackerCallback, backgroundExecutor) + userTracker.addCallback(userTrackerCallback, mainExecutor) loadSavedComponents() } } diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionCaptureTarget.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionCaptureTarget.kt index 11d0be5fc8bf..a618490c1b53 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionCaptureTarget.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/MediaProjectionCaptureTarget.kt @@ -16,7 +16,7 @@ package com.android.systemui.mediaprojection -import android.os.IBinder +import android.app.ActivityOptions.LaunchCookie import android.os.Parcel import android.os.Parcelable @@ -24,12 +24,12 @@ import android.os.Parcelable * Class that represents an area that should be captured. Currently it has only a launch cookie that * represents a task but we potentially could add more identifiers e.g. for a pair of tasks. */ -data class MediaProjectionCaptureTarget(val launchCookie: IBinder?) : Parcelable { +data class MediaProjectionCaptureTarget(val launchCookie: LaunchCookie?) : Parcelable { - constructor(parcel: Parcel) : this(parcel.readStrongBinder()) + constructor(parcel: Parcel) : this(LaunchCookie.readFromParcel(parcel)) override fun writeToParcel(dest: Parcel, flags: Int) { - dest.writeStrongBinder(launchCookie) + LaunchCookie.writeToParcel(launchCookie, dest) } override fun describeContents(): Int = 0 diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt index 50e9e7517a0f..4685c5a0cb21 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorActivity.kt @@ -16,6 +16,7 @@ package com.android.systemui.mediaprojection.appselector import android.app.ActivityOptions +import android.app.ActivityOptions.LaunchCookie import android.content.Intent import android.content.res.Configuration import android.content.res.Resources @@ -24,9 +25,7 @@ import android.media.projection.IMediaProjectionManager.EXTRA_USER_REVIEW_GRANTE import android.media.projection.MediaProjectionManager.EXTRA_MEDIA_PROJECTION import android.media.projection.ReviewGrantedConsentResult.RECORD_CANCEL import android.media.projection.ReviewGrantedConsentResult.RECORD_CONTENT_TASK -import android.os.Binder import android.os.Bundle -import android.os.IBinder import android.os.ResultReceiver import android.os.UserHandle import android.util.Log @@ -163,9 +162,9 @@ class MediaProjectionAppSelectorActivity( val intent = createIntent(targetInfo) - val launchToken: IBinder = Binder("media_projection_launch_token") + val launchCookie = LaunchCookie("media_projection_launch_token") val activityOptions = ActivityOptions.makeBasic() - activityOptions.launchCookie = launchToken + activityOptions.setLaunchCookie(launchCookie) val userHandle = mMultiProfilePagerAdapter.activeListAdapter.userHandle @@ -175,7 +174,7 @@ class MediaProjectionAppSelectorActivity( // is created and ready to be captured. val activityStarted = activityLauncher.startActivityAsUser(intent, userHandle, activityOptions.toBundle()) { - returnSelectedApp(launchToken) + returnSelectedApp(launchCookie) } // Rely on the ActivityManager to pop up a dialog regarding app suspension @@ -233,7 +232,7 @@ class MediaProjectionAppSelectorActivity( } } - override fun returnSelectedApp(launchCookie: IBinder) { + override fun returnSelectedApp(launchCookie: LaunchCookie) { taskSelected = true if (intent.hasExtra(EXTRA_CAPTURE_REGION_RESULT_RECEIVER)) { // The client requested to return the result in the result receiver instead of @@ -255,7 +254,7 @@ class MediaProjectionAppSelectorActivity( val mediaProjectionBinder = intent.getIBinderExtra(EXTRA_MEDIA_PROJECTION) val projection = IMediaProjection.Stub.asInterface(mediaProjectionBinder) - projection.launchCookie = launchCookie + projection.setLaunchCookie(launchCookie) val intent = Intent() intent.putExtra(EXTRA_MEDIA_PROJECTION, projection.asBinder()) diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorResultHandler.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorResultHandler.kt index 93c3bce87ad3..f204b3e74f4b 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorResultHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/MediaProjectionAppSelectorResultHandler.kt @@ -1,6 +1,6 @@ package com.android.systemui.mediaprojection.appselector -import android.os.IBinder +import android.app.ActivityOptions.LaunchCookie /** * Interface that allows to continue the media projection flow and return the selected app @@ -11,5 +11,5 @@ interface MediaProjectionAppSelectorResultHandler { * Return selected app to the original caller of the media projection app picker. * @param launchCookie launch cookie of the launched activity of the target app */ - fun returnSelectedApp(launchCookie: IBinder) + fun returnSelectedApp(launchCookie: LaunchCookie) } diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt index ba837dba5354..a811065fdc65 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt @@ -17,10 +17,10 @@ package com.android.systemui.mediaprojection.appselector.view import android.app.ActivityOptions +import android.app.ActivityOptions.LaunchCookie import android.app.ComponentOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED import android.app.IActivityTaskManager import android.graphics.Rect -import android.os.Binder import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -121,7 +121,7 @@ constructor( } override fun onRecentAppClicked(task: RecentTask, view: View) { - val launchCookie = Binder() + val launchCookie = LaunchCookie() val activityOptions = ActivityOptions.makeScaleUpAnimation( view, @@ -132,7 +132,7 @@ constructor( ) activityOptions.pendingIntentBackgroundActivityStartMode = MODE_BACKGROUND_ACTIVITY_START_ALLOWED - activityOptions.launchCookie = launchCookie + activityOptions.setLaunchCookie(launchCookie) activityOptions.launchDisplayId = task.displayId activityTaskManager.startActivityFromRecents(task.taskId, activityOptions.toBundle()) diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java index 039372d87835..8b034b293dcb 100644 --- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java +++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/MediaProjectionPermissionActivity.java @@ -28,6 +28,7 @@ import static com.android.systemui.mediaprojection.permission.ScreenShareOptionK import android.annotation.Nullable; import android.app.Activity; import android.app.ActivityManager; +import android.app.ActivityOptions.LaunchCookie; import android.app.AlertDialog; import android.app.StatusBarManager; import android.content.Context; @@ -146,6 +147,13 @@ public class MediaProjectionPermissionActivity extends Activity final IMediaProjection projection = MediaProjectionServiceHelper.createOrReuseProjection(mUid, mPackageName, mReviewGrantedConsentRequired); + + LaunchCookie launchCookie = launchingIntent.getParcelableExtra( + MediaProjectionManager.EXTRA_LAUNCH_COOKIE, LaunchCookie.class); + if (launchCookie != null) { + projection.setLaunchCookie(launchCookie); + } + // Automatically grant consent if a system-privileged component is recording. final Intent intent = new Intent(); intent.putExtra(MediaProjectionManager.EXTRA_MEDIA_PROJECTION, diff --git a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java index 21de185ee838..958ace358816 100644 --- a/packages/SystemUI/src/com/android/systemui/power/PowerUI.java +++ b/packages/SystemUI/src/com/android/systemui/power/PowerUI.java @@ -26,8 +26,6 @@ import android.content.res.Configuration; import android.database.ContentObserver; import android.os.BatteryManager; import android.os.Handler; -import android.os.HandlerExecutor; -import android.os.HandlerThread; import android.os.IThermalEventListener; import android.os.IThermalService; import android.os.PowerManager; @@ -97,7 +95,6 @@ public class PowerUI implements private Future mLastShowWarningTask; private boolean mEnableSkinTemperatureWarning; private boolean mEnableUsbTemperatureAlarm; - private final HandlerThread mHandlerThread; private int mLowBatteryAlertCloseLevel; private final int[] mLowBatteryReminderLevels = new int[2]; @@ -170,8 +167,6 @@ public class PowerUI implements mPowerManager = powerManager; mWakefulnessLifecycle = wakefulnessLifecycle; mUserTracker = userTracker; - mHandlerThread = new HandlerThread("PowerUI"); - mHandlerThread.start(); } public void start() { @@ -190,8 +185,7 @@ public class PowerUI implements false, obs, UserHandle.USER_ALL); updateBatteryWarningLevels(); mReceiver.init(); - mUserTracker.addCallback(mUserChangedCallback, - new HandlerExecutor(mHandlerThread.getThreadHandler())); + mUserTracker.addCallback(mUserChangedCallback, mContext.getMainExecutor()); mWakefulnessLifecycle.addObserver(mWakefulnessObserver); // Check to see if we need to let the user know that the phone previously shut down due diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java index 4e89fbfeb6e3..7d86a6a1794a 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java @@ -513,10 +513,6 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis notifySystemUiStateFlags(mSysUiState.getFlags()); notifyConnectionChanged(); - if (mDoneUserChanging != null) { - mDoneUserChanging.run(); - mDoneUserChanging = null; - } } @Override @@ -571,14 +567,11 @@ public class OverviewProxyService implements CallbackController<OverviewProxyLis } }; - private Runnable mDoneUserChanging; private final UserTracker.Callback mUserChangedCallback = new UserTracker.Callback() { @Override - public void onUserChanging(int newUser, @NonNull Context userContext, - @NonNull Runnable resultCallback) { + public void onUserChanged(int newUser, @NonNull Context userContext) { mConnectionBackoffAttempts = 0; - mDoneUserChanging = resultCallback; internalConnectToCurrentUser("User changed"); } }; diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt index b3d2e0918db6..b9e9fe7684e9 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneInteractor.kt @@ -171,28 +171,6 @@ constructor( return repository.setVisible(isVisible) } - /** True if there is a transition happening from and to the specified scenes. */ - fun transitioning(from: SceneKey, to: SceneKey): StateFlow<Boolean> { - fun transitioning( - state: ObservableTransitionState, - from: SceneKey, - to: SceneKey, - ): Boolean { - return (state as? ObservableTransitionState.Transition)?.let { - it.fromScene == from && it.toScene == to - } - ?: false - } - - return transitionState - .map { state -> transitioning(state, from, to) } - .stateIn( - scope = applicationScope, - started = SharingStarted.WhileSubscribed(), - initialValue = transitioning(transitionState.value, from, to), - ) - } - /** * Binds the given flow so the system remembers it. * diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt index 93cfc5dbcbe3..2b978b2375d9 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt @@ -97,6 +97,10 @@ object SceneWindowRootViewBinder { val legacyView = view.requireViewById<View>(R.id.legacy_window_root) view.addView(createVisibilityToggleView(legacyView)) + // This moves the SharedNotificationContainer to the WindowRootView just after + // the SceneContainerView. This SharedNotificationContainer should contain NSSL + // due to the NotificationStackScrollLayoutSection (legacy) or + // NotificationSection (scene container) moving it there. if (flags.flexiNotifsEnabled()) { (sharedNotificationContainer.parent as? ViewGroup)?.removeView( sharedNotificationContainer diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java index 19a58401cbec..ef820f3dd9ae 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowViewController.java @@ -18,6 +18,7 @@ package com.android.systemui.shade; import static com.android.systemui.flags.Flags.LOCKSCREEN_WALLPAPER_DREAM_ENABLED; import static com.android.systemui.flags.Flags.TRACKPAD_GESTURE_COMMON; +import static com.android.systemui.statusbar.StatusBarState.KEYGUARD; import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; import android.app.StatusBarManager; @@ -477,8 +478,9 @@ public class NotificationShadeWindowViewController implements Dumpable { if (KeyguardShadeMigrationNssl.isEnabled()) { // When on lockscreen, if the touch originates at the top of the screen // go directly to QS and not the shade - if (mQuickSettingsController.shouldQuickSettingsIntercept( - ev.getX(), ev.getY(), 0)) { + if (mStatusBarStateController.getState() == KEYGUARD + && mQuickSettingsController.shouldQuickSettingsIntercept( + ev.getX(), ev.getY(), 0)) { mShadeLogger.d("NSWVC: QS intercepted"); return true; } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt index 51276c6560a4..314637e4b27e 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt @@ -22,12 +22,11 @@ import android.content.IntentFilter import android.icu.text.DateFormat import android.icu.text.DisplayContext import android.os.UserHandle -import com.android.systemui.res.R import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.res.R import com.android.systemui.scene.domain.interactor.SceneInteractor -import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel import java.util.Date @@ -38,7 +37,6 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach @@ -57,16 +55,6 @@ constructor( val mobileIconsViewModel: MobileIconsViewModel, broadcastDispatcher: BroadcastDispatcher, ) { - /** True if we are transitioning between Shade and QuickSettings scenes, in either direction. */ - val isTransitioning = - combine( - sceneInteractor.transitioning(from = SceneKey.Shade, to = SceneKey.QuickSettings), - sceneInteractor.transitioning(from = SceneKey.QuickSettings, to = SceneKey.Shade) - ) { shadeToQuickSettings, quickSettingsToShade -> - shadeToQuickSettings || quickSettingsToShade - } - .stateIn(applicationScope, SharingStarted.WhileSubscribed(), false) - /** True if there is exactly one mobile connection. */ val isSingleCarrier: StateFlow<Boolean> = mobileIconsInteractor.isSingleCarrier diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java index ada7d3ea1698..ffb11dd3cf92 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/CommandQueue.java @@ -56,6 +56,7 @@ import android.view.KeyEvent; import android.view.WindowInsets.Type.InsetsType; import android.view.WindowInsetsController.Appearance; import android.view.WindowInsetsController.Behavior; +import android.view.accessibility.Flags; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; @@ -305,6 +306,13 @@ public class CommandQueue extends IStatusBar.Stub implements default void setTopAppHidesStatusBar(boolean topAppHidesStatusBar) { } default void addQsTile(ComponentName tile) { } + + /** + * Add a tile to the Quick Settings Panel + * @param tile the ComponentName of the {@link android.service.quicksettings.TileService} + * @param end if true, the tile will be added at the end. If false, at the beginning. + */ + default void addQsTileToFrontOrEnd(ComponentName tile, boolean end) { } default void remQsTile(ComponentName tile) { } default void setQsTiles(String[] tiles) {} @@ -904,8 +912,29 @@ public class CommandQueue extends IStatusBar.Stub implements @Override public void addQsTile(ComponentName tile) { - synchronized (mLock) { - mHandler.obtainMessage(MSG_ADD_QS_TILE, tile).sendToTarget(); + if (Flags.a11yQsShortcut()) { + addQsTileToFrontOrEnd(tile, false); + } else { + synchronized (mLock) { + mHandler.obtainMessage(MSG_ADD_QS_TILE, tile).sendToTarget(); + } + } + } + + /** + * Add a tile to the Quick Settings Panel + * @param tile the ComponentName of the {@link android.service.quicksettings.TileService} + * @param end if true, the tile will be added at the end. If false, at the beginning. + */ + @Override + public void addQsTileToFrontOrEnd(ComponentName tile, boolean end) { + if (Flags.a11yQsShortcut()) { + synchronized (mLock) { + SomeArgs args = SomeArgs.obtain(); + args.arg1 = tile; + args.arg2 = end; + mHandler.obtainMessage(MSG_ADD_QS_TILE, args).sendToTarget(); + } } } @@ -1546,11 +1575,21 @@ public class CommandQueue extends IStatusBar.Stub implements mCallbacks.get(i).showPictureInPictureMenu(); } break; - case MSG_ADD_QS_TILE: - for (int i = 0; i < mCallbacks.size(); i++) { - mCallbacks.get(i).addQsTile((ComponentName) msg.obj); + case MSG_ADD_QS_TILE: { + if (Flags.a11yQsShortcut()) { + SomeArgs someArgs = (SomeArgs) msg.obj; + for (int i = 0; i < mCallbacks.size(); i++) { + mCallbacks.get(i).addQsTileToFrontOrEnd( + (ComponentName) someArgs.arg1, (boolean) someArgs.arg2); + } + someArgs.recycle(); + } else { + for (int i = 0; i < mCallbacks.size(); i++) { + mCallbacks.get(i).addQsTile((ComponentName) msg.obj); + } } break; + } case MSG_REMOVE_QS_TILE: for (int i = 0; i < mCallbacks.size(); i++) { mCallbacks.get(i).remQsTile((ComponentName) msg.obj); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java index d6d3e6791074..04d9b0cbd428 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java @@ -591,8 +591,10 @@ public class KeyguardIndicationController { } private void updateLockScreenUserLockedMsg(int userId) { - if (!mKeyguardUpdateMonitor.isUserUnlocked(userId) - || mKeyguardUpdateMonitor.isEncryptedOrLockdown(userId)) { + boolean userUnlocked = mKeyguardUpdateMonitor.isUserUnlocked(userId); + boolean encryptedOrLockdown = mKeyguardUpdateMonitor.isEncryptedOrLockdown(userId); + mKeyguardLogger.logUpdateLockScreenUserLockedMsg(userId, userUnlocked, encryptedOrLockdown); + if (!userUnlocked || encryptedOrLockdown) { mRotateTextViewController.updateIndication( INDICATION_TYPE_USER_LOCKED, new KeyguardIndication.Builder() diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java index 28d4457b264b..fc84973c46bd 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/NetworkControllerImpl.java @@ -35,6 +35,7 @@ import android.net.wifi.WifiManager; import android.os.AsyncTask; import android.os.Bundle; import android.os.Handler; +import android.os.HandlerExecutor; import android.os.Looper; import android.provider.Settings; import android.telephony.CarrierConfigManager; @@ -60,6 +61,7 @@ import com.android.settingslib.mobile.MobileStatusTracker.SubscriptionDefaults; import com.android.settingslib.mobile.TelephonyIcons; import com.android.settingslib.net.DataUsageController; import com.android.systemui.Dumpable; +import com.android.systemui.res.R; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; @@ -71,7 +73,6 @@ import com.android.systemui.log.LogBuffer; import com.android.systemui.log.core.LogLevel; import com.android.systemui.log.dagger.StatusBarNetworkControllerLog; import com.android.systemui.qs.tiles.dialog.InternetDialogFactory; -import com.android.systemui.res.R; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.pipeline.StatusBarPipelineFlags; import com.android.systemui.statusbar.policy.ConfigurationController; @@ -84,8 +85,6 @@ import com.android.systemui.util.CarrierConfigTracker; import dalvik.annotation.optimization.NeverCompile; -import kotlin.Unit; - import java.io.PrintWriter; import java.text.SimpleDateFormat; import java.util.ArrayList; @@ -100,6 +99,8 @@ import java.util.stream.Collectors; import javax.inject.Inject; +import kotlin.Unit; + /** Platform implementation of the network controller. **/ @SysUISingleton public class NetworkControllerImpl extends BroadcastReceiver @@ -349,7 +350,7 @@ public class NetworkControllerImpl extends BroadcastReceiver // AIRPLANE_MODE_CHANGED is sent at boot; we've probably already missed it updateAirplaneMode(true /* force callback */); mUserTracker = userTracker; - mUserTracker.addCallback(mUserChangedCallback, mBgExecutor); + mUserTracker.addCallback(mUserChangedCallback, new HandlerExecutor(mMainHandler)); deviceProvisionedController.addCallback(new DeviceProvisionedListener() { @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt index 342828c4b5d3..aca8b64c05d2 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/KeyguardNotificationVisibilityProvider.kt @@ -6,7 +6,6 @@ import android.database.ContentObserver import android.net.Uri import android.os.Handler import android.os.HandlerExecutor -import android.os.HandlerThread import android.os.UserHandle import android.provider.Settings import com.android.keyguard.KeyguardUpdateMonitor @@ -88,7 +87,6 @@ class KeyguardNotificationVisibilityProviderImpl @Inject constructor( secureSettings.getUriFor(Settings.Secure.LOCK_SCREEN_SHOW_SILENT_NOTIFICATIONS) private val onStateChangedListeners = ListenerSet<Consumer<String>>() private var hideSilentNotificationsOnLockscreen: Boolean = false - private val handlerThread: HandlerThread = HandlerThread("KeyguardNotificationVis") private val userTrackerCallback = object : UserTracker.Callback { override fun onUserChanged(newUser: Int, userContext: Context) { @@ -156,9 +154,7 @@ class KeyguardNotificationVisibilityProviderImpl @Inject constructor( notifyStateChanged("onStatusBarUpcomingStateChanged") } }) - handlerThread.start() - userTracker.addCallback(userTrackerCallback, - HandlerExecutor(handlerThread.getThreadHandler())) + userTracker.addCallback(userTrackerCallback, HandlerExecutor(handler)) } override fun addOnStateChangedListener(listener: Consumer<String>) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index dd04531b6b34..b9afb1409d91 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -617,7 +617,11 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable private final ScrollAdapter mScrollAdapter = new ScrollAdapter() { @Override public boolean isScrolledToTop() { - return mOwnScrollY == 0; + if (SceneContainerFlag.isEnabled()) { + return mController.isPlaceholderScrolledToTop(); + } else { + return mOwnScrollY == 0; + } } @Override @@ -1442,7 +1446,14 @@ public class NotificationStackScrollLayout extends ViewGroup implements Dumpable fraction = BouncerPanelExpansionCalculator.aboutToShowBouncerProgress(fraction); } final float stackY = MathUtils.lerp(0, endTopPosition, fraction); - mAmbientState.setStackY(stackY); + // TODO(b/322228881): Clean up scene container vs legacy behavior in NSSL + if (SceneContainerFlag.isEnabled()) { + // stackY should be driven by scene container, not NSSL + mAmbientState.setStackY(mTopPadding); + } else { + mAmbientState.setStackY(stackY); + } + if (mOnStackYChanged != null) { mOnStackYChanged.accept(listenerNeedsAnimation); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index 49fde3984acc..ed266772ac15 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -1144,6 +1144,14 @@ public class NotificationStackScrollLayoutController implements Dumpable { return mStackAppearanceInteractor.getStackBounds().getValue().getTop(); } + /** + * Returns whether the notification stack is scrolled to the top; i.e., it cannot be scrolled + * down any further. + */ + public boolean isPlaceholderScrolledToTop() { + return mStackAppearanceInteractor.getScrolledToTop().getValue(); + } + /** Set the intrinsic height of the stack content without additional padding. */ public void setIntrinsicContentHeight(float intrinsicContentHeight) { mStackAppearanceInteractor.setIntrinsicContentHeight(intrinsicContentHeight); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt index aac3c28a3426..01972646f394 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt @@ -44,4 +44,10 @@ class NotificationStackAppearanceRepository @Inject constructor() { * screen. */ val contentTop = MutableStateFlow(0f) + + /** + * Whether the notification stack is scrolled to the top; i.e., it cannot be scrolled down any + * further. + */ + val scrolledToTop = MutableStateFlow(true) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt index 1dfde09f3a85..8307397c57da 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt @@ -42,10 +42,16 @@ constructor( * notifications, this can exceed the space available on screen to show notifications, at which * point the notification stack should become scrollable. */ - val intrinsicContentHeight = repository.intrinsicContentHeight.asStateFlow() + val intrinsicContentHeight: StateFlow<Float> = repository.intrinsicContentHeight.asStateFlow() /** The y-coordinate in px of top of the contents of the notification stack. */ - val contentTop = repository.contentTop.asStateFlow() + val contentTop: StateFlow<Float> = repository.contentTop.asStateFlow() + + /** + * Whether the notification stack is scrolled to the top; i.e., it cannot be scrolled down any + * further. + */ + val scrolledToTop: StateFlow<Boolean> = repository.scrolledToTop.asStateFlow() /** Sets the position of the notification stack in the current scene. */ fun setStackBounds(bounds: NotificationContainerBounds) { @@ -62,4 +68,9 @@ constructor( fun setContentTop(startY: Float) { repository.contentTop.value = startY } + + /** Sets whether the notification stack is scrolled to the top. */ + fun setScrolledToTop(scrolledToTop: Boolean) { + repository.scrolledToTop.value = scrolledToTop + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt index ed15f557fb39..6c2cbbecb477 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt @@ -25,7 +25,6 @@ import com.android.systemui.statusbar.notification.stack.AmbientState import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel -import kotlin.math.pow import kotlin.math.roundToInt import kotlinx.coroutines.launch @@ -65,7 +64,9 @@ object NotificationStackAppearanceViewBinder { viewModel.expandFraction.collect { expandFraction -> ambientState.expansionFraction = expandFraction controller.expandedHeight = expandFraction * controller.view.height - controller.setMaxAlphaForExpansion(expandFraction.pow(0.75f)) + controller.setMaxAlphaForExpansion( + ((expandFraction - 0.5f) / 0.5f).coerceAtLeast(0f) + ) } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt index 74db5831f7f8..56ff7f9e50df 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt @@ -19,11 +19,15 @@ package com.android.systemui.statusbar.notification.stack.ui.viewmodel import com.android.systemui.common.shared.model.NotificationContainerBounds import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.shared.model.ObservableTransitionState +import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor import javax.inject.Inject import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine /** ViewModel which represents the state of the NSSL/Controller in the world of flexiglass */ @SysUISingleton @@ -32,9 +36,40 @@ class NotificationStackAppearanceViewModel constructor( stackAppearanceInteractor: NotificationStackAppearanceInteractor, shadeInteractor: ShadeInteractor, + sceneInteractor: SceneInteractor, ) { - /** The expansion fraction from the top of the notification shade. */ - val expandFraction: Flow<Float> = shadeInteractor.shadeExpansion + /** + * The expansion fraction of the notification stack. It should go from 0 to 1 when transitioning + * from Gone to Shade scenes, and remain at 1 when in Lockscreen or Shade scenes and while + * transitioning from Shade to QuickSettings scenes. + */ + val expandFraction: Flow<Float> = + combine( + shadeInteractor.shadeExpansion, + sceneInteractor.transitionState, + ) { shadeExpansion, transitionState -> + when (transitionState) { + is ObservableTransitionState.Idle -> { + if (transitionState.scene == SceneKey.Lockscreen) { + 1f + } else { + shadeExpansion + } + } + is ObservableTransitionState.Transition -> { + if ( + (transitionState.fromScene == SceneKey.Shade && + transitionState.toScene == SceneKey.QuickSettings) || + (transitionState.fromScene == SceneKey.QuickSettings && + transitionState.toScene == SceneKey.Shade) + ) { + 1f + } else { + shadeExpansion + } + } + } + } /** The bounds of the notification stack in the current scene. */ val stackBounds: Flow<NotificationContainerBounds> = stackAppearanceInteractor.stackBounds diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt index 385f0619288d..a436f1783a0c 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt @@ -87,4 +87,9 @@ constructor( fun onContentTopChanged(padding: Float) { interactor.setContentTop(padding) } + + /** Sets whether the notification stack is scrolled to the top. */ + fun setScrolledToTop(scrolledToTop: Boolean) { + interactor.setScrolledToTop(scrolledToTop) + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt index 4617ce49f44a..3915c3763a41 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModel.kt @@ -24,13 +24,24 @@ import com.android.systemui.communal.domain.interactor.CommunalInteractor import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor +import com.android.systemui.keyguard.shared.model.Edge import com.android.systemui.keyguard.shared.model.KeyguardState +import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCER +import com.android.systemui.keyguard.shared.model.KeyguardState.AOD +import com.android.systemui.keyguard.shared.model.KeyguardState.DOZING +import com.android.systemui.keyguard.shared.model.KeyguardState.DREAMING +import com.android.systemui.keyguard.shared.model.KeyguardState.GLANCEABLE_HUB +import com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN +import com.android.systemui.keyguard.shared.model.KeyguardState.OCCLUDED +import com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER import com.android.systemui.keyguard.shared.model.StatusBarState.SHADE_LOCKED import com.android.systemui.keyguard.shared.model.TransitionState.RUNNING import com.android.systemui.keyguard.shared.model.TransitionState.STARTED import com.android.systemui.keyguard.ui.viewmodel.AodBurnInViewModel import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters +import com.android.systemui.keyguard.ui.viewmodel.DreamingToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.GlanceableHubToLockscreenTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.LockscreenToDreamingTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.LockscreenToGlanceableHubTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.LockscreenToOccludedTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.OccludedToLockscreenTransitionViewModel @@ -51,6 +62,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.onStart @@ -69,43 +81,52 @@ constructor( communalInteractor: CommunalInteractor, private val occludedToLockscreenTransitionViewModel: OccludedToLockscreenTransitionViewModel, lockscreenToOccludedTransitionViewModel: LockscreenToOccludedTransitionViewModel, + dreamingToLockscreenTransitionViewModel: DreamingToLockscreenTransitionViewModel, + lockscreenToDreamingTransitionViewModel: LockscreenToDreamingTransitionViewModel, glanceableHubToLockscreenTransitionViewModel: GlanceableHubToLockscreenTransitionViewModel, lockscreenToGlanceableHubTransitionViewModel: LockscreenToGlanceableHubTransitionViewModel, private val aodBurnInViewModel: AodBurnInViewModel, ) { - private val statesForConstrainedNotifications = - setOf( - KeyguardState.AOD, - KeyguardState.LOCKSCREEN, - KeyguardState.DOZING, - KeyguardState.ALTERNATE_BOUNCER, - KeyguardState.PRIMARY_BOUNCER + private val statesForConstrainedNotifications: Set<KeyguardState> = + setOf(AOD, LOCKSCREEN, DOZING, ALTERNATE_BOUNCER, PRIMARY_BOUNCER) + + private val edgeToAlphaViewModel = + mapOf<Edge?, Flow<Float>>( + Edge(from = LOCKSCREEN, to = DREAMING) to + lockscreenToDreamingTransitionViewModel.lockscreenAlpha, + Edge(from = DREAMING, to = LOCKSCREEN) to + dreamingToLockscreenTransitionViewModel.lockscreenAlpha, + Edge(from = LOCKSCREEN, to = OCCLUDED) to + lockscreenToOccludedTransitionViewModel.lockscreenAlpha, + Edge(from = OCCLUDED, to = LOCKSCREEN) to + occludedToLockscreenTransitionViewModel.lockscreenAlpha, ) - private val lockscreenToOccludedRunning = - keyguardTransitionInteractor - .transition(KeyguardState.LOCKSCREEN, KeyguardState.OCCLUDED) - .map { it.transitionState == STARTED || it.transitionState == RUNNING } + private val lockscreenTransitionInProgress: Flow<Edge?> = + keyguardTransitionInteractor.transitions + .map { step -> + if ( + (step.transitionState == STARTED || step.transitionState == RUNNING) && + (step.from == LOCKSCREEN || step.to == LOCKSCREEN) + ) { + Edge(step.from, step.to) + } else { + null + } + } .distinctUntilChanged() - .onStart { emit(false) } - - private val occludedToLockscreenRunning = - keyguardTransitionInteractor - .transition(KeyguardState.OCCLUDED, KeyguardState.LOCKSCREEN) - .map { it.transitionState == STARTED || it.transitionState == RUNNING } - .distinctUntilChanged() - .onStart { emit(false) } + .onStart { emit(null) } private val lockscreenToGlanceableHubRunning = keyguardTransitionInteractor - .transition(KeyguardState.LOCKSCREEN, KeyguardState.GLANCEABLE_HUB) + .transition(LOCKSCREEN, GLANCEABLE_HUB) .map { it.transitionState == STARTED || it.transitionState == RUNNING } .distinctUntilChanged() .onStart { emit(false) } private val glanceableHubToLockscreenRunning = keyguardTransitionInteractor - .transition(KeyguardState.GLANCEABLE_HUB, KeyguardState.LOCKSCREEN) + .transition(GLANCEABLE_HUB, LOCKSCREEN) .map { it.transitionState == STARTED || it.transitionState == RUNNING } .distinctUntilChanged() .onStart { emit(false) } @@ -141,7 +162,7 @@ constructor( statesForConstrainedNotifications.contains(it) }, keyguardTransitionInteractor - .transitionValue(KeyguardState.LOCKSCREEN) + .transitionValue(LOCKSCREEN) .onStart { emit(0f) } .map { it > 0 } ) { constrainedNotificationState, transitioningToOrFromLockscreen -> @@ -242,38 +263,46 @@ constructor( initialValue = NotificationContainerBounds(), ) + /** As QS is expanding, fade out notifications unless in splitshade */ + private val alphaForQsExpansion: Flow<Float> = + interactor.configurationBasedDimensions.flatMapLatest { + if (it.useSplitShade) { + flowOf(1f) + } else { + shadeInteractor.qsExpansion.map { 1f - it } + } + } + val expansionAlpha: Flow<Float> = // Due to issues with the legacy shade, some shade expansion events are sent incorrectly, - // such as when the shade resets. This can happen while the LOCKSCREEN<->OCCLUDED transition + // such as when the shade resets. This can happen while the transition to/from LOCKSCREEN // is running. Therefore use a series of flatmaps to prevent unwanted interruptions while // those transitions are in progress. Without this, the alpha value will produce a visible // flicker. - lockscreenToOccludedRunning.flatMapLatest { isLockscreenToOccludedRunning -> - if (isLockscreenToOccludedRunning) { - lockscreenToOccludedTransitionViewModel.lockscreenAlpha - } else { - occludedToLockscreenRunning.flatMapLatest { isOccludedToLockscreenRunning -> - if (isOccludedToLockscreenRunning) { - occludedToLockscreenTransitionViewModel.lockscreenAlpha.onStart { emit(0f) } - } else { + lockscreenTransitionInProgress + .flatMapLatest { edge -> + edgeToAlphaViewModel.getOrElse( + edge, + { isOnLockscreenWithoutShade.flatMapLatest { isOnLockscreenWithoutShade -> combineTransform( keyguardInteractor.keyguardAlpha, shadeCollpaseFadeIn, - ) { alpha, shadeCollpaseFadeIn -> + alphaForQsExpansion, + ) { alpha, shadeCollpaseFadeIn, alphaForQsExpansion -> if (isOnLockscreenWithoutShade) { if (!shadeCollpaseFadeIn) { emit(alpha) } } else { - emit(1f) + emit(alphaForQsExpansion) } } } } - } + ) } - } + .distinctUntilChanged() /** * Returns a flow of the expected alpha while running a LOCKSCREEN<->GLANCEABLE_HUB transition diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java index 60a4606ef0d0..39ca7b219663 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java @@ -179,6 +179,11 @@ public class CentralSurfacesCommandQueueCallbacks implements CommandQueue.Callba } @Override + public void addQsTileToFrontOrEnd(ComponentName tile, boolean end) { + mQSHost.addTile(tile, end); + } + + @Override public void remQsTile(ComponentName tile) { mQSHost.removeTileByUser(tile); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java index ade417d7bf5c..64fcef51755d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java @@ -125,6 +125,7 @@ import com.android.systemui.charging.WiredChargingRippleController; import com.android.systemui.charging.WirelessChargingAnimation; import com.android.systemui.classifier.FalsingCollector; import com.android.systemui.colorextraction.SysuiColorExtractor; +import com.android.systemui.communal.domain.interactor.CommunalInteractor; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dagger.qualifiers.UiBackground; @@ -245,6 +246,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.Executor; +import java.util.function.Consumer; import javax.inject.Inject; import javax.inject.Named; @@ -551,6 +553,25 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { private final WakefulnessLifecycle mWakefulnessLifecycle; protected final PowerInteractor mPowerInteractor; + private final CommunalInteractor mCommunalInteractor; + + /** + * True if the device is showing the glanceable hub. See + * {@link CommunalInteractor#isIdleOnCommunal()} for more details. + */ + private boolean mIsIdleOnCommunal = false; + private final Consumer<Boolean> mIdleOnCommunalConsumer = (Boolean idleOnCommunal) -> { + if (idleOnCommunal == mIsIdleOnCommunal) { + // Ignore initial value coming through the flow. + return; + } + + mIsIdleOnCommunal = idleOnCommunal; + // Trigger an update for the scrim state when we enter or exit glanceable hub, so that we + // can transition to/from ScrimState.GLANCEABLE_HUB if needed. + updateScrimController(); + }; + private boolean mNoAnimationOnNextBarModeChange; private final SysuiStatusBarStateController mStatusBarStateController; @@ -618,6 +639,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { ScreenLifecycle screenLifecycle, WakefulnessLifecycle wakefulnessLifecycle, PowerInteractor powerInteractor, + CommunalInteractor communalInteractor, SysuiStatusBarStateController statusBarStateController, Optional<Bubbles> bubblesOptional, Lazy<NoteTaskController> noteTaskControllerLazy, @@ -722,6 +744,7 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { mScreenLifecycle = screenLifecycle; mWakefulnessLifecycle = wakefulnessLifecycle; mPowerInteractor = powerInteractor; + mCommunalInteractor = communalInteractor; mStatusBarStateController = statusBarStateController; mBubblesOptional = bubblesOptional; mNoteTaskControllerLazy = noteTaskControllerLazy; @@ -1051,6 +1074,10 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { //TODO(b/264502026) move the rest of the listeners here. mDeviceStateManager.registerCallback(mMainExecutor, new FoldStateListener(mContext, this::onFoldedStateChanged)); + + mJavaAdapter.alwaysCollectFlow( + mCommunalInteractor.isIdleOnCommunal(), + mIdleOnCommunalConsumer); } /** @@ -2795,6 +2822,8 @@ public class CentralSurfacesImpl implements CoreStartable, CentralSurfaces { // This will cancel the keyguardFadingAway animation if it is running. We need to do // this as otherwise it can remain pending and leave keyguard in a weird state. mUnlockScrimCallback.onCancelled(); + } else if (mIsIdleOnCommunal) { + mScrimController.transitionTo(ScrimState.GLANCEABLE_HUB); } else if (mKeyguardStateController.isShowing() && !mKeyguardStateController.isOccluded() && !unlocking) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt deleted file mode 100644 index 9f0863385ca1..000000000000 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/KeyguardLiftController.kt +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Copyright (C) 2019 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.phone - -import android.content.Context -import android.content.pm.PackageManager -import android.hardware.Sensor -import android.hardware.TriggerEvent -import android.hardware.TriggerEventListener -import com.android.keyguard.ActiveUnlockConfig -import com.android.keyguard.KeyguardUpdateMonitor -import com.android.keyguard.KeyguardUpdateMonitorCallback -import com.android.systemui.CoreStartable -import com.android.systemui.Dumpable -import com.android.systemui.dagger.SysUISingleton -import com.android.systemui.dump.DumpManager -import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor -import com.android.systemui.plugins.statusbar.StatusBarStateController -import com.android.systemui.user.domain.interactor.SelectedUserInteractor -import com.android.systemui.util.Assert -import com.android.systemui.util.sensors.AsyncSensorManager -import java.io.PrintWriter -import javax.inject.Inject - -/** - * Triggers face auth on lift when the device is showing the lock screen. Only initialized - * if face auth is supported on the device. Not to be confused with the lift to wake gesture - * which is handled by {@link com.android.server.policy.PhoneWindowManager}. - */ -@SysUISingleton -class KeyguardLiftController @Inject constructor( - private val context: Context, - private val statusBarStateController: StatusBarStateController, - private val asyncSensorManager: AsyncSensorManager, - private val keyguardUpdateMonitor: KeyguardUpdateMonitor, - private val deviceEntryFaceAuthInteractor: DeviceEntryFaceAuthInteractor, - private val dumpManager: DumpManager, - private val selectedUserInteractor: SelectedUserInteractor, -) : Dumpable, CoreStartable { - - private val pickupSensor = asyncSensorManager.getDefaultSensor(Sensor.TYPE_PICK_UP_GESTURE) - private var isListening = false - private var bouncerVisible = false - - override fun start() { - if (context.packageManager.hasSystemFeature(PackageManager.FEATURE_FACE)) { - init() - } - } - - private fun init() { - dumpManager.registerDumpable(this) - statusBarStateController.addCallback(statusBarStateListener) - keyguardUpdateMonitor.registerCallback(keyguardUpdateMonitorCallback) - updateListeningState() - } - - private val listener: TriggerEventListener = object : TriggerEventListener() { - override fun onTrigger(event: TriggerEvent?) { - Assert.isMainThread() - // Not listening anymore since trigger events unregister themselves - isListening = false - updateListeningState() - deviceEntryFaceAuthInteractor.onDeviceLifted() - keyguardUpdateMonitor.requestActiveUnlock( - ActiveUnlockConfig.ActiveUnlockRequestOrigin.WAKE, - "KeyguardLiftController") - } - } - - private val keyguardUpdateMonitorCallback = object : KeyguardUpdateMonitorCallback() { - override fun onKeyguardBouncerFullyShowingChanged(bouncer: Boolean) { - bouncerVisible = bouncer - updateListeningState() - } - - override fun onKeyguardVisibilityChanged(visible: Boolean) { - updateListeningState() - } - } - - private val statusBarStateListener = object : StatusBarStateController.StateListener { - override fun onDozingChanged(isDozing: Boolean) { - updateListeningState() - } - } - - override fun dump(pw: PrintWriter, args: Array<out String>) { - pw.println("KeyguardLiftController:") - pw.println(" pickupSensor: $pickupSensor") - pw.println(" isListening: $isListening") - pw.println(" bouncerVisible: $bouncerVisible") - } - - private fun updateListeningState() { - if (pickupSensor == null) { - return - } - val onKeyguard = keyguardUpdateMonitor.isKeyguardVisible && - !statusBarStateController.isDozing - - val isFaceEnabled = deviceEntryFaceAuthInteractor.isFaceAuthEnabledAndEnrolled() - val shouldListen = (onKeyguard || bouncerVisible) && isFaceEnabled - if (shouldListen != isListening) { - isListening = shouldListen - - if (shouldListen) { - asyncSensorManager.requestTriggerSensor(listener, pickupSensor) - } else { - asyncSensorManager.cancelTriggerSensor(listener, pickupSensor) - } - } - } -} diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java index 3f20eaf45260..6f78604e996f 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimController.java @@ -17,7 +17,9 @@ package com.android.systemui.statusbar.phone; import static com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCER; +import static com.android.systemui.keyguard.shared.model.KeyguardState.GLANCEABLE_HUB; import static com.android.systemui.keyguard.shared.model.KeyguardState.GONE; +import static com.android.systemui.keyguard.shared.model.KeyguardState.LOCKSCREEN; import static com.android.systemui.keyguard.shared.model.KeyguardState.PRIMARY_BOUNCER; import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; @@ -62,6 +64,7 @@ import com.android.systemui.dock.DockManager; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; +import com.android.systemui.keyguard.shared.model.KeyguardState; import com.android.systemui.keyguard.shared.model.ScrimAlpha; import com.android.systemui.keyguard.shared.model.TransitionState; import com.android.systemui.keyguard.shared.model.TransitionStep; @@ -292,6 +295,30 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump mScrimBehind.setViewAlpha(mBehindAlpha); }; + /** + * Consumer that fades the behind scrim in and out during the transition between the lock screen + * and the glanceable hub. + * + * While the lock screen is showing, the behind scrim is used to slightly darken the lock screen + * wallpaper underneath. Since the glanceable hub is under all of the scrims, we want to fade + * out the scrim so that the glanceable hub isn't darkened when it opens. + * + * {@link #applyState()} handles the scrim alphas once on the glanceable hub, this is only + * responsible for setting the behind alpha during the transition. + */ + private final Consumer<TransitionStep> mGlanceableHubConsumer = (TransitionStep step) -> { + final float baseAlpha = ScrimState.KEYGUARD.getBehindAlpha(); + final float transitionProgress = step.getValue(); + if (step.getTo() == KeyguardState.LOCKSCREEN) { + // Transitioning back to lock screen, fade in behind scrim again. + mBehindAlpha = baseAlpha * transitionProgress; + } else if (step.getTo() == GLANCEABLE_HUB) { + // Transitioning to glanceable hub, fade out behind scrim. + mBehindAlpha = baseAlpha * (1 - transitionProgress); + } + mScrimBehind.setViewAlpha(mBehindAlpha); + }; + Consumer<TransitionStep> mBouncerToGoneTransition; @Inject @@ -444,6 +471,14 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump mBouncerToGoneTransition, mMainDispatcher); collectFlow(behindScrim, mAlternateBouncerToGoneTransitionViewModel.getScrimAlpha(), mScrimAlphaConsumer, mMainDispatcher); + + // LOCKSCREEN<->GLANCEABLE_HUB + collectFlow(behindScrim, + mKeyguardTransitionInteractor.transition(LOCKSCREEN, GLANCEABLE_HUB), + mGlanceableHubConsumer, mMainDispatcher); + collectFlow(behindScrim, + mKeyguardTransitionInteractor.transition(GLANCEABLE_HUB, LOCKSCREEN), + mGlanceableHubConsumer, mMainDispatcher); } // TODO(b/270984686) recompute scrim height accurately, based on shade contents. @@ -815,9 +850,9 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump return; } mBouncerHiddenFraction = bouncerHiddenAmount; - if (mState == ScrimState.DREAMING) { - // Only the dreaming state requires this for the scrim calculation, so we should - // only trigger an update if dreaming. + if (mState == ScrimState.DREAMING || mState == ScrimState.GLANCEABLE_HUB) { + // The dreaming and glanceable hub states requires this for the scrim calculation, so we + // should only trigger an update in those states. applyAndDispatchState(); } } @@ -939,7 +974,7 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump } else if (mState == ScrimState.AUTH_SCRIMMED_SHADE) { mNotificationsAlpha = (float) Math.pow(getInterpolatedFraction(), 0.8f); } else if (mState == ScrimState.KEYGUARD || mState == ScrimState.SHADE_LOCKED - || mState == ScrimState.PULSING) { + || mState == ScrimState.PULSING || mState == ScrimState.GLANCEABLE_HUB) { Pair<Integer, Float> result = calculateBackStateForState(mState); int behindTint = result.first; float behindAlpha = result.second; @@ -950,6 +985,11 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump mTransitionToFullShadeProgress); behindTint = ColorUtils.blendARGB(behindTint, shadeResult.first, mTransitionToFullShadeProgress); + } else if (mState == ScrimState.GLANCEABLE_HUB && mTransitionToFullShadeProgress == 0.0f + && mBouncerHiddenFraction == KeyguardBouncerConstants.EXPANSION_HIDDEN) { + // Behind scrim should not be visible when idle on the glanceable hub and neither + // bouncer nor shade are showing. + behindAlpha = 0f; } mInFrontAlpha = mState.getFrontAlpha(); if (mClipsQsScrim) { @@ -965,6 +1005,13 @@ public class ScrimController implements ViewTreeObserver.OnPreDrawListener, Dump } else if (mState == ScrimState.SHADE_LOCKED) { // going from KEYGUARD to SHADE_LOCKED state mNotificationsAlpha = getInterpolatedFraction(); + } else if (mState == ScrimState.GLANCEABLE_HUB + && mTransitionToFullShadeProgress == 0.0f) { + // Notification scrim should not be visible on the glanceable hub unless the + // shade is showing or transitioning in. Otherwise the notification scrim will + // be visible as the bouncer transitions in or after the notification shade + // closes. + mNotificationsAlpha = 0; } else { mNotificationsAlpha = Math.max(1.0f - getInterpolatedFraction(), mQsExpansion); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java index 61bd112121bc..f2a649ba2e32 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ScrimState.java @@ -296,6 +296,21 @@ public enum ScrimState { updateScrimColor(mScrimBehind, 1f /* alpha */, mBackgroundColor); } } + }, + + /** + * Device is locked or on dream and user has swiped from the right edge to enter the glanceable + * hub UI. From this state, the user can swipe from the left edge to go back to the lock screen + * or dream, as well as swipe down for the notifications and up for the bouncer. + */ + GLANCEABLE_HUB { + @Override + public void prepare(ScrimState previousState) { + // No scrims should be visible by default in this state. + mBehindAlpha = 0; + mNotifAlpha = 0; + mFrontAlpha = 0; + } }; boolean mBlankScreen = false; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt index 0051161eff35..1c33d3fd0288 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModel.kt @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.pipeline.satellite.ui.viewmodel import com.android.systemui.common.shared.model.Icon import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.statusbar.pipeline.airplane.data.repository.AirplaneModeRepository import com.android.systemui.statusbar.pipeline.satellite.domain.interactor.DeviceBasedSatelliteInteractor import com.android.systemui.statusbar.pipeline.satellite.ui.model.SatelliteIconModel import javax.inject.Inject @@ -40,6 +41,7 @@ class DeviceBasedSatelliteViewModel constructor( interactor: DeviceBasedSatelliteInteractor, @Application scope: CoroutineScope, + airplaneModeRepository: AirplaneModeRepository, ) { private val shouldShowIcon: StateFlow<Boolean> = interactor.areAllConnectionsOutOfService @@ -47,7 +49,11 @@ constructor( if (!allOos) { flowOf(false) } else { - interactor.isSatelliteAllowed + combine(interactor.isSatelliteAllowed, airplaneModeRepository.isAirplaneMode) { + isSatelliteAllowed, + isAirplaneMode -> + isSatelliteAllowed && !isAirplaneMode + } } } .stateIn(scope, SharingStarted.WhileSubscribed(), false) diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java index a2d8d1579e3d..20d1fff91443 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/Clock.java @@ -28,8 +28,6 @@ import android.icu.lang.UCharacter; import android.icu.text.DateTimePatternGenerator; import android.os.Bundle; import android.os.Handler; -import android.os.HandlerExecutor; -import android.os.HandlerThread; import android.os.Parcelable; import android.os.SystemClock; import android.os.UserHandle; @@ -50,11 +48,11 @@ import android.widget.TextView; import com.android.settingslib.Utils; import com.android.systemui.Dependency; import com.android.systemui.FontSizeUtils; +import com.android.systemui.res.R; import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.demomode.DemoModeCommandReceiver; import com.android.systemui.plugins.DarkIconDispatcher; import com.android.systemui.plugins.DarkIconDispatcher.DarkReceiver; -import com.android.systemui.res.R; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.phone.StatusBarIconController; @@ -108,7 +106,6 @@ public class Clock extends TextView implements private final int mAmPmStyle; private boolean mShowSeconds; private Handler mSecondsHandler; - private HandlerThread mHandlerThread; // Fields to cache the width so the clock remains at an approximately constant width private int mCharsAtCurrentWidth = -1; @@ -149,8 +146,6 @@ public class Clock extends TextView implements } mBroadcastDispatcher = Dependency.get(BroadcastDispatcher.class); mUserTracker = Dependency.get(UserTracker.class); - mHandlerThread = new HandlerThread("Clock"); - mHandlerThread.start(); setIncludeFontPadding(false); } @@ -210,8 +205,7 @@ public class Clock extends TextView implements Dependency.get(TunerService.class).addTunable(this, CLOCK_SECONDS, StatusBarIconController.ICON_HIDE_LIST); mCommandQueue.addCallback(this); - mUserTracker.addCallback(mUserChangedCallback, - new HandlerExecutor(mHandlerThread.getThreadHandler())); + mUserTracker.addCallback(mUserChangedCallback, mContext.getMainExecutor()); mCurrentUserId = mUserTracker.getUserId(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmControllerImpl.java index a7440d6c200e..b7d8ee3943e3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/NextAlarmControllerImpl.java @@ -21,8 +21,6 @@ import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.os.HandlerExecutor; -import android.os.HandlerThread; import android.os.UserHandle; import androidx.annotation.NonNull; @@ -53,7 +51,6 @@ public class NextAlarmControllerImpl extends BroadcastReceiver private final UserTracker mUserTracker; private AlarmManager mAlarmManager; private AlarmManager.AlarmClockInfo mNextAlarm; - private HandlerThread mHandlerThread; private final UserTracker.Callback mUserChangedCallback = new UserTracker.Callback() { @@ -78,10 +75,7 @@ public class NextAlarmControllerImpl extends BroadcastReceiver IntentFilter filter = new IntentFilter(); filter.addAction(AlarmManager.ACTION_NEXT_ALARM_CLOCK_CHANGED); broadcastDispatcher.registerReceiver(this, filter, null, UserHandle.ALL); - mHandlerThread = new HandlerThread("NextAlarmControllerImpl"); - mHandlerThread.start(); - mUserTracker.addCallback(mUserChangedCallback, - new HandlerExecutor(mHandlerThread.getThreadHandler())); + mUserTracker.addCallback(mUserChangedCallback, mainExecutor); updateNextAlarm(); } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java index 6a6efbc11362..9f4a90658b2e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/SecurityControllerImpl.java @@ -157,7 +157,7 @@ public class SecurityControllerImpl implements SecurityController { // TODO: re-register network callback on user change. mConnectivityManager.registerNetworkCallback(REQUEST, mNetworkCallback); onUserSwitched(mUserTracker.getUserId()); - mUserTracker.addCallback(mUserChangedCallback, mBgExecutor); + mUserTracker.addCallback(mUserChangedCallback, mMainExecutor); } public void dump(PrintWriter pw, String[] args) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoControllerImpl.java index 0bc0e88114a5..2ed9d1548007 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/UserInfoControllerImpl.java @@ -36,9 +36,9 @@ import androidx.annotation.NonNull; import com.android.internal.util.UserIcons; import com.android.settingslib.drawable.UserIconDrawable; -import com.android.systemui.dagger.SysUISingleton; -import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.res.R; +import com.android.systemui.dagger.SysUISingleton; +import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.settings.UserTracker; import java.util.ArrayList; @@ -66,11 +66,11 @@ public class UserInfoControllerImpl implements UserInfoController { /** */ @Inject - public UserInfoControllerImpl(Context context, @Background Executor bgExecutor, + public UserInfoControllerImpl(Context context, @Main Executor mainExecutor, UserTracker userTracker) { mContext = context; mUserTracker = userTracker; - mUserTracker.addCallback(mUserChangedCallback, bgExecutor); + mUserTracker.addCallback(mUserChangedCallback, mainExecutor); IntentFilter profileFilter = new IntentFilter(); profileFilter.addAction(ContactsContract.Intents.ACTION_PROFILE_CHANGED); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java index f0b49307aad5..df210b073e77 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java @@ -29,7 +29,6 @@ import android.database.ContentObserver; import android.net.Uri; import android.os.Handler; import android.os.HandlerExecutor; -import android.os.HandlerThread; import android.os.Trace; import android.os.UserHandle; import android.os.UserManager; @@ -82,7 +81,6 @@ public class ZenModeControllerImpl implements ZenModeController, Dumpable { private volatile int mZenMode; private long mZenUpdateTime; private NotificationManager.Policy mConsolidatedNotificationPolicy; - private HandlerThread mHandlerThread; private final UserTracker.Callback mUserChangedCallback = new UserTracker.Callback() { @@ -135,8 +133,6 @@ public class ZenModeControllerImpl implements ZenModeController, Dumpable { } } }; - mHandlerThread = new HandlerThread("ZenModeControllerImpl"); - mHandlerThread.start(); mNoMan = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); globalSettings.registerContentObserver(Global.ZEN_MODE, modeContentObserver); updateZenMode(getModeSettingValueFromProvider()); @@ -147,8 +143,7 @@ public class ZenModeControllerImpl implements ZenModeController, Dumpable { mSetupObserver = new SetupObserver(handler); mSetupObserver.register(); mUserManager = context.getSystemService(UserManager.class); - mUserTracker.addCallback(mUserChangedCallback, - new HandlerExecutor(mHandlerThread.getThreadHandler())); + mUserTracker.addCallback(mUserChangedCallback, new HandlerExecutor(handler)); // This registers the alarm broadcast receiver for the current user mUserChangedCallback.onUserChanged(getCurrentUser(), context); diff --git a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java index 77518db9184c..2b9ad50c1257 100644 --- a/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java +++ b/packages/SystemUI/src/com/android/systemui/theme/ThemeOverlayController.java @@ -480,7 +480,7 @@ public class ThemeOverlayController implements CoreStartable, Dumpable { return; } - mUserTracker.addCallback(mUserTrackerCallback, mBgExecutor); + mUserTracker.addCallback(mUserTrackerCallback, mMainExecutor); mDeviceProvisionedController.addCallback(mDeviceProvisionedListener); diff --git a/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java b/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java index f5b4d17ae7d3..550a65c01bfc 100644 --- a/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java +++ b/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java @@ -26,7 +26,6 @@ import android.database.ContentObserver; import android.net.Uri; import android.os.Handler; import android.os.HandlerExecutor; -import android.os.HandlerThread; import android.os.Looper; import android.os.UserManager; import android.provider.Settings; @@ -39,11 +38,11 @@ import androidx.annotation.WorkerThread; import com.android.internal.util.ArrayUtils; import com.android.systemui.DejankUtils; +import com.android.systemui.res.R; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.demomode.DemoModeController; import com.android.systemui.qs.QSHost; -import com.android.systemui.res.R; import com.android.systemui.settings.UserTracker; import com.android.systemui.statusbar.phone.StatusBarIconController; import com.android.systemui.statusbar.phone.SystemUIDialog; @@ -99,7 +98,6 @@ public class TunerServiceImpl extends TunerService { private UserTracker.Callback mCurrentUserTracker; private UserTracker mUserTracker; private final ComponentName mTunerComponent; - private HandlerThread mHandlerThread; /** */ @@ -119,8 +117,7 @@ public class TunerServiceImpl extends TunerService { mDemoModeController = demoModeController; mUserTracker = userTracker; mTunerComponent = new ComponentName(mContext, TunerActivity.class); - mHandlerThread = new HandlerThread("TunerServiceImpl"); - mHandlerThread.start(); + for (UserInfo user : UserManager.get(mContext).getUsers()) { mCurrentUser = user.getUserHandle().getIdentifier(); if (getValue(TUNER_VERSION, 0) != CURRENT_TUNER_VERSION) { @@ -138,7 +135,7 @@ public class TunerServiceImpl extends TunerService { } }; mUserTracker.addCallback(mCurrentUserTracker, - new HandlerExecutor(mHandlerThread.getThreadHandler())); + new HandlerExecutor(mainHandler)); } @Override diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt index 74e133923378..37be1c6aa73d 100644 --- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt @@ -17,6 +17,7 @@ package com.android.systemui.user.data.repository +import android.annotation.SuppressLint import android.content.Context import android.content.pm.UserInfo import android.os.UserHandle @@ -190,7 +191,7 @@ constructor( } } - tracker.addCallback(callback, backgroundDispatcher.asExecutor()) + tracker.addCallback(callback, mainDispatcher.asExecutor()) send(currentSelectionStatus) awaitClose { tracker.removeCallback(callback) } @@ -209,18 +210,15 @@ constructor( override val selectedUserInfo: Flow<UserInfo> = selectedUser.map { it.userInfo } + @SuppressLint("MissingPermission") override fun refreshUsers() { applicationScope.launch { - val result = withContext(backgroundDispatcher) { manager.aliveUsers } - - if (result != null) { - _userInfos.value = - result - // Users should be sorted by ascending creation time. - .sortedBy { it.creationTime } - // The guest user is always last, regardless of creation time. - .sortedBy { it.isGuest } - } + _userInfos.value = + withContext(backgroundDispatcher) { manager.aliveUsers } + // Users should be sorted by ascending creation time. + .sortedBy { it.creationTime } + // The guest user is always last, regardless of creation time. + .sortedBy { it.isGuest } if (mainUserId == UserHandle.USER_NULL) { val mainUser = withContext(backgroundDispatcher) { manager.mainUser } diff --git a/packages/SystemUI/src/com/android/systemui/util/animation/data/repository/AnimationStatusRepository.kt b/packages/SystemUI/src/com/android/systemui/util/animation/data/repository/AnimationStatusRepository.kt index adae782eeb98..31a8d864de95 100644 --- a/packages/SystemUI/src/com/android/systemui/util/animation/data/repository/AnimationStatusRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/util/animation/data/repository/AnimationStatusRepository.kt @@ -26,7 +26,7 @@ import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.withContext +import kotlinx.coroutines.flow.flowOn /** Utility class that could give information about if animation are enabled in the system */ interface AnimationStatusRepository { @@ -45,24 +45,26 @@ constructor( * Emits true if animations are enabled in the system, after subscribing it immediately emits * the current state */ - override fun areAnimationsEnabled(): Flow<Boolean> = conflatedCallbackFlow { - val initialValue = withContext(backgroundDispatcher) { resolver.areAnimationsEnabled() } - trySend(initialValue) + override fun areAnimationsEnabled(): Flow<Boolean> = + conflatedCallbackFlow { + val initialValue = resolver.areAnimationsEnabled() + trySend(initialValue) - val observer = - object : ContentObserver(backgroundHandler) { - override fun onChange(selfChange: Boolean) { - val updatedValue = resolver.areAnimationsEnabled() - trySend(updatedValue) - } - } + val observer = + object : ContentObserver(backgroundHandler) { + override fun onChange(selfChange: Boolean) { + val updatedValue = resolver.areAnimationsEnabled() + trySend(updatedValue) + } + } - resolver.registerContentObserver( - Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE), - /* notifyForDescendants= */ false, - observer - ) + resolver.registerContentObserver( + Settings.Global.getUriFor(Settings.Global.ANIMATOR_DURATION_SCALE), + /* notifyForDescendants= */ false, + observer + ) - awaitClose { resolver.unregisterContentObserver(observer) } - } + awaitClose { resolver.unregisterContentObserver(observer) } + } + .flowOn(backgroundDispatcher) } diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt index 8fe57e116405..d47413faeadf 100644 --- a/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt +++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/Flow.kt @@ -20,21 +20,19 @@ import com.android.systemui.util.time.SystemClock import com.android.systemui.util.time.SystemClockImpl import java.util.concurrent.atomic.AtomicReference import kotlin.math.max -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onStart -import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch /** @@ -106,6 +104,14 @@ fun <S, T : S> Flow<T>.pairwise(initialValue: S): Flow<WithPrev<S, T>> = /** Holds a [newValue] emitted from a [Flow], along with the [previousValue] emitted value. */ data class WithPrev<out S, out T : S>(val previousValue: S, val newValue: T) +/** Emits a [Unit] only when the number of downstream subscribers of this flow increases. */ +fun <T> MutableSharedFlow<T>.onSubscriberAdded(): Flow<Unit> { + return subscriptionCount + .pairwise(initialValue = 0) + .filter { (previous, current) -> current > previous } + .map {} +} + /** * Returns a new [Flow] that combines the [Set] changes between each emission from [this] using * [transform]. @@ -183,34 +189,6 @@ fun <A> Flow<*>.sample(other: Flow<A>): Flow<A> = sample(other) { _, a -> a } /** * Returns a flow that mirrors the original flow, but delays values following emitted values for the - * given [periodMs]. If the original flow emits more than one value during this period, only the - * latest value is emitted. - * - * Example: - * ```kotlin - * flow { - * emit(1) // t=0ms - * delay(90) - * emit(2) // t=90ms - * delay(90) - * emit(3) // t=180ms - * delay(1010) - * emit(4) // t=1190ms - * delay(1010) - * emit(5) // t=2200ms - * }.throttle(1000) - * ``` - * - * produces the following emissions at the following times - * - * ```text - * 1 (t=0ms), 3 (t=1000ms), 4 (t=2000ms), 5 (t=3000ms) - * ``` - */ -fun <T> Flow<T>.throttle(periodMs: Long): Flow<T> = this.throttle(periodMs, SystemClockImpl()) - -/** - * Returns a flow that mirrors the original flow, but delays values following emitted values for the * given [periodMs] as reported by the given [clock]. If the original flow emits more than one value * during this period, only The latest value is emitted. * @@ -235,70 +213,37 @@ fun <T> Flow<T>.throttle(periodMs: Long): Flow<T> = this.throttle(periodMs, Syst * 1 (t=0ms), 3 (t=1000ms), 4 (t=2000ms), 5 (t=3000ms) * ``` */ -fun <T> Flow<T>.throttle(periodMs: Long, clock: SystemClock): Flow<T> = channelFlow { - coroutineScope { - var previousEmitTimeMs = 0L - var delayJob: Job? = null - var sendJob: Job? = null - val outerScope = this +fun <T> Flow<T>.throttle(periodMs: Long, clock: SystemClock = SystemClockImpl()): Flow<T> = + channelFlow { + coroutineScope { + var previousEmitTimeMs = 0L + var delayJob: Job? = null + var sendJob: Job? = null + val outerScope = this - collect { - delayJob?.cancel() - sendJob?.join() - val currentTimeMs = clock.elapsedRealtime() - val timeSinceLastEmit = currentTimeMs - previousEmitTimeMs - val timeUntilNextEmit = max(0L, periodMs - timeSinceLastEmit) - if (timeUntilNextEmit > 0L) { - // We create delayJob to allow cancellation during the delay period - delayJob = launch { - delay(timeUntilNextEmit) - sendJob = - outerScope.launch(start = CoroutineStart.UNDISPATCHED) { - send(it) - previousEmitTimeMs = clock.elapsedRealtime() - } + collect { + delayJob?.cancel() + sendJob?.join() + val currentTimeMs = clock.elapsedRealtime() + val timeSinceLastEmit = currentTimeMs - previousEmitTimeMs + val timeUntilNextEmit = max(0L, periodMs - timeSinceLastEmit) + if (timeUntilNextEmit > 0L) { + // We create delayJob to allow cancellation during the delay period + delayJob = launch { + delay(timeUntilNextEmit) + sendJob = + outerScope.launch(start = CoroutineStart.UNDISPATCHED) { + send(it) + previousEmitTimeMs = clock.elapsedRealtime() + } + } + } else { + send(it) + previousEmitTimeMs = currentTimeMs } - } else { - send(it) - previousEmitTimeMs = currentTimeMs } } } -} - -/** - * Returns a [StateFlow] launched in the surrounding [CoroutineScope]. This [StateFlow] gets its - * value by invoking [getValue] whenever an event is emitted from [changedSignals]. It will also - * immediately invoke [getValue] to establish its initial value. - */ -inline fun <T> CoroutineScope.stateFlow( - changedSignals: Flow<*>, - crossinline getValue: () -> T, -): StateFlow<T> = - changedSignals.map { getValue() }.stateIn(this, SharingStarted.Eagerly, getValue()) - -inline fun <T1, T2, T3, T4, T5, T6, R> combine( - flow: Flow<T1>, - flow2: Flow<T2>, - flow3: Flow<T3>, - flow4: Flow<T4>, - flow5: Flow<T5>, - flow6: Flow<T6>, - crossinline transform: suspend (T1, T2, T3, T4, T5, T6) -> R -): Flow<R> { - return kotlinx.coroutines.flow.combine(flow, flow2, flow3, flow4, flow5, flow6) { args: Array<*> - -> - @Suppress("UNCHECKED_CAST") - transform( - args[0] as T1, - args[1] as T2, - args[2] as T3, - args[3] as T4, - args[4] as T5, - args[5] as T6 - ) - } -} inline fun <T1, T2, T3, T4, T5, T6, T7, R> combine( flow: Flow<T1>, diff --git a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java index 7c6ad233d853..e8325065c219 100644 --- a/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java +++ b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java @@ -32,8 +32,6 @@ import android.content.pm.UserInfo; import android.content.res.Configuration; import android.graphics.Rect; import android.inputmethodservice.InputMethodService; -import android.os.HandlerExecutor; -import android.os.HandlerThread; import android.os.IBinder; import android.util.Log; import android.view.Display; @@ -127,7 +125,6 @@ public final class WMShell implements private final DisplayTracker mDisplayTracker; private final NoteTaskInitializer mNoteTaskInitializer; private final Executor mSysUiMainExecutor; - private HandlerThread mHandlerThread; // Listeners and callbacks. Note that we prefer member variable over anonymous class here to // avoid the situation that some implementations, like KeyguardUpdateMonitor, use WeakReference @@ -209,8 +206,6 @@ public final class WMShell implements mDisplayTracker = displayTracker; mNoteTaskInitializer = noteTaskInitializer; mSysUiMainExecutor = sysUiMainExecutor; - mHandlerThread = new HandlerThread("WMShell"); - mHandlerThread.start(); } @Override @@ -224,8 +219,7 @@ public final class WMShell implements mKeyguardUpdateMonitor.registerCallback(mKeyguardUpdateMonitorCallback); // Subscribe to user changes - mUserTracker.addCallback(mUserChangedCallback, - new HandlerExecutor(mHandlerThread.getThreadHandler())); + mUserTracker.addCallback(mUserChangedCallback, mContext.getMainExecutor()); mCommandQueue.addCallback(this); mPipOptional.ifPresent(this::initPip); @@ -353,10 +347,10 @@ public final class WMShell implements desktopMode.addVisibleTasksListener( new DesktopModeTaskRepository.VisibleTasksListener() { @Override - public void onVisibilityChanged(int displayId, boolean hasFreeformTasks) { + public void onTasksVisibilityChanged(int displayId, int visibleTasksCount) { if (displayId == Display.DEFAULT_DISPLAY) { mSysUiState.setFlag(SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE, - hasFreeformTasks) + visibleTasksCount > 0) .commitUpdate(mDisplayTracker.getDefaultDisplayId()); } // TODO(b/278084491): update sysui state for changes on other displays diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/TestableWindowManager.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/TestableWindowManager.java index 44770fab2d30..b23dfdc68a87 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/TestableWindowManager.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/TestableWindowManager.java @@ -16,8 +16,10 @@ package com.android.systemui.accessibility; +import android.annotation.NonNull; import android.graphics.Rect; import android.graphics.Region; +import android.os.IBinder; import android.view.Display; import android.view.View; import android.view.ViewGroup; @@ -89,6 +91,11 @@ public class TestableWindowManager implements WindowManager { return mWindowManager.getMaximumWindowMetrics(); } + @Override + public @NonNull IBinder getDefaultToken() { + return mWindowManager.getDefaultToken(); + } + public View getAttachedView() { return mView; } diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java index d86d12303140..8299acbc2d52 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationAnimationControllerTest.java @@ -21,8 +21,10 @@ import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentat import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyFloat; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; @@ -33,10 +35,16 @@ import android.animation.ValueAnimator; import android.annotation.Nullable; import android.content.Context; import android.graphics.Rect; +import android.os.Binder; import android.os.Handler; import android.os.RemoteException; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.testing.AndroidTestingRunner; import android.view.SurfaceControl; +import android.view.SurfaceControlViewHost; import android.view.View; import android.view.WindowManager; import android.view.WindowManagerGlobal; @@ -46,6 +54,7 @@ import android.view.animation.AccelerateInterpolator; import androidx.test.filters.LargeTest; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; +import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.AnimatorTestRule; import com.android.systemui.model.SysUiState; @@ -63,7 +72,10 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; +import java.util.ArrayList; +import java.util.List; import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; @LargeTest @RunWith(AndroidTestingRunner.class) @@ -71,6 +83,8 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { @Rule public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(); + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); private static final float DEFAULT_SCALE = 4.0f; private static final float DEFAULT_CENTER_X = 400.0f; private static final float DEFAULT_CENTER_Y = 500.0f; @@ -107,6 +121,13 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { private TestableWindowManager mWindowManager; private ValueAnimator mValueAnimator; + // This list contains all SurfaceControlViewHosts created during a given test. If the + // magnification window is recreated during a test, the list will contain more than a single + // element. + private List<SurfaceControlViewHost> mSurfaceControlViewHosts = new ArrayList<>(); + // The most recently created SurfaceControlViewHost. + private SurfaceControlViewHost mSurfaceControlViewHost; + private SurfaceControl.Transaction mTransaction; @Before public void setUp() throws Exception { @@ -123,10 +144,27 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { mValueAnimator = newValueAnimator(); mWindowMagnificationAnimationController = new WindowMagnificationAnimationController( mContext, mValueAnimator); - mController = new SpyWindowMagnificationController(mContext, mHandler, + + Supplier<SurfaceControlViewHost> scvhSupplier = () -> { + mSurfaceControlViewHost = spy(new SurfaceControlViewHost( + mContext, mContext.getDisplay(), new Binder(), "WindowMagnification")); + mSurfaceControlViewHosts.add(mSurfaceControlViewHost); + return mSurfaceControlViewHost; + }; + + mTransaction = spy(new SurfaceControl.Transaction()); + mController = new SpyWindowMagnificationController( + mContext, + mHandler, mWindowMagnificationAnimationController, - mSfVsyncFrameProvider, null, new SurfaceControl.Transaction(), - mWindowMagnifierCallback, mSysUiState, mSecureSettings); + /* mirrorWindowControl= */ null, + mTransaction, + mWindowMagnifierCallback, + mSysUiState, + mSecureSettings, + scvhSupplier, + mSfVsyncFrameProvider); + mSpyController = mController.getSpyController(); } @@ -235,8 +273,52 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { verifyFinalSpec(DEFAULT_SCALE, DEFAULT_CENTER_X, DEFAULT_CENTER_Y); } + @RequiresFlagsEnabled(Flags.FLAG_CREATE_WINDOWLESS_WINDOW_MAGNIFIER) @Test - public void enableWindowMagnificationWithScaleOne_enabled_AnimationAndInvokeCallback() + public void + enableWindowMagnificationScaleOne_enabledAndWindowlessFlagOn_AnimationAndCallbackTrue() + throws RemoteException { + enableWindowMagnificationWithoutAnimation(); + + // Wait for Rects updated. + waitForIdleSync(); + View mirrorView = mSurfaceControlViewHost.getView(); + final float targetScale = 1.0f; + // Move the magnifier to the top left corner, within the boundary + final float targetCenterX = mirrorView.getWidth() / 2.0f; + final float targetCenterY = mirrorView.getHeight() / 2.0f; + + Mockito.reset(mSpyController); + getInstrumentation().runOnMainSync(() -> { + mWindowMagnificationAnimationController.enableWindowMagnification(targetScale, + targetCenterX, targetCenterY, mAnimationCallback); + mCurrentScale.set(mController.getScale()); + mCurrentCenterX.set(mController.getCenterX()); + mCurrentCenterY.set(mController.getCenterY()); + advanceTimeBy(mWaitAnimationDuration); + }); + + verify(mSpyController, atLeast(2)).enableWindowMagnificationInternal( + mScaleCaptor.capture(), + mCenterXCaptor.capture(), mCenterYCaptor.capture(), + mOffsetXCaptor.capture(), mOffsetYCaptor.capture()); + verifyStartValue(mScaleCaptor, mCurrentScale.get()); + verifyStartValue(mCenterXCaptor, mCurrentCenterX.get()); + verifyStartValue(mCenterYCaptor, mCurrentCenterY.get()); + verifyStartValue(mOffsetXCaptor, 0f); + verifyStartValue(mOffsetYCaptor, 0f); + + verifyFinalSpec(targetScale, targetCenterX, targetCenterY); + + verify(mAnimationCallback).onResult(true); + assertEquals(WindowMagnificationAnimationController.STATE_ENABLED, + mWindowMagnificationAnimationController.getState()); + } + + @RequiresFlagsDisabled(Flags.FLAG_CREATE_WINDOWLESS_WINDOW_MAGNIFIER) + @Test + public void + enableWindowMagnificationScaleOne_enabledAndWindowlessFlagOff_AnimationAndCallbackTrue() throws RemoteException { enableWindowMagnificationWithoutAnimation(); @@ -475,8 +557,46 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { verify(mAnimationCallback2).onResult(true); } + @RequiresFlagsEnabled(Flags.FLAG_CREATE_WINDOWLESS_WINDOW_MAGNIFIER) + @Test + public void enableWindowMagnificationWithOffset_windowlessFlagOn_expectedValues() { + final float offsetRatio = -0.1f; + final Rect windowBounds = new Rect(mWindowManager.getCurrentWindowMetrics().getBounds()); + + Mockito.reset(mSpyController); + getInstrumentation().runOnMainSync(() -> { + mWindowMagnificationAnimationController.enableWindowMagnification(DEFAULT_SCALE, + windowBounds.exactCenterX(), windowBounds.exactCenterY(), + offsetRatio, offsetRatio, mAnimationCallback); + advanceTimeBy(mWaitAnimationDuration); + }); + // Wait for Rects update + waitForIdleSync(); + + final int mirrorSurfaceMargin = mContext.getResources().getDimensionPixelSize( + R.dimen.magnification_mirror_surface_margin); + final int defaultMagnificationWindowSize = + mController.getMagnificationWindowSizeFromIndex( + WindowMagnificationSettings.MagnificationSize.MEDIUM); + final int defaultMagnificationFrameSize = + defaultMagnificationWindowSize - 2 * mirrorSurfaceMargin; + final int expectedOffset = (int) (defaultMagnificationFrameSize / 2 * offsetRatio); + + final float expectedX = (int) (windowBounds.exactCenterX() + expectedOffset + - defaultMagnificationWindowSize / 2); + final float expectedY = (int) (windowBounds.exactCenterY() + expectedOffset + - defaultMagnificationWindowSize / 2); + + // This is called 4 times when (1) first creating WindowlessMirrorWindow (2) SurfaceView is + // created and we place the mirrored content as a child of the SurfaceView + // (3) the animation starts (4) the animation updates + verify(mTransaction, times(4)) + .setPosition(any(SurfaceControl.class), eq(expectedX), eq(expectedY)); + } + + @RequiresFlagsDisabled(Flags.FLAG_CREATE_WINDOWLESS_WINDOW_MAGNIFIER) @Test - public void enableWindowMagnificationWithOffset_expectedValues() { + public void enableWindowMagnificationWithOffset_windowlessFlagOff_expectedValues() { final float offsetRatio = -0.1f; final Rect windowBounds = new Rect(mWindowManager.getCurrentWindowMetrics().getBounds()); @@ -876,23 +996,28 @@ public class WindowMagnificationAnimationControllerTest extends SysuiTestCase { private static class SpyWindowMagnificationController extends WindowMagnificationController { private WindowMagnificationController mSpyController; - SpyWindowMagnificationController(Context context, Handler handler, + SpyWindowMagnificationController(Context context, + Handler handler, WindowMagnificationAnimationController animationController, - SfVsyncFrameCallbackProvider sfVsyncFrameProvider, - MirrorWindowControl mirrorWindowControl, SurfaceControl.Transaction transaction, - WindowMagnifierCallback callback, SysUiState sysUiState, - SecureSettings secureSettings) { + MirrorWindowControl mirrorWindowControl, + SurfaceControl.Transaction transaction, + WindowMagnifierCallback callback, + SysUiState sysUiState, + SecureSettings secureSettings, + Supplier<SurfaceControlViewHost> scvhSupplier, + SfVsyncFrameCallbackProvider sfVsyncFrameProvider) { super( context, handler, animationController, - sfVsyncFrameProvider, mirrorWindowControl, transaction, callback, sysUiState, - WindowManagerGlobal::getWindowSession, - secureSettings); + secureSettings, + scvhSupplier, + sfVsyncFrameProvider, + WindowManagerGlobal::getWindowSession); mSpyController = Mockito.mock(WindowMagnificationController.class); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java index 04aef82cef43..2225ad6e49d7 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerTest.java @@ -68,6 +68,9 @@ import android.graphics.RegionIterator; import android.os.Handler; import android.os.RemoteException; import android.os.SystemClock; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.provider.Settings; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; @@ -90,6 +93,7 @@ import androidx.test.InstrumentationRegistry; import androidx.test.filters.LargeTest; import com.android.internal.graphics.SfVsyncFrameCallbackProvider; +import com.android.systemui.Flags; import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.AnimatorTestRule; import com.android.systemui.kosmos.KosmosJavaAdapter; @@ -121,10 +125,13 @@ import java.util.concurrent.atomic.AtomicInteger; @LargeTest @TestableLooper.RunWithLooper @RunWith(AndroidTestingRunner.class) +@RequiresFlagsDisabled(Flags.FLAG_CREATE_WINDOWLESS_WINDOW_MAGNIFIER) public class WindowMagnificationControllerTest extends SysuiTestCase { @Rule public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(); + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); private static final int LAYOUT_CHANGE_TIMEOUT_MS = 5000; @Mock @@ -216,13 +223,14 @@ public class WindowMagnificationControllerTest extends SysuiTestCase { mContext, mHandler, mWindowMagnificationAnimationController, - mSfVsyncFrameProvider, mMirrorWindowControl, mTransaction, mWindowMagnifierCallback, mSysUiState, - () -> mWindowSessionSpy, - mSecureSettings); + mSecureSettings, + /* scvhSupplier= */ () -> null, + mSfVsyncFrameProvider, + /* globalWindowSessionSupplier= */ () -> mWindowSessionSpy); verify(mMirrorWindowControl).setWindowDelegate( any(MirrorWindowControl.MirrorWindowDelegate.class)); @@ -270,7 +278,7 @@ public class WindowMagnificationControllerTest extends SysuiTestCase { mInstrumentation.runOnMainSync( () -> mWindowMagnificationController.enableWindowMagnification(Float.NaN, Float.NaN, Float.NaN, /* magnificationFrameOffsetRatioX= */ 0, - /* magnificationFrameOffsetRatioY= */ 0, null)); + /* magnificationFrameOffsetRatioY= */ 0, null)); // Waits for the surface created verify(mWindowMagnifierCallback, timeout(LAYOUT_CHANGE_TIMEOUT_MS)).onSourceBoundsChanged( @@ -1415,7 +1423,7 @@ public class WindowMagnificationControllerTest extends SysuiTestCase { } private MotionEvent obtainMotionEvent(long downTime, long eventTime, int action, float x, - float y) { + float y) { return mMotionEventHelper.obtainMotionEvent(downTime, eventTime, action, x, y); } @@ -1474,4 +1482,4 @@ public class WindowMagnificationControllerTest extends SysuiTestCase { }); } } -} +}
\ No newline at end of file diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerWindowlessMagnifierTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerWindowlessMagnifierTest.java new file mode 100644 index 000000000000..66fb63b6c331 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/WindowMagnificationControllerWindowlessMagnifierTest.java @@ -0,0 +1,1502 @@ +/* + * 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.accessibility; + +import static android.content.pm.PackageManager.FEATURE_WINDOW_MAGNIFICATION; +import static android.content.res.Configuration.ORIENTATION_LANDSCAPE; +import static android.content.res.Configuration.ORIENTATION_PORTRAIT; +import static android.content.res.Configuration.ORIENTATION_UNDEFINED; +import static android.view.MotionEvent.ACTION_DOWN; +import static android.view.MotionEvent.ACTION_UP; +import static android.view.WindowInsets.Type.systemGestures; +import static android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction; + +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_MAGNIFICATION_OVERLAP; + +import static org.hamcrest.Matchers.containsString; +import static org.hamcrest.Matchers.hasItems; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertTrue; +import static org.mockito.AdditionalAnswers.returnsSecondArg; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyFloat; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeast; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.timeout; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.animation.ValueAnimator; +import android.annotation.IdRes; +import android.annotation.Nullable; +import android.app.Instrumentation; +import android.content.Context; +import android.content.pm.ActivityInfo; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.graphics.Insets; +import android.graphics.PointF; +import android.graphics.Rect; +import android.os.Binder; +import android.os.Handler; +import android.os.RemoteException; +import android.os.SystemClock; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.provider.Settings; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.testing.TestableResources; +import android.text.TextUtils; +import android.util.Size; +import android.view.AttachedSurfaceControl; +import android.view.Display; +import android.view.Gravity; +import android.view.MotionEvent; +import android.view.Surface; +import android.view.SurfaceControl; +import android.view.SurfaceControlViewHost; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewRootImpl; +import android.view.WindowInsets; +import android.view.WindowManager; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.IRemoteMagnificationAnimationCallback; +import android.widget.FrameLayout; + +import androidx.test.InstrumentationRegistry; +import androidx.test.filters.LargeTest; + +import com.android.systemui.Flags; +import com.android.systemui.SysuiTestCase; +import com.android.systemui.animation.AnimatorTestRule; +import com.android.systemui.kosmos.KosmosJavaAdapter; +import com.android.systemui.model.SysUiState; +import com.android.systemui.res.R; +import com.android.systemui.settings.FakeDisplayTracker; +import com.android.systemui.util.leak.ReferenceTestUtils; +import com.android.systemui.util.settings.SecureSettings; +import com.android.systemui.utils.os.FakeHandler; + +import com.google.common.util.concurrent.AtomicDouble; + +import org.junit.After; +import org.junit.Assume; +import org.junit.Before; +import org.junit.Ignore; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Supplier; + +@LargeTest +@TestableLooper.RunWithLooper +@RunWith(AndroidTestingRunner.class) +@RequiresFlagsEnabled(Flags.FLAG_CREATE_WINDOWLESS_WINDOW_MAGNIFIER) +public class WindowMagnificationControllerWindowlessMagnifierTest extends SysuiTestCase { + + @Rule + public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(); + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + + private static final int LAYOUT_CHANGE_TIMEOUT_MS = 5000; + @Mock + private MirrorWindowControl mMirrorWindowControl; + @Mock + private WindowMagnifierCallback mWindowMagnifierCallback; + @Mock + IRemoteMagnificationAnimationCallback mAnimationCallback; + @Mock + IRemoteMagnificationAnimationCallback mAnimationCallback2; + + private SurfaceControl.Transaction mTransaction; + @Mock + private SecureSettings mSecureSettings; + + private long mWaitAnimationDuration; + private long mWaitBounceEffectDuration; + + private Handler mHandler; + private TestableWindowManager mWindowManager; + private SysUiState mSysUiState; + private Resources mResources; + private WindowMagnificationAnimationController mWindowMagnificationAnimationController; + private WindowMagnificationController mWindowMagnificationController; + private Instrumentation mInstrumentation; + private final ValueAnimator mValueAnimator = ValueAnimator.ofFloat(0, 1.0f).setDuration(0); + private final FakeDisplayTracker mDisplayTracker = new FakeDisplayTracker(mContext); + + private View mSpyView; + private View.OnTouchListener mTouchListener; + + private MotionEventHelper mMotionEventHelper = new MotionEventHelper(); + + // This list contains all SurfaceControlViewHosts created during a given test. If the + // magnification window is recreated during a test, the list will contain more than a single + // element. + private List<SurfaceControlViewHost> mSurfaceControlViewHosts = new ArrayList<>(); + // The most recently created SurfaceControlViewHost. + private SurfaceControlViewHost mSurfaceControlViewHost; + private KosmosJavaAdapter mKosmos; + + /** + * return whether window magnification is supported for current test context. + */ + private boolean isWindowModeSupported() { + return getContext().getPackageManager().hasSystemFeature(FEATURE_WINDOW_MAGNIFICATION); + } + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mKosmos = new KosmosJavaAdapter(this); + mContext = Mockito.spy(getContext()); + mHandler = new FakeHandler(TestableLooper.get(this).getLooper()); + mInstrumentation = InstrumentationRegistry.getInstrumentation(); + final WindowManager wm = mContext.getSystemService(WindowManager.class); + mWindowManager = spy(new TestableWindowManager(wm)); + + mContext.addMockSystemService(Context.WINDOW_SERVICE, mWindowManager); + mSysUiState = new SysUiState(mDisplayTracker, mKosmos.getSceneContainerPlugin()); + mSysUiState.addCallback(Mockito.mock(SysUiState.SysUiStateCallback.class)); + when(mSecureSettings.getIntForUser(anyString(), anyInt(), anyInt())).then( + returnsSecondArg()); + when(mSecureSettings.getFloatForUser(anyString(), anyFloat(), anyInt())).then( + returnsSecondArg()); + + mResources = getContext().getOrCreateTestableResources().getResources(); + // prevent the config orientation from undefined, which may cause config.diff method + // neglecting the orientation update. + if (mResources.getConfiguration().orientation == ORIENTATION_UNDEFINED) { + mResources.getConfiguration().orientation = ORIENTATION_PORTRAIT; + } + + // Using the animation duration in WindowMagnificationAnimationController for testing. + mWaitAnimationDuration = mResources.getInteger( + com.android.internal.R.integer.config_longAnimTime); + // Using the bounce effect duration in WindowMagnificationController for testing. + mWaitBounceEffectDuration = mResources.getInteger( + com.android.internal.R.integer.config_shortAnimTime); + + mWindowMagnificationAnimationController = new WindowMagnificationAnimationController( + mContext, mValueAnimator); + Supplier<SurfaceControlViewHost> scvhSupplier = () -> { + mSurfaceControlViewHost = spy(new SurfaceControlViewHost( + mContext, mContext.getDisplay(), new Binder(), "WindowMagnification")); + ViewRootImpl viewRoot = mock(ViewRootImpl.class); + when(mSurfaceControlViewHost.getRootSurfaceControl()).thenReturn(viewRoot); + mSurfaceControlViewHosts.add(mSurfaceControlViewHost); + return mSurfaceControlViewHost; + }; + mTransaction = spy(new SurfaceControl.Transaction()); + mWindowMagnificationController = + new WindowMagnificationController( + mContext, + mHandler, + mWindowMagnificationAnimationController, + mMirrorWindowControl, + mTransaction, + mWindowMagnifierCallback, + mSysUiState, + mSecureSettings, + scvhSupplier, + /* sfVsyncFrameProvider= */ null, + /* globalWindowSessionSupplier= */ null); + + verify(mMirrorWindowControl).setWindowDelegate( + any(MirrorWindowControl.MirrorWindowDelegate.class)); + mSpyView = Mockito.spy(new View(mContext)); + doAnswer((invocation) -> { + mTouchListener = invocation.getArgument(0); + return null; + }).when(mSpyView).setOnTouchListener( + any(View.OnTouchListener.class)); + + // skip test if window magnification is not supported to prevent fail results. (b/279820875) + Assume.assumeTrue(isWindowModeSupported()); + } + + @After + public void tearDown() { + mInstrumentation.runOnMainSync( + () -> mWindowMagnificationController.deleteWindowMagnification()); + mValueAnimator.cancel(); + } + + @Test + public void initWindowMagnificationController_checkAllowDiagonalScrollingWithSecureSettings() { + verify(mSecureSettings).getIntForUser( + eq(Settings.Secure.ACCESSIBILITY_ALLOW_DIAGONAL_SCROLLING), + /* def */ eq(1), /* userHandle= */ anyInt()); + assertTrue(mWindowMagnificationController.isDiagonalScrollingEnabled()); + } + + @Test + public void enableWindowMagnification_showControlAndNotifyBoundsChanged() { + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + }); + + verify(mMirrorWindowControl).showControl(); + verify(mWindowMagnifierCallback, + timeout(LAYOUT_CHANGE_TIMEOUT_MS).atLeastOnce()).onWindowMagnifierBoundsChanged( + eq(mContext.getDisplayId()), any(Rect.class)); + } + + @Test + public void enableWindowMagnification_notifySourceBoundsChanged() { + mInstrumentation.runOnMainSync( + () -> mWindowMagnificationController.enableWindowMagnification(Float.NaN, Float.NaN, + Float.NaN, /* magnificationFrameOffsetRatioX= */ 0, + /* magnificationFrameOffsetRatioY= */ 0, null)); + + // Waits for the surface created + verify(mWindowMagnifierCallback, timeout(LAYOUT_CHANGE_TIMEOUT_MS)).onSourceBoundsChanged( + (eq(mContext.getDisplayId())), any()); + } + + @Test + public void enableWindowMagnification_disabled_notifySourceBoundsChanged() { + enableWindowMagnification_notifySourceBoundsChanged(); + mInstrumentation.runOnMainSync( + () -> mWindowMagnificationController.deleteWindowMagnification(null)); + Mockito.reset(mWindowMagnifierCallback); + + enableWindowMagnification_notifySourceBoundsChanged(); + } + + @Test + public void enableWindowMagnification_withAnimation_schedulesFrame() { + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnification(2.0f, 10, + 10, /* magnificationFrameOffsetRatioX= */ 0, + /* magnificationFrameOffsetRatioY= */ 0, + Mockito.mock(IRemoteMagnificationAnimationCallback.class)); + }); + advanceTimeBy(LAYOUT_CHANGE_TIMEOUT_MS); + + verify(mTransaction, atLeastOnce()).setGeometry(any(), any(), any(), + eq(Surface.ROTATION_0)); + } + + @Test + public void moveWindowMagnifier_enabled_notifySourceBoundsChanged() { + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnification(Float.NaN, Float.NaN, + Float.NaN, 0, 0, null); + }); + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.moveWindowMagnifier(10, 10); + }); + + final ArgumentCaptor<Rect> sourceBoundsCaptor = ArgumentCaptor.forClass(Rect.class); + verify(mWindowMagnifierCallback, atLeast(2)).onSourceBoundsChanged( + (eq(mContext.getDisplayId())), sourceBoundsCaptor.capture()); + assertEquals(mWindowMagnificationController.getCenterX(), + sourceBoundsCaptor.getValue().exactCenterX(), 0); + assertEquals(mWindowMagnificationController.getCenterY(), + sourceBoundsCaptor.getValue().exactCenterY(), 0); + } + + @Test + public void enableWindowMagnification_systemGestureExclusionRectsIsSet() { + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + }); + // Wait for Rects updated. + waitForIdleSync(); + + List<Rect> rects = mSurfaceControlViewHost.getView().getSystemGestureExclusionRects(); + assertFalse(rects.isEmpty()); + } + + @Ignore("The default window size should be constrained after fixing b/288056772") + @Test + public void enableWindowMagnification_LargeScreen_windowSizeIsConstrained() { + final int screenSize = mWindowManager.getCurrentWindowMetrics().getBounds().width() * 10; + mWindowManager.setWindowBounds(new Rect(0, 0, screenSize, screenSize)); + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + }); + + final int halfScreenSize = screenSize / 2; + ViewGroup.LayoutParams params = mSurfaceControlViewHost.getView().getLayoutParams(); + // The frame size should be the half of smaller value of window height/width unless it + //exceed the max frame size. + assertTrue(params.width < halfScreenSize); + assertTrue(params.height < halfScreenSize); + } + + @Test + public void deleteWindowMagnification_destroyControlAndUnregisterComponentCallback() { + mInstrumentation.runOnMainSync( + () -> mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, + Float.NaN, + Float.NaN)); + + mInstrumentation.runOnMainSync( + () -> mWindowMagnificationController.deleteWindowMagnification()); + + verify(mMirrorWindowControl).destroyControl(); + verify(mContext).unregisterComponentCallbacks(mWindowMagnificationController); + } + + @Test + public void deleteWindowMagnification_enableAtTheBottom_overlapFlagIsFalse() { + final WindowManager wm = mContext.getSystemService(WindowManager.class); + final Rect bounds = wm.getCurrentWindowMetrics().getBounds(); + setSystemGestureInsets(); + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + bounds.bottom); + }); + ReferenceTestUtils.waitForCondition(this::hasMagnificationOverlapFlag); + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.deleteWindowMagnification(); + }); + + verify(mMirrorWindowControl).destroyControl(); + assertFalse(hasMagnificationOverlapFlag()); + } + + @Test + public void deleteWindowMagnification_notifySourceBoundsChanged() { + mInstrumentation.runOnMainSync( + () -> mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, + Float.NaN, + Float.NaN)); + + mInstrumentation.runOnMainSync( + () -> mWindowMagnificationController.deleteWindowMagnification()); + + // The first time is for notifying magnification enabled and the second time is for + // notifying magnification disabled. + verify(mWindowMagnifierCallback, times(2)).onSourceBoundsChanged( + (eq(mContext.getDisplayId())), any()); + } + + @Test + public void moveMagnifier_schedulesFrame() { + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + }); + + waitForIdleSync(); + mInstrumentation.runOnMainSync( + () -> mWindowMagnificationController.moveWindowMagnifier(100f, 100f)); + + verify(mTransaction, atLeastOnce()).setGeometry(any(), any(), any(), + eq(Surface.ROTATION_0)); + } + + @Test + public void moveWindowMagnifierToPositionWithAnimation_expectedValuesAndInvokeCallback() + throws RemoteException { + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnification(Float.NaN, Float.NaN, + Float.NaN, 0, 0, null); + }); + + final ArgumentCaptor<Rect> sourceBoundsCaptor = ArgumentCaptor.forClass(Rect.class); + verify(mWindowMagnifierCallback, timeout(LAYOUT_CHANGE_TIMEOUT_MS)) + .onSourceBoundsChanged((eq(mContext.getDisplayId())), sourceBoundsCaptor.capture()); + final float targetCenterX = sourceBoundsCaptor.getValue().exactCenterX() + 10; + final float targetCenterY = sourceBoundsCaptor.getValue().exactCenterY() + 10; + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.moveWindowMagnifierToPosition( + targetCenterX, targetCenterY, mAnimationCallback); + }); + advanceTimeBy(mWaitAnimationDuration); + + verify(mAnimationCallback, times(1)).onResult(eq(true)); + verify(mAnimationCallback, never()).onResult(eq(false)); + verify(mWindowMagnifierCallback, timeout(LAYOUT_CHANGE_TIMEOUT_MS)) + .onSourceBoundsChanged((eq(mContext.getDisplayId())), sourceBoundsCaptor.capture()); + assertEquals(mWindowMagnificationController.getCenterX(), + sourceBoundsCaptor.getValue().exactCenterX(), 0); + assertEquals(mWindowMagnificationController.getCenterY(), + sourceBoundsCaptor.getValue().exactCenterY(), 0); + assertEquals(mWindowMagnificationController.getCenterX(), targetCenterX, 0); + assertEquals(mWindowMagnificationController.getCenterY(), targetCenterY, 0); + } + + @Test + public void moveWindowMagnifierToPositionMultipleTimes_expectedValuesAndInvokeCallback() + throws RemoteException { + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnification(Float.NaN, Float.NaN, + Float.NaN, 0, 0, null); + }); + + final ArgumentCaptor<Rect> sourceBoundsCaptor = ArgumentCaptor.forClass(Rect.class); + verify(mWindowMagnifierCallback, timeout(LAYOUT_CHANGE_TIMEOUT_MS)) + .onSourceBoundsChanged((eq(mContext.getDisplayId())), sourceBoundsCaptor.capture()); + final float centerX = sourceBoundsCaptor.getValue().exactCenterX(); + final float centerY = sourceBoundsCaptor.getValue().exactCenterY(); + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.moveWindowMagnifierToPosition( + centerX + 10, centerY + 10, mAnimationCallback); + mWindowMagnificationController.moveWindowMagnifierToPosition( + centerX + 20, centerY + 20, mAnimationCallback); + mWindowMagnificationController.moveWindowMagnifierToPosition( + centerX + 30, centerY + 30, mAnimationCallback); + mWindowMagnificationController.moveWindowMagnifierToPosition( + centerX + 40, centerY + 40, mAnimationCallback2); + }); + advanceTimeBy(mWaitAnimationDuration); + + // only the last one callback will return true + verify(mAnimationCallback2).onResult(eq(true)); + // the others will return false + verify(mAnimationCallback, times(3)).onResult(eq(false)); + verify(mWindowMagnifierCallback, timeout(LAYOUT_CHANGE_TIMEOUT_MS)) + .onSourceBoundsChanged((eq(mContext.getDisplayId())), sourceBoundsCaptor.capture()); + assertEquals(mWindowMagnificationController.getCenterX(), + sourceBoundsCaptor.getValue().exactCenterX(), 0); + assertEquals(mWindowMagnificationController.getCenterY(), + sourceBoundsCaptor.getValue().exactCenterY(), 0); + assertEquals(mWindowMagnificationController.getCenterX(), centerX + 40, 0); + assertEquals(mWindowMagnificationController.getCenterY(), centerY + 40, 0); + } + + @Test + public void setScale_enabled_expectedValueAndUpdateStateDescription() { + mInstrumentation.runOnMainSync( + () -> mWindowMagnificationController.enableWindowMagnificationInternal(2.0f, + Float.NaN, Float.NaN)); + + mInstrumentation.runOnMainSync(() -> mWindowMagnificationController.setScale(3.0f)); + + assertEquals(3.0f, mWindowMagnificationController.getScale(), 0); + final View mirrorView = mSurfaceControlViewHost.getView(); + assertNotNull(mirrorView); + assertThat(mirrorView.getStateDescription().toString(), containsString("300")); + } + + @Test + public void onConfigurationChanged_disabled_withoutException() { + Display display = Mockito.spy(mContext.getDisplay()); + when(display.getRotation()).thenReturn(Surface.ROTATION_90); + when(mContext.getDisplay()).thenReturn(display); + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_DENSITY); + mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_ORIENTATION); + mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_LOCALE); + mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_SCREEN_SIZE); + }); + } + + @Test + public void onOrientationChanged_enabled_updateDisplayRotationAndCenterStayAtSamePosition() { + final int newRotation = simulateRotateTheDevice(); + final Rect windowBounds = new Rect(mWindowManager.getCurrentWindowMetrics().getBounds()); + final float center = Math.min(windowBounds.exactCenterX(), windowBounds.exactCenterY()); + final float displayWidth = windowBounds.width(); + final PointF magnifiedCenter = new PointF(center, center + 5f); + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, + magnifiedCenter.x, magnifiedCenter.y); + // Get the center again in case the center we set is out of screen. + magnifiedCenter.set(mWindowMagnificationController.getCenterX(), + mWindowMagnificationController.getCenterY()); + }); + // Rotate the window clockwise 90 degree. + windowBounds.set(windowBounds.top, windowBounds.left, windowBounds.bottom, + windowBounds.right); + mWindowManager.setWindowBounds(windowBounds); + + mInstrumentation.runOnMainSync(() -> mWindowMagnificationController.onConfigurationChanged( + ActivityInfo.CONFIG_ORIENTATION)); + + assertEquals(newRotation, mWindowMagnificationController.mRotation); + final PointF expectedCenter = new PointF(magnifiedCenter.y, + displayWidth - magnifiedCenter.x); + final PointF actualCenter = new PointF(mWindowMagnificationController.getCenterX(), + mWindowMagnificationController.getCenterY()); + assertEquals(expectedCenter, actualCenter); + } + + @Test + public void onOrientationChanged_disabled_updateDisplayRotation() { + final Rect windowBounds = new Rect(mWindowManager.getCurrentWindowMetrics().getBounds()); + // Rotate the window clockwise 90 degree. + windowBounds.set(windowBounds.top, windowBounds.left, windowBounds.bottom, + windowBounds.right); + mWindowManager.setWindowBounds(windowBounds); + final int newRotation = simulateRotateTheDevice(); + + mInstrumentation.runOnMainSync(() -> mWindowMagnificationController.onConfigurationChanged( + ActivityInfo.CONFIG_ORIENTATION)); + + assertEquals(newRotation, mWindowMagnificationController.mRotation); + } + + @Test + public void onScreenSizeAndDensityChanged_enabledAtTheCenterOfScreen_keepSameWindowSizeRatio() { + // The default position is at the center of the screen. + final float expectedRatio = 0.5f; + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + }); + + // Screen size and density change + mContext.getResources().getConfiguration().smallestScreenWidthDp = + mContext.getResources().getConfiguration().smallestScreenWidthDp * 2; + final Rect testWindowBounds = new Rect( + mWindowManager.getCurrentWindowMetrics().getBounds()); + testWindowBounds.set(testWindowBounds.left, testWindowBounds.top, + testWindowBounds.right + 100, testWindowBounds.bottom + 100); + mWindowManager.setWindowBounds(testWindowBounds); + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_SCREEN_SIZE); + }); + + // The ratio of center to window size should be the same. + assertEquals(expectedRatio, + mWindowMagnificationController.getCenterX() / testWindowBounds.width(), + 0); + assertEquals(expectedRatio, + mWindowMagnificationController.getCenterY() / testWindowBounds.height(), + 0); + } + + @Test + public void onScreenChangedToSavedDensity_enabled_restoreSavedMagnifierWindow() { + mContext.getResources().getConfiguration().smallestScreenWidthDp = + mContext.getResources().getConfiguration().smallestScreenWidthDp * 2; + int windowFrameSize = mResources.getDimensionPixelSize( + com.android.internal.R.dimen.accessibility_window_magnifier_min_size); + mWindowMagnificationController.mWindowMagnificationSizePrefs.saveSizeForCurrentDensity( + new Size(windowFrameSize, windowFrameSize)); + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + }); + + ViewGroup.LayoutParams params = mSurfaceControlViewHost.getView().getLayoutParams(); + assertTrue(params.width == windowFrameSize); + assertTrue(params.height == windowFrameSize); + } + + @Test + public void screenSizeIsChangedToLarge_enabled_defaultWindowSize() { + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + }); + final int screenSize = mWindowManager.getCurrentWindowMetrics().getBounds().width() * 10; + // Screen size and density change + mContext.getResources().getConfiguration().smallestScreenWidthDp = + mContext.getResources().getConfiguration().smallestScreenWidthDp * 2; + mWindowManager.setWindowBounds(new Rect(0, 0, screenSize, screenSize)); + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_SCREEN_SIZE); + }); + + final int defaultWindowSize = + mWindowMagnificationController.getMagnificationWindowSizeFromIndex( + WindowMagnificationSettings.MagnificationSize.MEDIUM); + ViewGroup.LayoutParams params = mSurfaceControlViewHost.getView().getLayoutParams(); + + assertTrue(params.width == defaultWindowSize); + assertTrue(params.height == defaultWindowSize); + } + + @Test + public void onDensityChanged_enabled_updateDimensionsAndResetWindowMagnification() { + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + Mockito.reset(mWindowManager); + Mockito.reset(mMirrorWindowControl); + }); + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_DENSITY); + }); + + verify(mResources, atLeastOnce()).getDimensionPixelSize(anyInt()); + verify(mSurfaceControlViewHosts.get(0)).release(); + verify(mMirrorWindowControl).destroyControl(); + verify(mSurfaceControlViewHosts.get(1)).setView(any(), any()); + verify(mMirrorWindowControl).showControl(); + } + + @Test + public void onDensityChanged_disabled_updateDimensions() { + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_DENSITY); + }); + + verify(mResources, atLeastOnce()).getDimensionPixelSize(anyInt()); + } + + @Test + public void initializeA11yNode_enabled_expectedValues() { + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(2.5f, Float.NaN, + Float.NaN); + }); + final View mirrorView = mSurfaceControlViewHost.getView(); + assertNotNull(mirrorView); + final AccessibilityNodeInfo nodeInfo = new AccessibilityNodeInfo(); + + mirrorView.onInitializeAccessibilityNodeInfo(nodeInfo); + + assertNotNull(nodeInfo.getContentDescription()); + assertThat(nodeInfo.getStateDescription().toString(), containsString("250")); + assertThat(nodeInfo.getActionList(), + hasItems(new AccessibilityAction(R.id.accessibility_action_zoom_in, null), + new AccessibilityAction(R.id.accessibility_action_zoom_out, null), + new AccessibilityAction(R.id.accessibility_action_move_right, null), + new AccessibilityAction(R.id.accessibility_action_move_left, null), + new AccessibilityAction(R.id.accessibility_action_move_down, null), + new AccessibilityAction(R.id.accessibility_action_move_up, null))); + } + + @Test + public void performA11yActions_visible_expectedResults() { + final int displayId = mContext.getDisplayId(); + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(1.5f, Float.NaN, + Float.NaN); + }); + + final View mirrorView = mSurfaceControlViewHost.getView(); + assertTrue( + mirrorView.performAccessibilityAction(R.id.accessibility_action_zoom_out, null)); + // Minimum scale is 1.0. + verify(mWindowMagnifierCallback).onPerformScaleAction( + eq(displayId), /* scale= */ eq(1.0f), /* updatePersistence= */ eq(true)); + + assertTrue(mirrorView.performAccessibilityAction(R.id.accessibility_action_zoom_in, null)); + verify(mWindowMagnifierCallback).onPerformScaleAction( + eq(displayId), /* scale= */ eq(2.5f), /* updatePersistence= */ eq(true)); + + // TODO: Verify the final state when the mirror surface is visible. + assertTrue(mirrorView.performAccessibilityAction(R.id.accessibility_action_move_up, null)); + assertTrue( + mirrorView.performAccessibilityAction(R.id.accessibility_action_move_down, null)); + assertTrue( + mirrorView.performAccessibilityAction(R.id.accessibility_action_move_right, null)); + assertTrue( + mirrorView.performAccessibilityAction(R.id.accessibility_action_move_left, null)); + verify(mWindowMagnifierCallback, times(4)).onMove(eq(displayId)); + + assertTrue(mirrorView.performAccessibilityAction( + AccessibilityAction.ACTION_CLICK.getId(), null)); + verify(mWindowMagnifierCallback).onClickSettingsButton(eq(displayId)); + } + + @Test + public void performA11yActions_visible_notifyAccessibilityActionPerformed() { + final int displayId = mContext.getDisplayId(); + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(2.5f, Float.NaN, + Float.NaN); + }); + + final View mirrorView = mSurfaceControlViewHost.getView(); + mirrorView.performAccessibilityAction(R.id.accessibility_action_move_up, null); + + verify(mWindowMagnifierCallback).onAccessibilityActionPerformed(eq(displayId)); + } + + @Test + public void windowMagnifierEditMode_performA11yClickAction_exitEditMode() { + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + mWindowMagnificationController.setEditMagnifierSizeMode(true); + }); + + View closeButton = getInternalView(R.id.close_button); + View bottomRightCorner = getInternalView(R.id.bottom_right_corner); + View bottomLeftCorner = getInternalView(R.id.bottom_left_corner); + View topRightCorner = getInternalView(R.id.top_right_corner); + View topLeftCorner = getInternalView(R.id.top_left_corner); + + assertEquals(View.VISIBLE, closeButton.getVisibility()); + assertEquals(View.VISIBLE, bottomRightCorner.getVisibility()); + assertEquals(View.VISIBLE, bottomLeftCorner.getVisibility()); + assertEquals(View.VISIBLE, topRightCorner.getVisibility()); + assertEquals(View.VISIBLE, topLeftCorner.getVisibility()); + + final View mirrorView = mSurfaceControlViewHost.getView(); + mInstrumentation.runOnMainSync(() -> + mirrorView.performAccessibilityAction(AccessibilityAction.ACTION_CLICK.getId(), + null)); + + assertEquals(View.GONE, closeButton.getVisibility()); + assertEquals(View.GONE, bottomRightCorner.getVisibility()); + assertEquals(View.GONE, bottomLeftCorner.getVisibility()); + assertEquals(View.GONE, topRightCorner.getVisibility()); + assertEquals(View.GONE, topLeftCorner.getVisibility()); + } + + @Test + + public void windowWidthIsNotMax_performA11yActionIncreaseWidth_windowWidthIncreased() { + final Rect windowBounds = mWindowManager.getCurrentWindowMetrics().getBounds(); + final int startingWidth = (int) (windowBounds.width() * 0.8); + final int startingHeight = (int) (windowBounds.height() * 0.8); + final float changeWindowSizeAmount = mContext.getResources().getFraction( + R.fraction.magnification_resize_window_size_amount, + /* base= */ 1, + /* pbase= */ 1); + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + mWindowMagnificationController.setWindowSize(startingWidth, startingHeight); + mWindowMagnificationController.setEditMagnifierSizeMode(true); + }); + + final View mirrorView = mSurfaceControlViewHost.getView(); + final AtomicInteger actualWindowHeight = new AtomicInteger(); + final AtomicInteger actualWindowWidth = new AtomicInteger(); + + mInstrumentation.runOnMainSync( + () -> { + mirrorView.performAccessibilityAction( + R.id.accessibility_action_increase_window_width, null); + actualWindowHeight.set( + mSurfaceControlViewHost.getView().getLayoutParams().height); + actualWindowWidth.set( + mSurfaceControlViewHost.getView().getLayoutParams().width); + }); + + final int mirrorSurfaceMargin = mResources.getDimensionPixelSize( + R.dimen.magnification_mirror_surface_margin); + // Window width includes the magnifier frame and the margin. Increasing the window size + // will be increasing the amount of the frame size only. + int newWindowWidth = + (int) ((startingWidth - 2 * mirrorSurfaceMargin) * (1 + changeWindowSizeAmount)) + + 2 * mirrorSurfaceMargin; + assertEquals(newWindowWidth, actualWindowWidth.get()); + assertEquals(startingHeight, actualWindowHeight.get()); + } + + @Test + public void windowHeightIsNotMax_performA11yActionIncreaseHeight_windowHeightIncreased() { + final Rect windowBounds = mWindowManager.getCurrentWindowMetrics().getBounds(); + final int startingWidth = (int) (windowBounds.width() * 0.8); + final int startingHeight = (int) (windowBounds.height() * 0.8); + final float changeWindowSizeAmount = mContext.getResources().getFraction( + R.fraction.magnification_resize_window_size_amount, + /* base= */ 1, + /* pbase= */ 1); + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + mWindowMagnificationController.setWindowSize(startingWidth, startingHeight); + mWindowMagnificationController.setEditMagnifierSizeMode(true); + }); + + final View mirrorView = mSurfaceControlViewHost.getView(); + final AtomicInteger actualWindowHeight = new AtomicInteger(); + final AtomicInteger actualWindowWidth = new AtomicInteger(); + + mInstrumentation.runOnMainSync( + () -> { + mirrorView.performAccessibilityAction( + R.id.accessibility_action_increase_window_height, null); + actualWindowHeight.set( + mSurfaceControlViewHost.getView().getLayoutParams().height); + actualWindowWidth.set( + mSurfaceControlViewHost.getView().getLayoutParams().width); + }); + + final int mirrorSurfaceMargin = mResources.getDimensionPixelSize( + R.dimen.magnification_mirror_surface_margin); + // Window height includes the magnifier frame and the margin. Increasing the window size + // will be increasing the amount of the frame size only. + int newWindowHeight = + (int) ((startingHeight - 2 * mirrorSurfaceMargin) * (1 + changeWindowSizeAmount)) + + 2 * mirrorSurfaceMargin; + assertEquals(startingWidth, actualWindowWidth.get()); + assertEquals(newWindowHeight, actualWindowHeight.get()); + } + + @Test + public void windowWidthIsMax_noIncreaseWindowWidthA11yAction() { + final Rect windowBounds = mWindowManager.getCurrentWindowMetrics().getBounds(); + final int startingWidth = windowBounds.width(); + final int startingHeight = windowBounds.height(); + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + mWindowMagnificationController.setWindowSize(startingWidth, startingHeight); + mWindowMagnificationController.setEditMagnifierSizeMode(true); + }); + + final View mirrorView = mSurfaceControlViewHost.getView(); + final AccessibilityNodeInfo accessibilityNodeInfo = + mirrorView.createAccessibilityNodeInfo(); + assertFalse(accessibilityNodeInfo.getActionList().contains( + new AccessibilityAction(R.id.accessibility_action_increase_window_width, null))); + } + + @Test + public void windowHeightIsMax_noIncreaseWindowHeightA11yAction() { + final Rect windowBounds = mWindowManager.getCurrentWindowMetrics().getBounds(); + final int startingWidth = windowBounds.width(); + final int startingHeight = windowBounds.height(); + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + mWindowMagnificationController.setWindowSize(startingWidth, startingHeight); + mWindowMagnificationController.setEditMagnifierSizeMode(true); + }); + + final View mirrorView = mSurfaceControlViewHost.getView(); + final AccessibilityNodeInfo accessibilityNodeInfo = + mirrorView.createAccessibilityNodeInfo(); + assertFalse(accessibilityNodeInfo.getActionList().contains( + new AccessibilityAction(R.id.accessibility_action_increase_window_height, null))); + } + + @Test + public void windowWidthIsNotMin_performA11yActionDecreaseWidth_windowWidthDecreased() { + int mMinWindowSize = mResources.getDimensionPixelSize( + com.android.internal.R.dimen.accessibility_window_magnifier_min_size); + final int startingSize = (int) (mMinWindowSize * 1.1); + final float changeWindowSizeAmount = mContext.getResources().getFraction( + R.fraction.magnification_resize_window_size_amount, + /* base= */ 1, + /* pbase= */ 1); + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + mWindowMagnificationController.setWindowSize(startingSize, startingSize); + mWindowMagnificationController.setEditMagnifierSizeMode(true); + }); + + final View mirrorView = mSurfaceControlViewHost.getView(); + final AtomicInteger actualWindowHeight = new AtomicInteger(); + final AtomicInteger actualWindowWidth = new AtomicInteger(); + + mInstrumentation.runOnMainSync( + () -> { + mirrorView.performAccessibilityAction( + R.id.accessibility_action_decrease_window_width, null); + actualWindowHeight.set( + mSurfaceControlViewHost.getView().getLayoutParams().height); + actualWindowWidth.set( + mSurfaceControlViewHost.getView().getLayoutParams().width); + }); + + final int mirrorSurfaceMargin = mResources.getDimensionPixelSize( + R.dimen.magnification_mirror_surface_margin); + // Window width includes the magnifier frame and the margin. Decreasing the window size + // will be decreasing the amount of the frame size only. + int newWindowWidth = + (int) ((startingSize - 2 * mirrorSurfaceMargin) * (1 - changeWindowSizeAmount)) + + 2 * mirrorSurfaceMargin; + assertEquals(newWindowWidth, actualWindowWidth.get()); + assertEquals(startingSize, actualWindowHeight.get()); + } + + @Test + public void windowHeightIsNotMin_performA11yActionDecreaseHeight_windowHeightDecreased() { + int mMinWindowSize = mResources.getDimensionPixelSize( + com.android.internal.R.dimen.accessibility_window_magnifier_min_size); + final int startingSize = (int) (mMinWindowSize * 1.1); + final float changeWindowSizeAmount = mContext.getResources().getFraction( + R.fraction.magnification_resize_window_size_amount, + /* base= */ 1, + /* pbase= */ 1); + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + mWindowMagnificationController.setWindowSize(startingSize, startingSize); + mWindowMagnificationController.setEditMagnifierSizeMode(true); + }); + + final View mirrorView = mSurfaceControlViewHost.getView(); + final AtomicInteger actualWindowHeight = new AtomicInteger(); + final AtomicInteger actualWindowWidth = new AtomicInteger(); + + mInstrumentation.runOnMainSync( + () -> { + mirrorView.performAccessibilityAction( + R.id.accessibility_action_decrease_window_height, null); + actualWindowHeight.set( + mSurfaceControlViewHost.getView().getLayoutParams().height); + actualWindowWidth.set( + mSurfaceControlViewHost.getView().getLayoutParams().width); + }); + + final int mirrorSurfaceMargin = mResources.getDimensionPixelSize( + R.dimen.magnification_mirror_surface_margin); + // Window height includes the magnifier frame and the margin. Decreasing the window size + // will be decreasing the amount of the frame size only. + int newWindowHeight = + (int) ((startingSize - 2 * mirrorSurfaceMargin) * (1 - changeWindowSizeAmount)) + + 2 * mirrorSurfaceMargin; + assertEquals(startingSize, actualWindowWidth.get()); + assertEquals(newWindowHeight, actualWindowHeight.get()); + } + + @Test + public void windowWidthIsMin_noDecreaseWindowWidthA11yAction() { + int mMinWindowSize = mResources.getDimensionPixelSize( + com.android.internal.R.dimen.accessibility_window_magnifier_min_size); + final int startingSize = mMinWindowSize; + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + mWindowMagnificationController.setWindowSize(startingSize, startingSize); + mWindowMagnificationController.setEditMagnifierSizeMode(true); + }); + + final View mirrorView = mSurfaceControlViewHost.getView(); + final AccessibilityNodeInfo accessibilityNodeInfo = + mirrorView.createAccessibilityNodeInfo(); + assertFalse(accessibilityNodeInfo.getActionList().contains( + new AccessibilityAction(R.id.accessibility_action_decrease_window_width, null))); + } + + @Test + public void windowHeightIsMin_noDecreaseWindowHeightA11yAction() { + int mMinWindowSize = mResources.getDimensionPixelSize( + com.android.internal.R.dimen.accessibility_window_magnifier_min_size); + final int startingSize = mMinWindowSize; + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + mWindowMagnificationController.setWindowSize(startingSize, startingSize); + mWindowMagnificationController.setEditMagnifierSizeMode(true); + }); + + final View mirrorView = mSurfaceControlViewHost.getView(); + final AccessibilityNodeInfo accessibilityNodeInfo = + mirrorView.createAccessibilityNodeInfo(); + assertFalse(accessibilityNodeInfo.getActionList().contains( + new AccessibilityAction(R.id.accessibility_action_decrease_window_height, null))); + } + + @Test + public void enableWindowMagnification_hasA11yWindowTitle() { + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + }); + + assertEquals(getContext().getResources().getString( + com.android.internal.R.string.android_system_label), getAccessibilityWindowTitle()); + } + + @Test + public void enableWindowMagnificationWithScaleLessThanOne_enabled_disabled() { + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + }); + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(0.9f, Float.NaN, + Float.NaN); + }); + + assertEquals(Float.NaN, mWindowMagnificationController.getScale(), 0); + } + + @Test + public void enableWindowMagnification_rotationIsChanged_updateRotationValue() { + // the config orientation should not be undefined, since it would cause config.diff + // returning 0 and thus the orientation changed would not be detected + assertNotEquals(ORIENTATION_UNDEFINED, mResources.getConfiguration().orientation); + + final Configuration config = mResources.getConfiguration(); + config.orientation = config.orientation == ORIENTATION_LANDSCAPE ? ORIENTATION_PORTRAIT + : ORIENTATION_LANDSCAPE; + final int newRotation = simulateRotateTheDevice(); + + mInstrumentation.runOnMainSync( + () -> mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, + Float.NaN, Float.NaN)); + + assertEquals(newRotation, mWindowMagnificationController.mRotation); + } + + @Test + public void enableWindowMagnification_registerComponentCallback() { + mInstrumentation.runOnMainSync( + () -> mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, + Float.NaN, + Float.NaN)); + + verify(mContext).registerComponentCallbacks(mWindowMagnificationController); + } + + @Test + public void onLocaleChanged_enabled_updateA11yWindowTitle() { + final String newA11yWindowTitle = "new a11y window title"; + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + }); + final TestableResources testableResources = getContext().getOrCreateTestableResources(); + testableResources.addOverride(com.android.internal.R.string.android_system_label, + newA11yWindowTitle); + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.onConfigurationChanged(ActivityInfo.CONFIG_LOCALE); + }); + + assertTrue(TextUtils.equals(newA11yWindowTitle, getAccessibilityWindowTitle())); + } + + @Ignore("it's flaky in presubmit but works in abtd, filter for now. b/305654925") + @Test + public void onSingleTap_enabled_scaleAnimates() { + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + }); + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.onSingleTap(mSpyView); + }); + + final View mirrorView = mSurfaceControlViewHost.getView(); + + final AtomicDouble maxScaleX = new AtomicDouble(); + advanceTimeBy(mWaitBounceEffectDuration, /* runnableOnEachRefresh= */ () -> { + // For some reason the fancy way doesn't compile... + // maxScaleX.getAndAccumulate(mirrorView.getScaleX(), Math::max); + final double oldMax = maxScaleX.get(); + final double newMax = Math.max(mirrorView.getScaleX(), oldMax); + assertTrue(maxScaleX.compareAndSet(oldMax, newMax)); + }); + + assertTrue(maxScaleX.get() > 1.0); + } + + @Test + public void moveWindowMagnificationToTheBottom_enabledWithGestureInset_overlapFlagIsTrue() { + final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds(); + setSystemGestureInsets(); + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, Float.NaN, + Float.NaN); + }); + + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.moveWindowMagnifier(0, bounds.height()); + }); + + ReferenceTestUtils.waitForCondition(() -> hasMagnificationOverlapFlag()); + } + + @Test + public void moveWindowMagnificationToRightEdge_dragHandleMovesToLeftAndUpdatesTapExcludeRegion() + throws RemoteException { + final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds(); + setSystemGestureInsets(); + mInstrumentation.runOnMainSync( + () -> { + mWindowMagnificationController.enableWindowMagnificationInternal( + Float.NaN, Float.NaN, Float.NaN); + }); + // Wait for Region updated. + waitForIdleSync(); + + mInstrumentation.runOnMainSync( + () -> { + mWindowMagnificationController.moveWindowMagnifier(bounds.width(), 0); + }); + // Wait for Region updated. + waitForIdleSync(); + + AttachedSurfaceControl viewRoot = mSurfaceControlViewHost.getRootSurfaceControl(); + // Verifying two times in: (1) enable window magnification (2) reposition drag handle + verify(viewRoot, times(2)).setTouchableRegion(any()); + + View dragButton = getInternalView(R.id.drag_handle); + FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) dragButton.getLayoutParams(); + assertEquals(Gravity.BOTTOM | Gravity.LEFT, params.gravity); + } + + @Test + public void moveWindowMagnificationToLeftEdge_dragHandleMovesToRightAndUpdatesTapExcludeRegion() + throws RemoteException { + final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds(); + setSystemGestureInsets(); + mInstrumentation.runOnMainSync( + () -> { + mWindowMagnificationController.enableWindowMagnificationInternal( + Float.NaN, Float.NaN, Float.NaN); + }); + // Wait for Region updated. + waitForIdleSync(); + + mInstrumentation.runOnMainSync( + () -> { + mWindowMagnificationController.moveWindowMagnifier(-bounds.width(), 0); + }); + // Wait for Region updated. + waitForIdleSync(); + + AttachedSurfaceControl viewRoot = mSurfaceControlViewHost.getRootSurfaceControl(); + // Verifying one times in: (1) enable window magnification + verify(viewRoot).setTouchableRegion(any()); + + View dragButton = getInternalView(R.id.drag_handle); + FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) dragButton.getLayoutParams(); + assertEquals(Gravity.BOTTOM | Gravity.RIGHT, params.gravity); + } + + @Test + public void setMinimumWindowSize_enabled_expectedWindowSize() { + final int minimumWindowSize = mResources.getDimensionPixelSize( + com.android.internal.R.dimen.accessibility_window_magnifier_min_size); + final int expectedWindowHeight = minimumWindowSize; + final int expectedWindowWidth = minimumWindowSize; + mInstrumentation.runOnMainSync( + () -> mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, + Float.NaN, Float.NaN)); + + final AtomicInteger actualWindowHeight = new AtomicInteger(); + final AtomicInteger actualWindowWidth = new AtomicInteger(); + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.setWindowSize(expectedWindowWidth, expectedWindowHeight); + actualWindowHeight.set(mSurfaceControlViewHost.getView().getLayoutParams().height); + actualWindowWidth.set(mSurfaceControlViewHost.getView().getLayoutParams().width); + + }); + + assertEquals(expectedWindowHeight, actualWindowHeight.get()); + assertEquals(expectedWindowWidth, actualWindowWidth.get()); + } + + @Test + public void setMinimumWindowSizeThenEnable_expectedWindowSize() { + final int minimumWindowSize = mResources.getDimensionPixelSize( + com.android.internal.R.dimen.accessibility_window_magnifier_min_size); + final int expectedWindowHeight = minimumWindowSize; + final int expectedWindowWidth = minimumWindowSize; + + final AtomicInteger actualWindowHeight = new AtomicInteger(); + final AtomicInteger actualWindowWidth = new AtomicInteger(); + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.setWindowSize(expectedWindowWidth, expectedWindowHeight); + mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, + Float.NaN, Float.NaN); + actualWindowHeight.set(mSurfaceControlViewHost.getView().getLayoutParams().height); + actualWindowWidth.set(mSurfaceControlViewHost.getView().getLayoutParams().width); + }); + + assertEquals(expectedWindowHeight, actualWindowHeight.get()); + assertEquals(expectedWindowWidth, actualWindowWidth.get()); + } + + @Test + public void setWindowSizeLessThanMin_enabled_minimumWindowSize() { + final int minimumWindowSize = mResources.getDimensionPixelSize( + com.android.internal.R.dimen.accessibility_window_magnifier_min_size); + mInstrumentation.runOnMainSync( + () -> mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, + Float.NaN, Float.NaN)); + + final AtomicInteger actualWindowHeight = new AtomicInteger(); + final AtomicInteger actualWindowWidth = new AtomicInteger(); + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.setWindowSize(minimumWindowSize - 10, + minimumWindowSize - 10); + actualWindowHeight.set(mSurfaceControlViewHost.getView().getLayoutParams().height); + actualWindowWidth.set(mSurfaceControlViewHost.getView().getLayoutParams().width); + }); + + assertEquals(minimumWindowSize, actualWindowHeight.get()); + assertEquals(minimumWindowSize, actualWindowWidth.get()); + } + + @Test + public void setWindowSizeLargerThanScreenSize_enabled_windowSizeIsScreenSize() { + final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds(); + mInstrumentation.runOnMainSync( + () -> mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, + Float.NaN, Float.NaN)); + + final AtomicInteger actualWindowHeight = new AtomicInteger(); + final AtomicInteger actualWindowWidth = new AtomicInteger(); + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.setWindowSize(bounds.width() + 10, bounds.height() + 10); + actualWindowHeight.set(mSurfaceControlViewHost.getView().getLayoutParams().height); + actualWindowWidth.set(mSurfaceControlViewHost.getView().getLayoutParams().width); + }); + + assertEquals(bounds.height(), actualWindowHeight.get()); + assertEquals(bounds.width(), actualWindowWidth.get()); + } + + @Test + public void changeMagnificationSize_expectedWindowSize() { + final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds(); + + final float magnificationScaleLarge = 2.5f; + final int initSize = Math.min(bounds.width(), bounds.height()) / 3; + final int magnificationSize = (int) (initSize * magnificationScaleLarge) + - (int) (initSize * magnificationScaleLarge) % 2; + + final int expectedWindowHeight = magnificationSize; + final int expectedWindowWidth = magnificationSize; + + mInstrumentation.runOnMainSync( + () -> + mWindowMagnificationController.enableWindowMagnificationInternal( + Float.NaN, Float.NaN, Float.NaN)); + + final AtomicInteger actualWindowHeight = new AtomicInteger(); + final AtomicInteger actualWindowWidth = new AtomicInteger(); + mInstrumentation.runOnMainSync( + () -> { + mWindowMagnificationController.changeMagnificationSize( + WindowMagnificationSettings.MagnificationSize.LARGE); + actualWindowHeight.set( + mSurfaceControlViewHost.getView().getLayoutParams().height); + actualWindowWidth.set( + mSurfaceControlViewHost.getView().getLayoutParams().width); + }); + + assertEquals(expectedWindowHeight, actualWindowHeight.get()); + assertEquals(expectedWindowWidth, actualWindowWidth.get()); + } + + @Test + public void editModeOnDragCorner_resizesWindow() { + final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds(); + + final int startingSize = (int) (bounds.width() / 2); + + mInstrumentation.runOnMainSync( + () -> + mWindowMagnificationController.enableWindowMagnificationInternal( + Float.NaN, Float.NaN, Float.NaN)); + + final AtomicInteger actualWindowHeight = new AtomicInteger(); + final AtomicInteger actualWindowWidth = new AtomicInteger(); + + mInstrumentation.runOnMainSync( + () -> { + mWindowMagnificationController.setWindowSize(startingSize, startingSize); + mWindowMagnificationController.setEditMagnifierSizeMode(true); + }); + + waitForIdleSync(); + + mInstrumentation.runOnMainSync( + () -> { + mWindowMagnificationController + .onDrag(getInternalView(R.id.bottom_right_corner), 2f, 1f); + actualWindowHeight.set( + mSurfaceControlViewHost.getView().getLayoutParams().height); + actualWindowWidth.set( + mSurfaceControlViewHost.getView().getLayoutParams().width); + }); + + assertEquals(startingSize + 1, actualWindowHeight.get()); + assertEquals(startingSize + 2, actualWindowWidth.get()); + } + + @Test + public void editModeOnDragEdge_resizesWindowInOnlyOneDirection() { + final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds(); + + final int startingSize = (int) (bounds.width() / 2f); + + mInstrumentation.runOnMainSync( + () -> + mWindowMagnificationController.enableWindowMagnificationInternal( + Float.NaN, Float.NaN, Float.NaN)); + + final AtomicInteger actualWindowHeight = new AtomicInteger(); + final AtomicInteger actualWindowWidth = new AtomicInteger(); + + mInstrumentation.runOnMainSync( + () -> { + mWindowMagnificationController.setWindowSize(startingSize, startingSize); + mWindowMagnificationController.setEditMagnifierSizeMode(true); + mWindowMagnificationController + .onDrag(getInternalView(R.id.bottom_handle), 2f, 1f); + actualWindowHeight.set( + mSurfaceControlViewHost.getView().getLayoutParams().height); + actualWindowWidth.set( + mSurfaceControlViewHost.getView().getLayoutParams().width); + }); + assertEquals(startingSize + 1, actualWindowHeight.get()); + assertEquals(startingSize, actualWindowWidth.get()); + } + + @Test + public void setWindowCenterOutOfScreen_enabled_magnificationCenterIsInsideTheScreen() { + + final int minimumWindowSize = mResources.getDimensionPixelSize( + com.android.internal.R.dimen.accessibility_window_magnifier_min_size); + final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds(); + mInstrumentation.runOnMainSync( + () -> mWindowMagnificationController.enableWindowMagnificationInternal(Float.NaN, + Float.NaN, Float.NaN)); + + final AtomicInteger magnificationCenterX = new AtomicInteger(); + final AtomicInteger magnificationCenterY = new AtomicInteger(); + mInstrumentation.runOnMainSync(() -> { + mWindowMagnificationController.setWindowSizeAndCenter(minimumWindowSize, + minimumWindowSize, bounds.right, bounds.bottom); + magnificationCenterX.set((int) mWindowMagnificationController.getCenterX()); + magnificationCenterY.set((int) mWindowMagnificationController.getCenterY()); + }); + + assertTrue(magnificationCenterX.get() < bounds.right); + assertTrue(magnificationCenterY.get() < bounds.bottom); + } + + @Test + public void performSingleTap_DragHandle() { + final Rect bounds = mWindowManager.getCurrentWindowMetrics().getBounds(); + mInstrumentation.runOnMainSync( + () -> { + mWindowMagnificationController.enableWindowMagnificationInternal( + 1.5f, bounds.centerX(), bounds.centerY()); + }); + View dragButton = getInternalView(R.id.drag_handle); + + // Perform a single-tap + final long downTime = SystemClock.uptimeMillis(); + dragButton.dispatchTouchEvent( + obtainMotionEvent(downTime, 0, ACTION_DOWN, 100, 100)); + dragButton.dispatchTouchEvent( + obtainMotionEvent(downTime, downTime, ACTION_UP, 100, 100)); + + verify(mSurfaceControlViewHost).setView(any(View.class), any()); + } + + private <T extends View> T getInternalView(@IdRes int idRes) { + View mirrorView = mSurfaceControlViewHost.getView(); + T view = mirrorView.findViewById(idRes); + assertNotNull(view); + return view; + } + + private MotionEvent obtainMotionEvent(long downTime, long eventTime, int action, float x, + float y) { + return mMotionEventHelper.obtainMotionEvent(downTime, eventTime, action, x, y); + } + + private CharSequence getAccessibilityWindowTitle() { + final View mirrorView = mSurfaceControlViewHost.getView(); + if (mirrorView == null) { + return null; + } + WindowManager.LayoutParams layoutParams = + (WindowManager.LayoutParams) mirrorView.getLayoutParams(); + return layoutParams.accessibilityTitle; + } + + private boolean hasMagnificationOverlapFlag() { + return (mSysUiState.getFlags() & SYSUI_STATE_MAGNIFICATION_OVERLAP) != 0; + } + + private void setSystemGestureInsets() { + final WindowInsets testInsets = new WindowInsets.Builder() + .setInsets(systemGestures(), Insets.of(0, 0, 0, 10)) + .build(); + mWindowManager.setWindowInsets(testInsets); + } + + private int updateMirrorSurfaceMarginDimension() { + return mContext.getResources().getDimensionPixelSize( + R.dimen.magnification_mirror_surface_margin); + } + + @Surface.Rotation + private int simulateRotateTheDevice() { + final Display display = Mockito.spy(mContext.getDisplay()); + final int currentRotation = display.getRotation(); + final int newRotation = (currentRotation + 1) % 4; + when(display.getRotation()).thenReturn(newRotation); + when(mContext.getDisplay()).thenReturn(display); + return newRotation; + } + + // advance time based on the device frame refresh rate + private void advanceTimeBy(long timeDelta) { + advanceTimeBy(timeDelta, /* runnableOnEachRefresh= */ null); + } + + // advance time based on the device frame refresh rate, and trigger runnable on each refresh + private void advanceTimeBy(long timeDelta, @Nullable Runnable runnableOnEachRefresh) { + final float frameRate = mContext.getDisplay().getRefreshRate(); + final int timeSlot = (int) (1000 / frameRate); + int round = (int) Math.ceil((double) timeDelta / timeSlot); + for (; round >= 0; round--) { + mInstrumentation.runOnMainSync(() -> { + mAnimatorTestRule.advanceTimeBy(timeSlot); + if (runnableOnEachRefresh != null) { + runnableOnEachRefresh.run(); + } + }); + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt index e93ad0be3e85..28127186706b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt @@ -1895,6 +1895,37 @@ class KeyguardTransitionScenariosTest : SysuiTestCase() { coroutineContext.cancelChildren() } + @Test + fun glanceableHubToDreaming() = + testScope.runTest { + // GIVEN a device that is not dreaming or dozing + keyguardRepository.setDreamingWithOverlay(false) + keyguardRepository.setDozeTransitionModel( + DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH) + ) + runCurrent() + + // GIVEN a prior transition has run to GLANCEABLE_HUB + runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.GLANCEABLE_HUB) + + // WHEN the device begins to dream + keyguardRepository.setDreamingWithOverlay(true) + advanceUntilIdle() + + val info = + withArgCaptor<TransitionInfo> { + verify(transitionRepository).startTransition(capture()) + } + // THEN a transition to DREAMING should occur + assertThat(info.ownerName) + .isEqualTo(FromGlanceableHubTransitionInteractor::class.simpleName) + assertThat(info.from).isEqualTo(KeyguardState.GLANCEABLE_HUB) + assertThat(info.to).isEqualTo(KeyguardState.DREAMING) + assertThat(info.animator).isNotNull() + + coroutineContext.cancelChildren() + } + private fun createKeyguardInteractor(): KeyguardInteractor { return KeyguardInteractorFactory.create( featureFlags = featureFlags, diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsTest.kt index 75994da6c934..ad2ae8b41af9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsTest.kt @@ -19,7 +19,7 @@ package com.android.systemui.keyguard.ui.viewmodel import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase -import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor +import com.android.systemui.bouncer.domain.interactor.mockPrimaryBouncerInteractor import com.android.systemui.coroutines.collectValues import com.android.systemui.flags.Flags import com.android.systemui.flags.fakeFeatureFlagsClassic @@ -65,7 +65,7 @@ class BouncerToGoneFlowsTest : SysuiTestCase() { private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository private val shadeRepository = kosmos.shadeRepository private val sysuiStatusBarStateController = kosmos.sysuiStatusBarStateController - private val primaryBouncerInteractor = kosmos.primaryBouncerInteractor + private val primaryBouncerInteractor = kosmos.mockPrimaryBouncerInteractor private val underTest = kosmos.bouncerToGoneFlows @Before diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java index a2aed988a423..9ce77e58a5f2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java @@ -28,10 +28,10 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import android.app.ActivityOptions.LaunchCookie; import android.app.Notification; import android.app.NotificationManager; import android.content.Intent; -import android.os.Binder; import android.os.Handler; import android.os.RemoteException; import android.os.UserHandle; @@ -146,7 +146,7 @@ public class RecordingServiceTest extends SysuiTestCase { @Test public void testLogStartPartialRecording() { - MediaProjectionCaptureTarget target = new MediaProjectionCaptureTarget(new Binder()); + MediaProjectionCaptureTarget target = new MediaProjectionCaptureTarget(new LaunchCookie()); Intent startIntent = RecordingService.getStartIntent(mContext, 0, 0, false, target); mRecordingService.onStartCommand(startIntent, 0, 0); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java index 260bef8e98ce..5da3a569f91a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/CommandQueueTest.java @@ -25,6 +25,7 @@ import static org.mockito.Matchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.verifyZeroInteractions; import android.content.ComponentName; import android.graphics.Rect; @@ -32,11 +33,14 @@ import android.hardware.biometrics.IBiometricSysuiReceiver; import android.hardware.biometrics.PromptInfo; import android.hardware.fingerprint.IUdfpsRefreshRateRequestCallback; import android.os.Bundle; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.view.KeyEvent; import android.view.WindowInsets; import android.view.WindowInsets.Type.InsetsType; import android.view.WindowInsetsController.Appearance; import android.view.WindowInsetsController.Behavior; +import android.view.accessibility.Flags; import androidx.test.filters.SmallTest; @@ -365,14 +369,50 @@ public class CommandQueueTest extends SysuiTestCase { } @Test - public void testAddQsTile() { + @DisableFlags(Flags.FLAG_A11Y_QS_SHORTCUT) + public void addQsTile_withA11yQsShortcutFlagOff() { ComponentName c = new ComponentName("testpkg", "testcls"); + mCommandQueue.addQsTile(c); waitForIdleSync(); + verify(mCallbacks).addQsTile(eq(c)); } @Test + @DisableFlags(Flags.FLAG_A11Y_QS_SHORTCUT) + public void addQsTileToFrontOrEnd_withA11yQsShortcutFlagOff_doNothing() { + ComponentName c = new ComponentName("testpkg", "testcls"); + + mCommandQueue.addQsTileToFrontOrEnd(c, true); + waitForIdleSync(); + + verifyZeroInteractions(mCallbacks); + } + + @Test + @EnableFlags(Flags.FLAG_A11Y_QS_SHORTCUT) + public void addQsTile_withA11yQsShortcutFlagOn() { + ComponentName c = new ComponentName("testpkg", "testcls"); + + mCommandQueue.addQsTile(c); + waitForIdleSync(); + + verify(mCallbacks).addQsTileToFrontOrEnd(eq(c), eq(false)); + } + + @Test + @EnableFlags(Flags.FLAG_A11Y_QS_SHORTCUT) + public void addQsTileAtTheEnd_withA11yQsShortcutFlagOn() { + ComponentName c = new ComponentName("testpkg", "testcls"); + + mCommandQueue.addQsTileToFrontOrEnd(c, true); + waitForIdleSync(); + + verify(mCallbacks).addQsTileToFrontOrEnd(eq(c), eq(true)); + } + + @Test public void testRemoveQsTile() { ComponentName c = new ComponentName("testpkg", "testcls"); mCommandQueue.remQsTile(c); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java index 912c27d854fa..ea4ae17de5e3 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java @@ -26,6 +26,7 @@ import static org.mockito.Mockito.when; import android.app.ActivityManager; import android.app.StatusBarManager; +import android.content.ComponentName; import android.os.PowerManager; import android.os.UserHandle; import android.os.Vibrator; @@ -190,4 +191,31 @@ public class CentralSurfacesCommandQueueCallbacksTest extends SysuiTestCase { HapticFeedbackConstants.GESTURE_START ); } + + @Test + public void addQsTile_delegateCallToQsHost() { + ComponentName c = new ComponentName("testpkg", "testcls"); + + mSbcqCallbacks.addQsTile(c); + + verify(mQSHost).addTile(c); + } + + @Test + public void addQsTileToFrontOrEnd_toTheEnd_delegateCallToQsHost() { + ComponentName c = new ComponentName("testpkg", "testcls"); + + mSbcqCallbacks.addQsTileToFrontOrEnd(c, true); + + verify(mQSHost).addTile(c, true); + } + + @Test + public void addQsTileToFrontOrEnd_toTheFront_delegateCallToQsHost() { + ComponentName c = new ComponentName("testpkg", "testcls"); + + mSbcqCallbacks.addQsTileToFrontOrEnd(c, false); + + verify(mQSHost).addTile(c, false); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java index 9c4984ee4769..849a13be58ff 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesImplTest.java @@ -41,6 +41,8 @@ import static org.mockito.Mockito.when; import static java.util.Collections.emptySet; +import static kotlinx.coroutines.flow.FlowKt.flowOf; + import android.app.ActivityManager; import android.app.IWallpaperManager; import android.app.WallpaperManager; @@ -77,7 +79,6 @@ import com.android.internal.logging.nano.MetricsProto.MetricsEvent; import com.android.internal.logging.testing.FakeMetricsLogger; import com.android.internal.statusbar.IStatusBarService; import com.android.keyguard.KeyguardUpdateMonitor; -import com.android.keyguard.TestScopeProvider; import com.android.keyguard.ViewMediatorCallback; import com.android.systemui.InitController; import com.android.systemui.SysuiTestCase; @@ -92,6 +93,10 @@ import com.android.systemui.charging.WiredChargingRippleController; import com.android.systemui.classifier.FalsingCollectorFake; import com.android.systemui.classifier.FalsingManagerFake; import com.android.systemui.colorextraction.SysuiColorExtractor; +import com.android.systemui.communal.data.repository.CommunalRepository; +import com.android.systemui.communal.domain.interactor.CommunalInteractor; +import com.android.systemui.communal.shared.model.CommunalSceneKey; +import com.android.systemui.communal.shared.model.ObservableCommunalTransitionState; import com.android.systemui.demomode.DemoModeController; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FakeFeatureFlags; @@ -102,6 +107,7 @@ import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.keyguard.ScreenLifecycle; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.keyguard.ui.viewmodel.LightRevealScrimViewModel; +import com.android.systemui.kosmos.KosmosJavaAdapter; import com.android.systemui.log.LogBuffer; import com.android.systemui.navigationbar.NavigationBarController; import com.android.systemui.notetask.NoteTaskController; @@ -195,6 +201,8 @@ import java.util.Optional; import javax.inject.Provider; +import kotlinx.coroutines.test.TestScope; + @SmallTest @RunWith(AndroidTestingRunner.class) @RunWithLooper(setAsMainLooper = true) @@ -203,11 +211,17 @@ public class CentralSurfacesImplTest extends SysuiTestCase { private static final int FOLD_STATE_FOLDED = 0; private static final int FOLD_STATE_UNFOLDED = 1; + private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this); + private CentralSurfacesImpl mCentralSurfaces; private FakeMetricsLogger mMetricsLogger; private PowerManager mPowerManager; private VisualInterruptionDecisionProvider mVisualInterruptionDecisionProvider; + + private final TestScope mTestScope = mKosmos.getTestScope(); + private final CommunalInteractor mCommunalInteractor = mKosmos.getCommunalInteractor(); + private final CommunalRepository mCommunalRepository = mKosmos.getCommunalRepository(); @Mock private NotificationsController mNotificationsController; @Mock private LightBarController mLightBarController; @Mock private StatusBarKeyguardViewManager mStatusBarKeyguardViewManager; @@ -461,7 +475,7 @@ public class CentralSurfacesImplTest extends SysuiTestCase { new DisplayMetrics(), mMetricsLogger, mShadeLogger, - new JavaAdapter(TestScopeProvider.getTestScope()), + new JavaAdapter(mTestScope), mUiBgExecutor, mNotificationPanelViewController, mNotificationMediaManager, @@ -473,6 +487,7 @@ public class CentralSurfacesImplTest extends SysuiTestCase { mScreenLifecycle, mWakefulnessLifecycle, mPowerInteractor, + mCommunalInteractor, mStatusBarStateController, Optional.of(mBubbles), () -> mNoteTaskController, @@ -821,6 +836,25 @@ public class CentralSurfacesImplTest extends SysuiTestCase { } @Test + public void testEnteringGlanceableHub_updatesScrim() { + // Transition to the glanceable hub. + mCommunalRepository.setTransitionState(flowOf(new ObservableCommunalTransitionState.Idle( + CommunalSceneKey.Communal.INSTANCE))); + mTestScope.getTestScheduler().runCurrent(); + + // ScrimState also transitions. + verify(mScrimController).transitionTo(ScrimState.GLANCEABLE_HUB); + + // Transition away from the glanceable hub. + mCommunalRepository.setTransitionState(flowOf(new ObservableCommunalTransitionState.Idle( + CommunalSceneKey.Blank.INSTANCE))); + mTestScope.getTestScheduler().runCurrent(); + + // ScrimState goes back to UNLOCKED. + verify(mScrimController).transitionTo(eq(ScrimState.UNLOCKED), any()); + } + + @Test public void testShowKeyguardImplementation_setsState() { when(mLockscreenUserManager.getCurrentProfiles()).thenReturn(new SparseArray<>()); diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java index 423cc8478dda..3bde6e36a51f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/ScrimControllerTest.java @@ -51,6 +51,7 @@ import android.content.res.TypedArray; import android.graphics.Color; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; +import android.testing.ViewUtils; import android.util.MathUtils; import android.view.View; @@ -59,13 +60,13 @@ import androidx.test.filters.SmallTest; import com.android.internal.colorextraction.ColorExtractor.GradientColors; import com.android.keyguard.BouncerPanelExpansionCalculator; import com.android.keyguard.KeyguardUpdateMonitor; -import com.android.keyguard.TestScopeProvider; import com.android.systemui.DejankUtils; import com.android.systemui.SysuiTestCase; import com.android.systemui.animation.ShadeInterpolation; import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants; import com.android.systemui.dock.DockManager; import com.android.systemui.keyguard.KeyguardUnlockAnimationController; +import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor; import com.android.systemui.keyguard.shared.model.KeyguardState; @@ -73,6 +74,7 @@ import com.android.systemui.keyguard.shared.model.TransitionState; import com.android.systemui.keyguard.shared.model.TransitionStep; import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerToGoneTransitionViewModel; import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransitionViewModel; +import com.android.systemui.kosmos.KosmosJavaAdapter; import com.android.systemui.scrim.ScrimView; import com.android.systemui.shade.transition.LargeScreenShadeInterpolator; import com.android.systemui.shade.transition.LinearLargeScreenShadeInterpolator; @@ -103,7 +105,6 @@ import java.util.HashMap; import java.util.HashSet; import java.util.Map; -import kotlinx.coroutines.CoroutineDispatcher; import kotlinx.coroutines.test.TestScope; @RunWith(AndroidTestingRunner.class) @@ -112,13 +113,14 @@ import kotlinx.coroutines.test.TestScope; public class ScrimControllerTest extends SysuiTestCase { @Rule public Expect mExpect = Expect.create(); + private final KosmosJavaAdapter mKosmos = new KosmosJavaAdapter(this); private final FakeConfigurationController mConfigurationController = new FakeConfigurationController(); private final LargeScreenShadeInterpolator mLinearLargeScreenShadeInterpolator = new LinearLargeScreenShadeInterpolator(); - private final TestScope mTestScope = TestScopeProvider.getTestScope(); + private final TestScope mTestScope = mKosmos.getTestScope(); private final JavaAdapter mJavaAdapter = new JavaAdapter(mTestScope.getBackgroundScope()); private ScrimController mScrimController; @@ -145,10 +147,12 @@ public class ScrimControllerTest extends SysuiTestCase { @Mock private PrimaryBouncerToGoneTransitionViewModel mPrimaryBouncerToGoneTransitionViewModel; @Mock private AlternateBouncerToGoneTransitionViewModel mAlternateBouncerToGoneTransitionViewModel; - @Mock private KeyguardTransitionInteractor mKeyguardTransitionInteractor; + private final KeyguardTransitionInteractor mKeyguardTransitionInteractor = + mKosmos.getKeyguardTransitionInteractor(); + private final FakeKeyguardTransitionRepository mKeyguardTransitionRepository = + mKosmos.getKeyguardTransitionRepository(); @Mock private KeyguardInteractor mKeyguardInteractor; private final FakeWallpaperRepository mWallpaperRepository = new FakeWallpaperRepository(); - @Mock private CoroutineDispatcher mMainDispatcher; @Mock private TypedArray mMockTypedArray; // TODO(b/204991468): Use a real PanelExpansionStateManager object once this bug is fixed. (The @@ -265,8 +269,6 @@ public class ScrimControllerTest extends SysuiTestCase { when(mDelayedWakeLockFactory.create(any(String.class))).thenReturn(mWakeLock); when(mDockManager.isDocked()).thenReturn(false); - when(mKeyguardTransitionInteractor.transition(any(), any())) - .thenReturn(emptyFlow()); when(mPrimaryBouncerToGoneTransitionViewModel.getScrimAlpha()) .thenReturn(emptyFlow()); when(mAlternateBouncerToGoneTransitionViewModel.getScrimAlpha()) @@ -292,13 +294,16 @@ public class ScrimControllerTest extends SysuiTestCase { mKeyguardTransitionInteractor, mKeyguardInteractor, mWallpaperRepository, - mMainDispatcher, + mKosmos.getTestDispatcher(), mLinearLargeScreenShadeInterpolator); mScrimController.start(); mScrimController.setScrimVisibleListener(visible -> mScrimVisibility = visible); mScrimController.attachViews(mScrimBehind, mNotificationsScrim, mScrimInFront); mScrimController.setAnimatorListener(mAnimatorListener); + // Attach behind scrim so flows that are collecting on it start running. + ViewUtils.attachView(mScrimBehind); + mScrimController.setHasBackdrop(false); mWallpaperRepository.getWallpaperSupportsAmbientMode().setValue(false); @@ -629,6 +634,164 @@ public class ScrimControllerTest extends SysuiTestCase { } @Test + public void lockscreenToHubTransition_setsBehindScrimAlpha() { + // Start on lockscreen. + mScrimController.transitionTo(ScrimState.KEYGUARD); + finishAnimationsImmediately(); + + // Behind scrim starts at default alpha. + final float transitionProgress = 0f; + float expectedAlpha = ScrimState.KEYGUARD.getBehindAlpha(); + mKeyguardTransitionRepository.sendTransitionStepJava(mKosmos.getTestScope(), + new TransitionStep( + KeyguardState.LOCKSCREEN, + KeyguardState.GLANCEABLE_HUB, + transitionProgress, + TransitionState.STARTED + ), true); + mTestScope.getTestScheduler().runCurrent(); + assertThat(mScrimBehind.getViewAlpha()).isEqualTo(expectedAlpha); + + // Scrim fades out as transition runs. + final float runningProgress = 0.2f; + expectedAlpha = (1 - runningProgress) * ScrimState.KEYGUARD.getBehindAlpha(); + mKeyguardTransitionRepository.sendTransitionStepJava(mKosmos.getTestScope(), + new TransitionStep( + KeyguardState.LOCKSCREEN, + KeyguardState.GLANCEABLE_HUB, + runningProgress, + TransitionState.RUNNING + ), true); + mTestScope.getTestScheduler().runCurrent(); + assertThat(mScrimBehind.getViewAlpha()).isEqualTo(expectedAlpha); + + // Scrim invisible at end of transition. + final float finishedProgress = 1f; + expectedAlpha = 0f; + mKeyguardTransitionRepository.sendTransitionStepJava(mKosmos.getTestScope(), + new TransitionStep( + KeyguardState.LOCKSCREEN, + KeyguardState.GLANCEABLE_HUB, + finishedProgress, + TransitionState.FINISHED + ), true); + mTestScope.getTestScheduler().runCurrent(); + assertThat(mScrimBehind.getViewAlpha()).isEqualTo(expectedAlpha); + } + + @Test + public void hubToLockscreenTransition_setsViewAlpha() { + // Start on glanceable hub. + mScrimController.transitionTo(ScrimState.GLANCEABLE_HUB); + finishAnimationsImmediately(); + + // Behind scrim starts at 0 alpha. + final float transitionProgress = 0f; + float expectedAlpha = 0f; + mKeyguardTransitionRepository.sendTransitionStepJava(mKosmos.getTestScope(), + new TransitionStep( + KeyguardState.GLANCEABLE_HUB, + KeyguardState.LOCKSCREEN, + transitionProgress, + TransitionState.STARTED + ), true); + mTestScope.getTestScheduler().runCurrent(); + assertThat(mScrimBehind.getViewAlpha()).isEqualTo(expectedAlpha); + + // Scrim fades in as transition runs. + final float runningProgress = 0.2f; + expectedAlpha = runningProgress * ScrimState.KEYGUARD.getBehindAlpha(); + mKeyguardTransitionRepository.sendTransitionStepJava(mKosmos.getTestScope(), + new TransitionStep( + KeyguardState.GLANCEABLE_HUB, + KeyguardState.LOCKSCREEN, + runningProgress, + TransitionState.RUNNING + ), true); + mTestScope.getTestScheduler().runCurrent(); + assertThat(mScrimBehind.getViewAlpha()).isEqualTo(expectedAlpha); + + // Scrim at default visibility at end of transition. + final float finishedProgress = 1f; + expectedAlpha = finishedProgress * ScrimState.KEYGUARD.getBehindAlpha(); + mKeyguardTransitionRepository.sendTransitionStepJava(mKosmos.getTestScope(), + new TransitionStep( + KeyguardState.GLANCEABLE_HUB, + KeyguardState.LOCKSCREEN, + finishedProgress, + TransitionState.FINISHED + ), true); + mTestScope.getTestScheduler().runCurrent(); + assertThat(mScrimBehind.getViewAlpha()).isEqualTo(expectedAlpha); + } + + @Test + public void transitionToHub() { + mScrimController.setRawPanelExpansionFraction(0f); + mScrimController.setBouncerHiddenFraction(KeyguardBouncerConstants.EXPANSION_HIDDEN); + mScrimController.transitionTo(ScrimState.GLANCEABLE_HUB); + finishAnimationsImmediately(); + + // All scrims transparent on the hub. + assertScrimAlpha(Map.of( + mScrimInFront, TRANSPARENT, + mNotificationsScrim, TRANSPARENT, + mScrimBehind, TRANSPARENT)); + } + + @Test + public void openBouncerOnHub() { + mScrimController.transitionTo(ScrimState.GLANCEABLE_HUB); + + // Open the bouncer. + mScrimController.setRawPanelExpansionFraction(0f); + mScrimController.setBouncerHiddenFraction(KeyguardBouncerConstants.EXPANSION_VISIBLE); + finishAnimationsImmediately(); + + // Only behind widget is visible. + assertScrimAlpha(Map.of( + mScrimInFront, TRANSPARENT, + mNotificationsScrim, TRANSPARENT, + mScrimBehind, OPAQUE)); + + // Bouncer is closed. + mScrimController.setBouncerHiddenFraction(KeyguardBouncerConstants.EXPANSION_HIDDEN); + mScrimController.transitionTo(ScrimState.GLANCEABLE_HUB); + finishAnimationsImmediately(); + + // All scrims are transparent. + assertScrimAlpha(Map.of( + mScrimInFront, TRANSPARENT, + mNotificationsScrim, TRANSPARENT, + mScrimBehind, TRANSPARENT)); + } + + @Test + public void openShadeOnHub() { + mScrimController.transitionTo(ScrimState.GLANCEABLE_HUB); + + // Open the shade. + mScrimController.transitionTo(SHADE_LOCKED); + mScrimController.setQsPosition(1f, 0); + finishAnimationsImmediately(); + + // Shade scrims are visible. + assertScrimAlpha(Map.of( + mNotificationsScrim, OPAQUE, + mScrimInFront, TRANSPARENT, + mScrimBehind, OPAQUE)); + + mScrimController.transitionTo(ScrimState.GLANCEABLE_HUB); + finishAnimationsImmediately(); + + // All scrims are transparent. + assertScrimAlpha(Map.of( + mScrimInFront, TRANSPARENT, + mNotificationsScrim, TRANSPARENT, + mScrimBehind, TRANSPARENT)); + } + + @Test public void onThemeChange_bouncerBehindTint_isUpdatedToSurfaceColor() { assertEquals(BOUNCER.getBehindTint(), 0x112233); mSurfaceColor = 0x223344; @@ -1001,7 +1164,7 @@ public class ScrimControllerTest extends SysuiTestCase { mKeyguardTransitionInteractor, mKeyguardInteractor, mWallpaperRepository, - mMainDispatcher, + mKosmos.getTestDispatcher(), mLinearLargeScreenShadeInterpolator); mScrimController.start(); mScrimController.setScrimVisibleListener(visible -> mScrimVisibility = visible); @@ -1267,7 +1430,7 @@ public class ScrimControllerTest extends SysuiTestCase { ScrimState.UNINITIALIZED, ScrimState.KEYGUARD, BOUNCER, ScrimState.DREAMING, ScrimState.BOUNCER_SCRIMMED, ScrimState.BRIGHTNESS_MIRROR, ScrimState.UNLOCKED, SHADE_LOCKED, ScrimState.AUTH_SCRIMMED, - ScrimState.AUTH_SCRIMMED_SHADE)); + ScrimState.AUTH_SCRIMMED_SHADE, ScrimState.GLANCEABLE_HUB)); for (ScrimState state : ScrimState.values()) { if (!lowPowerModeStates.contains(state) && !regularStates.contains(state)) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt index 21c038ad476d..f53fc46d8ab2 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/satellite/ui/viewmodel/DeviceBasedSatelliteViewModelTest.kt @@ -20,6 +20,7 @@ import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.common.shared.model.Icon import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.FakeMobileIconsInteractor import com.android.systemui.statusbar.pipeline.mobile.util.FakeMobileMappingsProxy import com.android.systemui.statusbar.pipeline.satellite.data.prod.FakeDeviceBasedSatelliteRepository @@ -36,6 +37,7 @@ import org.mockito.MockitoAnnotations class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { private lateinit var underTest: DeviceBasedSatelliteViewModel private lateinit var interactor: DeviceBasedSatelliteInteractor + private lateinit var airplaneModeRepository: FakeAirplaneModeRepository private val repo = FakeDeviceBasedSatelliteRepository() private val mobileIconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock()) @@ -45,6 +47,7 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { @Before fun setUp() { MockitoAnnotations.initMocks(this) + airplaneModeRepository = FakeAirplaneModeRepository() interactor = DeviceBasedSatelliteInteractor( @@ -57,6 +60,7 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { DeviceBasedSatelliteViewModel( interactor, testScope.backgroundScope, + airplaneModeRepository, ) } @@ -72,6 +76,9 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1) i1.isInService.value = false + // GIVEN apm is disabled + airplaneModeRepository.setIsAirplaneMode(false) + // THEN icon is null because we should not be showing it assertThat(latest).isNull() } @@ -88,11 +95,33 @@ class DeviceBasedSatelliteViewModelTest : SysuiTestCase() { val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1) i1.isInService.value = true + // GIVEN apm is disabled + airplaneModeRepository.setIsAirplaneMode(false) + // THEN icon is null because we have service assertThat(latest).isNull() } @Test + fun icon_nullWhenShouldNotShow_apmIsEnabled() = + testScope.runTest { + val latest by collectLastValue(underTest.icon) + + // GIVEN satellite is allowed + repo.isSatelliteAllowedForCurrentLocation.value = true + + // GIVEN all icons are OOS + val i1 = mobileIconsInteractor.getMobileConnectionInteractorForSubId(1) + i1.isInService.value = false + + // GIVEN apm is enabled + airplaneModeRepository.setIsAirplaneMode(true) + + // THEN icon is null because we should not be showing it + assertThat(latest).isNull() + } + + @Test fun icon_satelliteIsOff() = testScope.runTest { val latest by collectLastValue(underTest.icon) diff --git a/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java index 457acd214222..b58a41c89a4e 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/theme/ThemeOverlayControllerTest.java @@ -190,7 +190,7 @@ public class ThemeOverlayControllerTest extends SysuiTestCase { mWakefulnessLifecycle.dispatchFinishedWakingUp(); mThemeOverlayController.start(); - verify(mUserTracker).addCallback(mUserTrackerCallback.capture(), eq(mBgExecutor)); + verify(mUserTracker).addCallback(mUserTrackerCallback.capture(), eq(mMainExecutor)); verify(mWallpaperManager).addOnColorsChangedListener(mColorsListener.capture(), eq(null), eq(UserHandle.USER_ALL)); verify(mBroadcastDispatcher).registerReceiver(mBroadcastReceiver.capture(), any(), diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt index abfff34f6dba..0669cb8b8ba5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/data/repository/UserRepositoryImplTest.kt @@ -31,6 +31,7 @@ import com.android.systemui.util.settings.FakeGlobalSettings import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.Job import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.launchIn @@ -46,6 +47,7 @@ import org.mockito.Mockito.mock import org.mockito.Mockito.`when` as whenever import org.mockito.MockitoAnnotations +@OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(JUnit4::class) class UserRepositoryImplTest : SysuiTestCase() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java index 7eba3f0602d5..0a3c2d9a77cd 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java @@ -2217,7 +2217,8 @@ public class BubblesTest extends SysuiTestCase { FakeBubbleStateListener bubbleStateListener = new FakeBubbleStateListener(); mBubbleController.registerBubbleStateListener(bubbleStateListener); - mBubbleController.expandStackAndSelectBubbleFromLauncher(mBubbleEntry.getKey(), 500, 1000); + mBubbleController.expandStackAndSelectBubbleFromLauncher(mBubbleEntry.getKey(), + new Rect(500, 1000, 600, 1100)); assertThat(mBubbleController.getLayerView().isExpanded()).isTrue(); diff --git a/packages/SystemUI/tests/utils/src/android/content/PackageManagerKosmos.kt b/packages/SystemUI/tests/utils/src/android/content/PackageManagerKosmos.kt new file mode 100644 index 000000000000..8901314d8e76 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/android/content/PackageManagerKosmos.kt @@ -0,0 +1,22 @@ +/* + * 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 android.content + +import android.content.pm.PackageManager +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.mockito.mock + +val Kosmos.packageManager by Kosmos.Fixture { mock<PackageManager>() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorKosmos.kt index 06b6cda62806..244ef8d81ebd 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/PrimaryBouncerInteractorKosmos.kt @@ -16,7 +16,40 @@ package com.android.systemui.bouncer.domain.interactor +import android.content.applicationContext +import com.android.keyguard.keyguardSecurityModel +import com.android.keyguard.keyguardUpdateMonitor +import com.android.systemui.bouncer.data.repository.keyguardBouncerRepository +import com.android.systemui.bouncer.ui.BouncerView +import com.android.systemui.classifier.falsingCollector +import com.android.systemui.concurrency.fakeExecutor +import com.android.systemui.deviceentry.domain.interactor.deviceEntryFaceAuthInteractor +import com.android.systemui.keyguard.DismissCallbackRegistry +import com.android.systemui.keyguard.data.repository.trustRepository import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.statusbar.policy.KeyguardStateControllerImpl +import com.android.systemui.user.domain.interactor.selectedUserInteractor +import com.android.systemui.util.concurrency.mockExecutorHandler import com.android.systemui.util.mockito.mock -var Kosmos.primaryBouncerInteractor by Kosmos.Fixture { mock<PrimaryBouncerInteractor>() } +var Kosmos.mockPrimaryBouncerInteractor by Kosmos.Fixture { mock<PrimaryBouncerInteractor>() } +var Kosmos.primaryBouncerInteractor by + Kosmos.Fixture { + PrimaryBouncerInteractor( + repository = keyguardBouncerRepository, + primaryBouncerView = mock<BouncerView>(), + mainHandler = mockExecutorHandler(executor = fakeExecutor), + keyguardStateController = mock<KeyguardStateControllerImpl>(), + keyguardSecurityModel = keyguardSecurityModel, + primaryBouncerCallbackInteractor = mock<PrimaryBouncerCallbackInteractor>(), + falsingCollector = falsingCollector, + dismissCallbackRegistry = mock<DismissCallbackRegistry>(), + context = applicationContext, + keyguardUpdateMonitor = keyguardUpdateMonitor, + trustRepository = trustRepository, + applicationScope = applicationCoroutineScope, + selectedUserInteractor = selectedUserInteractor, + deviceEntryFaceAuthInteractor = deviceEntryFaceAuthInteractor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt index d91c5974815c..99dfe94af3df 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt @@ -21,11 +21,13 @@ import com.android.systemui.authentication.domain.interactor.authenticationInter import com.android.systemui.bouncer.domain.interactor.bouncerActionButtonInteractor import com.android.systemui.bouncer.domain.interactor.bouncerInteractor import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor +import com.android.systemui.inputmethod.domain.interactor.inputMethodInteractor import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.scene.shared.flag.sceneContainerFlags +import com.android.systemui.user.domain.interactor.selectedUserInteractor import com.android.systemui.user.ui.viewmodel.userSwitcherViewModel import com.android.systemui.util.mockito.mock import com.android.systemui.util.time.systemClock @@ -36,8 +38,10 @@ val Kosmos.bouncerViewModel by Fixture { applicationScope = testScope.backgroundScope, mainDispatcher = testDispatcher, bouncerInteractor = bouncerInteractor, + inputMethodInteractor = inputMethodInteractor, simBouncerInteractor = simBouncerInteractor, authenticationInteractor = authenticationInteractor, + selectedUserInteractor = selectedUserInteractor, flags = sceneContainerFlags, selectedUser = userSwitcherViewModel.selectedUser, users = userSwitcherViewModel.users, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt index bc7e7af245a6..fab64e38e1f8 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/data/repository/FakeCommunalWidgetRepository.kt @@ -36,6 +36,14 @@ class FakeCommunalWidgetRepository(private val coroutineScope: CoroutineScope) : } } + override fun deleteWidget(widgetId: Int) { + if (_communalWidgets.value.none { it.appWidgetId == widgetId }) { + return + } + + _communalWidgets.value = _communalWidgets.value.filter { it.appWidgetId != widgetId } + } + private fun onConfigured(id: Int, providerInfo: AppWidgetProviderInfo, priority: Int) { _communalWidgets.value += listOf(CommunalWidgetContentModel(id, providerInfo, priority)) } diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt index 002862e949ba..a231212518ec 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/controls/panels/FakeSelectedComponentRepository.kt @@ -17,6 +17,7 @@ package com.android.systemui.controls.panels import android.os.UserHandle +import com.android.systemui.kosmos.Kosmos import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow @@ -68,3 +69,6 @@ class FakeSelectedComponentRepository : SelectedComponentRepository { } } } + +val Kosmos.selectedComponentRepository by + Kosmos.Fixture<FakeSelectedComponentRepository> { FakeSelectedComponentRepository() } diff --git a/packages/SystemUI/tests/src/com/android/systemui/controls/settings/FakeControlsSettingsRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/controls/settings/FakeControlsSettingsRepository.kt index b6628db14235..b6628db14235 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/controls/settings/FakeControlsSettingsRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/controls/settings/FakeControlsSettingsRepository.kt diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt index 0b1fb4074226..5575b05b3874 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFaceAuthInteractorKosmos.kt @@ -23,7 +23,7 @@ import com.android.keyguard.keyguardUpdateMonitor import com.android.keyguard.trustManager import com.android.systemui.biometrics.data.repository.facePropertyRepository import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor -import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor +import com.android.systemui.bouncer.domain.interactor.mockPrimaryBouncerInteractor import com.android.systemui.deviceentry.data.repository.faceWakeUpTriggersConfig import com.android.systemui.keyguard.data.repository.biometricSettingsRepository import com.android.systemui.keyguard.data.repository.deviceEntryFaceAuthRepository @@ -46,7 +46,7 @@ val Kosmos.deviceEntryFaceAuthInteractor by applicationScope = applicationCoroutineScope, mainDispatcher = testDispatcher, repository = deviceEntryFaceAuthRepository, - primaryBouncerInteractor = { primaryBouncerInteractor }, + primaryBouncerInteractor = { mockPrimaryBouncerInteractor }, alternateBouncerInteractor = alternateBouncerInteractor, keyguardTransitionInteractor = keyguardTransitionInteractor, faceAuthenticationLogger = faceAuthLogger, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/ui/binder/LiftToRunFaceAuthBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/ui/binder/LiftToRunFaceAuthBinderKosmos.kt new file mode 100644 index 000000000000..2fead91b430a --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/deviceentry/ui/binder/LiftToRunFaceAuthBinderKosmos.kt @@ -0,0 +1,45 @@ +/* + * 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.deviceentry.ui.binder + +import android.content.packageManager +import com.android.keyguard.keyguardUpdateMonitor +import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor +import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor +import com.android.systemui.deviceentry.domain.interactor.deviceEntryFaceAuthInteractor +import com.android.systemui.keyguard.domain.interactor.keyguardInteractor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.power.domain.interactor.powerInteractor +import com.android.systemui.util.sensors.asyncSensorManager +import kotlinx.coroutines.ExperimentalCoroutinesApi + +@ExperimentalCoroutinesApi +val Kosmos.liftToRunFaceAuthBinder by + Kosmos.Fixture { + LiftToRunFaceAuthBinder( + scope = applicationCoroutineScope, + packageManager = packageManager, + asyncSensorManager = asyncSensorManager, + keyguardUpdateMonitor = keyguardUpdateMonitor, + keyguardInteractor = keyguardInteractor, + primaryBouncerInteractor = primaryBouncerInteractor, + alternateBouncerInteractor = alternateBouncerInteractor, + deviceEntryFaceAuthInteractor = deviceEntryFaceAuthInteractor, + powerInteractor = powerInteractor, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/inputmethod/data/repository/FakeInputMethodRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/inputmethod/data/repository/FakeInputMethodRepository.kt new file mode 100644 index 000000000000..8e4461dd5b1e --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/inputmethod/data/repository/FakeInputMethodRepository.kt @@ -0,0 +1,55 @@ +/* + * 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.inputmethod.data.repository + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.inputmethod.data.model.InputMethodModel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.asFlow +import kotlinx.coroutines.flow.flowOf + +@SysUISingleton +class FakeInputMethodRepository : InputMethodRepository { + + private var usersToEnabledInputMethods: MutableMap<Int, Flow<InputMethodModel>> = mutableMapOf() + + var selectedInputMethodSubtypes = listOf<InputMethodModel.Subtype>() + + /** + * The display ID on which the input method picker dialog was shown, or `null` if the dialog was + * not shown. + */ + var inputMethodPickerShownDisplayId: Int? = null + + fun setEnabledInputMethods(userId: Int, vararg enabledInputMethods: InputMethodModel) { + usersToEnabledInputMethods[userId] = enabledInputMethods.asFlow() + } + + override suspend fun enabledInputMethods( + userId: Int, + fetchSubtypes: Boolean, + ): Flow<InputMethodModel> { + return usersToEnabledInputMethods[userId] ?: flowOf() + } + + override suspend fun selectedInputMethodSubtypes(): List<InputMethodModel.Subtype> = + selectedInputMethodSubtypes + + override suspend fun showInputMethodPicker(displayId: Int, showAuxiliarySubtypes: Boolean) { + inputMethodPickerShownDisplayId = displayId + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/inputmethod/data/repository/InputMethodRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/inputmethod/data/repository/InputMethodRepositoryKosmos.kt new file mode 100644 index 000000000000..b71b9d878876 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/inputmethod/data/repository/InputMethodRepositoryKosmos.kt @@ -0,0 +1,23 @@ +/* + * 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.inputmethod.data.repository + +import com.android.systemui.kosmos.Kosmos + +var Kosmos.inputMethodRepository: InputMethodRepository by + Kosmos.Fixture { fakeInputMethodRepository } +val Kosmos.fakeInputMethodRepository by Kosmos.Fixture { FakeInputMethodRepository() } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/inputmethod/domain/interactor/InputMethodInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/inputmethod/domain/interactor/InputMethodInteractorKosmos.kt new file mode 100644 index 000000000000..da7757565888 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/inputmethod/domain/interactor/InputMethodInteractorKosmos.kt @@ -0,0 +1,27 @@ +/* + * 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.inputmethod.domain.interactor + +import com.android.systemui.inputmethod.data.repository.inputMethodRepository +import com.android.systemui.kosmos.Kosmos + +val Kosmos.inputMethodInteractor by + Kosmos.Fixture { + InputMethodInteractor( + repository = inputMethodRepository, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt index 0c1dbfebfb34..e20a0ab4190e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardTransitionRepository.kt @@ -28,9 +28,12 @@ import dagger.Module import java.util.UUID import javax.inject.Inject import junit.framework.Assert.fail +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.launch import kotlinx.coroutines.test.TestCoroutineScheduler import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent @@ -150,6 +153,15 @@ class FakeKeyguardTransitionRepository @Inject constructor() : KeyguardTransitio _transitions.emit(step) } + /** Version of [sendTransitionStep] that's usable from Java tests. */ + fun sendTransitionStepJava( + coroutineScope: CoroutineScope, + step: TransitionStep, + validateStep: Boolean = true + ): Job { + return coroutineScope.launch { sendTransitionStep(step, validateStep) } + } + suspend fun sendTransitionSteps( steps: List<TransitionStep>, testScope: TestScope, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsKosmos.kt index c71c1c3ea5f0..ffa4133c7269 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/BouncerToGoneFlowsKosmos.kt @@ -18,7 +18,7 @@ package com.android.systemui.keyguard.ui.viewmodel -import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor +import com.android.systemui.bouncer.domain.interactor.mockPrimaryBouncerInteractor import com.android.systemui.flags.featureFlagsClassic import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow import com.android.systemui.kosmos.Kosmos @@ -31,7 +31,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi val Kosmos.bouncerToGoneFlows by Fixture { BouncerToGoneFlows( statusBarStateController = sysuiStatusBarStateController, - primaryBouncerInteractor = primaryBouncerInteractor, + primaryBouncerInteractor = mockPrimaryBouncerInteractor, keyguardDismissActionInteractor = mock(), featureFlags = featureFlagsClassic, shadeInteractor = shadeInteractor, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelKosmos.kt index ab28d0d670ef..4ecff73f71ed 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/PrimaryBouncerToGoneTransitionViewModelKosmos.kt @@ -18,7 +18,7 @@ package com.android.systemui.keyguard.ui.viewmodel -import com.android.systemui.bouncer.domain.interactor.primaryBouncerInteractor +import com.android.systemui.bouncer.domain.interactor.mockPrimaryBouncerInteractor import com.android.systemui.flags.featureFlagsClassic import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow import com.android.systemui.kosmos.Kosmos @@ -30,7 +30,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi val Kosmos.primaryBouncerToGoneTransitionViewModel by Fixture { PrimaryBouncerToGoneTransitionViewModel( statusBarStateController = sysuiStatusBarStateController, - primaryBouncerInteractor = primaryBouncerInteractor, + primaryBouncerInteractor = mockPrimaryBouncerInteractor, keyguardDismissActionInteractor = mock(), featureFlags = featureFlagsClassic, bouncerToGoneFlows = bouncerToGoneFlows, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt index 11f2938141b4..083de107c971 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt @@ -32,6 +32,8 @@ import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteract import com.android.systemui.flags.fakeFeatureFlagsClassic import com.android.systemui.jank.interactionJankMonitor import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository +import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository +import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.model.sceneContainerPlugin import com.android.systemui.plugins.statusbar.statusBarStateController import com.android.systemui.power.data.repository.fakePowerRepository @@ -61,6 +63,8 @@ class KosmosJavaAdapter( val bouncerRepository by lazy { kosmos.bouncerRepository } val communalRepository by lazy { kosmos.fakeCommunalRepository } val keyguardRepository by lazy { kosmos.fakeKeyguardRepository } + val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository } + val keyguardTransitionInteractor by lazy { kosmos.keyguardTransitionInteractor } val powerRepository by lazy { kosmos.fakePowerRepository } val clock by lazy { kosmos.systemClock } val mobileConnectionsRepository by lazy { kosmos.fakeMobileConnectionsRepository } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModelKosmos.kt index f2f3a5a1ad72..d79633ae72ba 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModelKosmos.kt @@ -18,6 +18,7 @@ package com.android.systemui.statusbar.notification.stack.ui.viewmodel import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackAppearanceInteractor @@ -25,5 +26,6 @@ val Kosmos.notificationStackAppearanceViewModel by Fixture { NotificationStackAppearanceViewModel( stackAppearanceInteractor = notificationStackAppearanceInteractor, shadeInteractor = shadeInteractor, + sceneInteractor = sceneInteractor, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt index 7c398cd45f90..549a77513e9c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelKosmos.kt @@ -20,7 +20,9 @@ import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.keyguard.domain.interactor.keyguardInteractor import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor import com.android.systemui.keyguard.ui.viewmodel.aodBurnInViewModel +import com.android.systemui.keyguard.ui.viewmodel.dreamingToLockscreenTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.glanceableHubToLockscreenTransitionViewModel +import com.android.systemui.keyguard.ui.viewmodel.lockscreenToDreamingTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.lockscreenToGlanceableHubTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.lockscreenToOccludedTransitionViewModel import com.android.systemui.keyguard.ui.viewmodel.occludedToLockscreenTransitionViewModel @@ -40,6 +42,8 @@ val Kosmos.sharedNotificationContainerViewModel by Fixture { communalInteractor = communalInteractor, occludedToLockscreenTransitionViewModel = occludedToLockscreenTransitionViewModel, lockscreenToOccludedTransitionViewModel = lockscreenToOccludedTransitionViewModel, + dreamingToLockscreenTransitionViewModel = dreamingToLockscreenTransitionViewModel, + lockscreenToDreamingTransitionViewModel = lockscreenToDreamingTransitionViewModel, glanceableHubToLockscreenTransitionViewModel = glanceableHubToLockscreenTransitionViewModel, lockscreenToGlanceableHubTransitionViewModel = lockscreenToGlanceableHubTransitionViewModel, aodBurnInViewModel = aodBurnInViewModel, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/sensors/AsyncSensorManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/sensors/AsyncSensorManagerKosmos.kt new file mode 100644 index 000000000000..117ae8c46d3c --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/sensors/AsyncSensorManagerKosmos.kt @@ -0,0 +1,21 @@ +/* + * 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.util.sensors + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.util.mockito.mock + +val Kosmos.asyncSensorManager by Kosmos.Fixture { mock<AsyncSensorManager>() } diff --git a/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java b/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java index 3aa9cc84e9f6..155c523a96a7 100644 --- a/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java +++ b/packages/services/CameraExtensionsProxy/src/com/android/cameraextensions/CameraExtensionsProxyService.java @@ -132,7 +132,7 @@ public class CameraExtensionsProxyService extends Service { private static final String CAMERA_EXTENSION_VERSION_NAME = "androidx.camera.extensions.impl.ExtensionVersionImpl"; - private static final String LATEST_VERSION = "1.4.0"; + private static final String LATEST_VERSION = "1.5.0"; // No support for the init sequence private static final String NON_INIT_VERSION_PREFIX = "1.0"; // Support advanced API and latency queries @@ -1693,6 +1693,7 @@ public class CameraExtensionsProxyService extends Service { private final Size mSize; private final int mImageFormat; private final int mDataspace; + private final long mUsage; public OutputSurfaceImplStub(OutputSurface outputSurface) { mSurface = outputSurface.surface; @@ -1700,8 +1701,10 @@ public class CameraExtensionsProxyService extends Service { mImageFormat = outputSurface.imageFormat; if (mSurface != null) { mDataspace = SurfaceUtils.getSurfaceDataspace(mSurface); + mUsage = SurfaceUtils.getSurfaceUsage(mSurface); } else { mDataspace = -1; + mUsage = 0; } } @@ -1724,6 +1727,11 @@ public class CameraExtensionsProxyService extends Service { public int getDataspace() { return mDataspace; } + + @Override + public long getUsage() { + return mUsage; + } } private class PreviewExtenderImplStub extends IPreviewExtenderImpl.Stub implements @@ -2471,6 +2479,11 @@ public class CameraExtensionsProxyService extends Service { ret.size.height = imageReaderOutputConfig.getSize().getHeight(); ret.imageFormat = imageReaderOutputConfig.getImageFormat(); ret.capacity = imageReaderOutputConfig.getMaxImages(); + if (EFV_SUPPORTED) { + ret.usage = imageReaderOutputConfig.getUsage(); + } else { + ret.usage = 0; + } } else if (output instanceof MultiResolutionImageReaderOutputConfigImpl) { MultiResolutionImageReaderOutputConfigImpl multiResReaderConfig = (MultiResolutionImageReaderOutputConfigImpl) output; diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp index e013a3e41896..1ac69f6c4fc8 100644 --- a/ravenwood/Android.bp +++ b/ravenwood/Android.bp @@ -30,6 +30,9 @@ java_library { "junit-src/**/*.java", "junit-impl-src/**/*.java", ], + static_libs: [ + "androidx.test.monitor-for-device", + ], libs: [ "framework-minus-apex.ravenwood", "junit", @@ -61,3 +64,17 @@ java_host_for_device { "core-xml-for-host", ], } + +java_host_for_device { + name: "androidx.test.monitor-for-device", + libs: [ + "androidx.test.monitor-for-host", + ], +} + +java_device_for_host { + name: "androidx.test.monitor-for-host", + libs: [ + "androidx.test.monitor", + ], +} diff --git a/ravenwood/bulk_enable.py b/ravenwood/bulk_enable.py new file mode 100644 index 000000000000..36d398cc160c --- /dev/null +++ b/ravenwood/bulk_enable.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +# +# 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. + +""" +Tool to bulk-enable tests that are now passing on Ravenwood. + +Currently only offers to include classes which are fully passing; ignores +classes that have partial success. + +Typical usage: +$ ENABLE_PROBE_IGNORED=1 atest MyTestsRavenwood +$ cd /path/to/tests/root +$ python bulk_enable.py /path/to/atest/output/host_log.txt +""" + +import collections +import os +import re +import subprocess +import sys + +re_result = re.compile("I/ModuleListener.+?null-device-0 (.+?)#(.+?) ([A-Z_]+)(.*)$") + +ANNOTATION = "@android.platform.test.annotations.EnabledOnRavenwood" +SED_ARG = "s/^((public )?class )/%s\\n\\1/g" % (ANNOTATION) + +STATE_PASSED = "PASSED" +STATE_FAILURE = "FAILURE" +STATE_ASSUMPTION_FAILURE = "ASSUMPTION_FAILURE" +STATE_CANDIDATE = "CANDIDATE" + +stats_total = collections.defaultdict(int) +stats_class = collections.defaultdict(lambda: collections.defaultdict(int)) +stats_method = collections.defaultdict() + +with open(sys.argv[1]) as f: + for line in f.readlines(): + result = re_result.search(line) + if result: + clazz, method, state, msg = result.groups() + if state == STATE_FAILURE and "actually passed under Ravenwood" in msg: + state = STATE_CANDIDATE + stats_total[state] += 1 + stats_class[clazz][state] += 1 + stats_method[(clazz, method)] = state + +# Find classes who are fully "candidates" (would be entirely green if enabled) +num_enabled = 0 +for clazz in stats_class.keys(): + stats = stats_class[clazz] + if STATE_CANDIDATE in stats and len(stats) == 1: + num_enabled += stats[STATE_CANDIDATE] + print("Enabling fully-passing class", clazz) + clazz_match = re.compile("%s\.(kt|java)" % (clazz.split(".")[-1])) + for root, dirs, files in os.walk("."): + for f in files: + if clazz_match.match(f): + path = os.path.join(root, f) + subprocess.run(["sed", "-i", "-E", SED_ARG, path]) + +print("Overall stats", stats_total) +print("Candidates actually enabled", num_enabled) diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java index b3dbcde9d324..5588f4f811d5 100644 --- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java @@ -16,12 +16,38 @@ package android.platform.test.ravenwood; +import android.app.Instrumentation; +import android.os.Bundle; import android.os.HandlerThread; import android.os.Looper; +import android.util.Log; + +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.runner.Description; + +import java.io.PrintStream; +import java.util.Map; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; public class RavenwoodRuleImpl { private static final String MAIN_THREAD_NAME = "RavenwoodMain"; + /** + * When enabled, attempt to dump all thread stacks just before we hit the + * overall Tradefed timeout, to aid in debugging deadlocks. + */ + private static final boolean ENABLE_TIMEOUT_STACKS = false; + private static final int TIMEOUT_MILLIS = 9_000; + + private static final ScheduledExecutorService sTimeoutExecutor = + Executors.newScheduledThreadPool(1); + + private static ScheduledFuture<?> sPendingTimeout; + public static boolean isOnRavenwood() { return true; } @@ -41,9 +67,22 @@ public class RavenwoodRuleImpl { main.start(); Looper.setMainLooperForTest(main.getLooper()); } + + InstrumentationRegistry.registerInstance(new Instrumentation(), Bundle.EMPTY); + + if (ENABLE_TIMEOUT_STACKS) { + sPendingTimeout = sTimeoutExecutor.schedule(RavenwoodRuleImpl::dumpStacks, + TIMEOUT_MILLIS, TimeUnit.MILLISECONDS); + } } public static void reset(RavenwoodRule rule) { + if (ENABLE_TIMEOUT_STACKS) { + sPendingTimeout.cancel(false); + } + + InstrumentationRegistry.registerInstance(null, Bundle.EMPTY); + if (rule.mProvideMainThread) { Looper.getMainLooper().quit(); Looper.clearMainLooperForTest(); @@ -55,4 +94,26 @@ public class RavenwoodRuleImpl { android.os.Binder.reset$ravenwood(); android.os.Process.reset$ravenwood(); } + + public static void logTestRunner(String label, Description description) { + // This message string carefully matches the exact format emitted by on-device tests, to + // aid developers in debugging raw text logs + Log.e("TestRunner", label + ": " + description.getMethodName() + + "(" + description.getTestClass().getName() + ")"); + } + + private static void dumpStacks() { + final PrintStream out = System.err; + out.println("-----BEGIN ALL THREAD STACKS-----"); + final Map<Thread, StackTraceElement[]> stacks = Thread.getAllStackTraces(); + for (Map.Entry<Thread, StackTraceElement[]> stack : stacks.entrySet()) { + out.println(); + Thread t = stack.getKey(); + out.println(t.toString() + " ID=" + t.getId()); + for (StackTraceElement e : stack.getValue()) { + out.println("\tat " + e); + } + } + out.println("-----END ALL THREAD STACKS-----"); + } } diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java index 68b163ede89c..8d76970f9ad4 100644 --- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java +++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java @@ -19,6 +19,7 @@ package android.platform.test.ravenwood; import static android.platform.test.ravenwood.RavenwoodRule.ENABLE_PROBE_IGNORED; import static android.platform.test.ravenwood.RavenwoodRule.IS_ON_RAVENWOOD; import static android.platform.test.ravenwood.RavenwoodRule.shouldEnableOnRavenwood; +import static android.platform.test.ravenwood.RavenwoodRule.shouldStillIgnoreInProbeIgnoreMode; import android.platform.test.annotations.DisabledOnRavenwood; import android.platform.test.annotations.EnabledOnRavenwood; @@ -45,6 +46,7 @@ public class RavenwoodClassRule implements TestRule { } if (ENABLE_PROBE_IGNORED) { + Assume.assumeFalse(shouldStillIgnoreInProbeIgnoreMode(description)); // Pass through to possible underlying RavenwoodRule for both environment // configuration and handling method-level annotations return base; diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java index 952ee0e184b1..0285b386ed13 100644 --- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java +++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java @@ -27,7 +27,9 @@ import org.junit.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; +import java.util.Objects; import java.util.concurrent.atomic.AtomicInteger; +import java.util.regex.Pattern; /** * {@code @Rule} that configures the Ravenwood test environment. This rule has no effect when @@ -55,6 +57,43 @@ public class RavenwoodRule implements TestRule { static final boolean ENABLE_PROBE_IGNORED = "1".equals( System.getenv("RAVENWOOD_RUN_DISABLED_TESTS")); + /** + * When using ENABLE_PROBE_IGNORED, you may still want to skip certain tests, + * for example because the test would crash the JVM. + * + * This regex defines the tests that should still be disabled even if ENABLE_PROBE_IGNORED + * is set. + * + * Before running each test class and method, we check if this pattern can be found in + * the full test name (either [class full name], or [class full name] + "#" + [method name]), + * and if so, we skip it. + * + * For example, if you want to skip an entire test class, use: + * RAVENWOOD_REALLY_DISABLE='\.CustomTileDefaultsRepositoryTest$' + * + * For example, if you want to skip an entire test class, use: + * RAVENWOOD_REALLY_DISABLE='\.CustomTileDefaultsRepositoryTest#testSimple$' + * + * To ignore multiple classes, use (...|...), for example: + * RAVENWOOD_REALLY_DISABLE='\.(ClassA|ClassB)$' + * + * Because we use a regex-find, setting "." would disable all tests. + */ + private static final Pattern REALLY_DISABLE_PATTERN = Pattern.compile( + Objects.requireNonNullElse(System.getenv("RAVENWOOD_REALLY_DISABLE"), "")); + + private static final boolean ENABLE_REALLY_DISABLE_PATTERN = + !REALLY_DISABLE_PATTERN.pattern().isEmpty(); + + static { + if (ENABLE_PROBE_IGNORED) { + System.out.println("$RAVENWOOD_RUN_DISABLED_TESTS enabled: force running all tests"); + if (ENABLE_REALLY_DISABLE_PATTERN) { + System.out.println("$RAVENWOOD_REALLY_DISABLE=" + REALLY_DISABLE_PATTERN.pattern()); + } + } + } + private static final int SYSTEM_UID = 1000; private static final int NOBODY_UID = 9999; private static final int FIRST_APPLICATION_UID = 10000; @@ -203,6 +242,21 @@ public class RavenwoodRule implements TestRule { return true; } + static boolean shouldStillIgnoreInProbeIgnoreMode(Description description) { + if (!ENABLE_REALLY_DISABLE_PATTERN) { + return false; + } + + final var fullname = description.getTestClass().getName() + + (description.isTest() ? "#" + description.getMethodName() : ""); + + if (REALLY_DISABLE_PATTERN.matcher(fullname).find()) { + System.out.println("Still ignoring " + fullname); + return true; + } + return false; + } + @Override public Statement apply(Statement base, Description description) { // No special treatment when running outside Ravenwood; run tests as-is @@ -226,9 +280,14 @@ public class RavenwoodRule implements TestRule { public void evaluate() throws Throwable { Assume.assumeTrue(shouldEnableOnRavenwood(description)); + RavenwoodRuleImpl.logTestRunner("started", description); RavenwoodRuleImpl.init(RavenwoodRule.this); try { base.evaluate(); + RavenwoodRuleImpl.logTestRunner("finished", description); + } catch (Throwable t) { + RavenwoodRuleImpl.logTestRunner("failed", description); + throw t; } finally { RavenwoodRuleImpl.reset(RavenwoodRule.this); } @@ -245,6 +304,9 @@ public class RavenwoodRule implements TestRule { return new Statement() { @Override public void evaluate() throws Throwable { + Assume.assumeFalse(shouldStillIgnoreInProbeIgnoreMode(description)); + + RavenwoodRuleImpl.logTestRunner("started", description); RavenwoodRuleImpl.init(RavenwoodRule.this); try { base.evaluate(); @@ -254,6 +316,7 @@ public class RavenwoodRule implements TestRule { Assume.assumeTrue(shouldEnableOnRavenwood(description)); throw t; } finally { + RavenwoodRuleImpl.logTestRunner("finished", description); RavenwoodRuleImpl.reset(RavenwoodRule.this); } diff --git a/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java b/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java index d0c2e18be8df..7d172f2a83c0 100644 --- a/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java +++ b/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java @@ -16,6 +16,8 @@ package android.platform.test.ravenwood; +import org.junit.runner.Description; + public class RavenwoodRuleImpl { public static boolean isOnRavenwood() { return false; @@ -28,4 +30,8 @@ public class RavenwoodRuleImpl { public static void reset(RavenwoodRule rule) { // No-op when running on a real device } + + public static void logTestRunner(String label, Description description) { + // No-op when running on a real device + } } diff --git a/ravenwood/ravenwood-annotation-allowed-classes.txt b/ravenwood/ravenwood-annotation-allowed-classes.txt index eaf01a32592e..b775f9ad47ad 100644 --- a/ravenwood/ravenwood-annotation-allowed-classes.txt +++ b/ravenwood/ravenwood-annotation-allowed-classes.txt @@ -72,6 +72,7 @@ android.os.Process android.os.ServiceSpecificException android.os.SystemClock android.os.SystemProperties +android.os.TestLooperManager android.os.ThreadLocalWorkSource android.os.TimestampedValue android.os.Trace @@ -141,6 +142,8 @@ android.graphics.RectF android.content.ContentProvider +android.app.Instrumentation + android.metrics.LogMaker android.view.Display$HdrCapabilities diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java index 57c05396992e..63784ba61150 100644 --- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java +++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java @@ -16,6 +16,7 @@ package com.android.server.accessibility; +import static android.Manifest.permission.INTERNAL_SYSTEM_WINDOW; import static android.accessibilityservice.AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON; import static android.accessibilityservice.AccessibilityTrace.FLAGS_ACCESSIBILITY_MANAGER; import static android.accessibilityservice.AccessibilityTrace.FLAGS_ACCESSIBILITY_MANAGER_CLIENT; @@ -5724,6 +5725,21 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub } @Override + public void attachAccessibilityOverlayToDisplay_enforcePermission( + int displayId, SurfaceControl sc) { + mContext.enforceCallingPermission( + INTERNAL_SYSTEM_WINDOW, "attachAccessibilityOverlayToDisplay_enforcePermission"); + mMainHandler.sendMessage( + obtainMessage( + AccessibilityManagerService::attachAccessibilityOverlayToDisplayInternal, + this, + -1, + displayId, + sc, + null)); + } + + @Override public void attachAccessibilityOverlayToDisplay( int interactionId, int displayId, @@ -5759,12 +5775,15 @@ public class AccessibilityManagerService extends IAccessibilityManager.Stub t.close(); result = AccessibilityService.OVERLAY_RESULT_SUCCESS; } - // Send the result back to the service. - try { - callback.sendAttachOverlayResult(result, interactionId); - } catch (RemoteException re) { - Slog.e(LOG_TAG, "Exception while attaching overlay.", re); - // the other side will time out + + if (callback != null) { + // Send the result back to the service. + try { + callback.sendAttachOverlayResult(result, interactionId); + } catch (RemoteException re) { + Slog.e(LOG_TAG, "Exception while attaching overlay.", re); + // the other side will time out + } } } } diff --git a/services/backup/Android.bp b/services/backup/Android.bp index d08a97e2a573..2a85eb66f6c5 100644 --- a/services/backup/Android.bp +++ b/services/backup/Android.bp @@ -21,7 +21,6 @@ java_library_static { libs: ["services.core"], static_libs: [ "app-compat-annotations", - "backup_flags_lib", ], lint: { baseline_filename: "lint-baseline.xml", @@ -33,8 +32,3 @@ aconfig_declarations { package: "com.android.server.backup", srcs: ["flags.aconfig"], } - -java_aconfig_library { - name: "backup_flags_lib", - aconfig_declarations: "backup_flags", -} diff --git a/services/backup/flags.aconfig b/services/backup/flags.aconfig index 1416c888f790..6a63b3a9db24 100644 --- a/services/backup/flags.aconfig +++ b/services/backup/flags.aconfig @@ -24,4 +24,12 @@ flag { description: "Enables the write buffer to pipes to be of maximum size." bug: "265976737" is_fixed_read_only: true +} + +flag { + name: "enable_clear_pipe_after_restore_file" + namespace: "onboarding" + description: "Enables clearing the pipe buffer after restoring a single file to a BackupAgent." + bug: "320633449" + is_fixed_read_only: true }
\ No newline at end of file diff --git a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java index 055970819e28..d85dd879e21d 100644 --- a/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java +++ b/services/backup/java/com/android/server/backup/restore/PerformUnifiedRestoreTask.java @@ -732,6 +732,7 @@ public class PerformUnifiedRestoreTask implements BackupRestoreTask { BackupManagerMonitor.LOG_EVENT_CATEGORY_BACKUP_MANAGER_POLICY, monitoringExtras); EventLog.writeEvent(EventLogTags.RESTORE_TRANSPORT_FAILURE); + mStatus = BackupTransport.TRANSPORT_ERROR; nextState = UnifiedRestoreState.FINAL; } finally { executeNextState(nextState); diff --git a/services/core/Android.bp b/services/core/Android.bp index 8e35b7455d02..89896c3735b6 100644 --- a/services/core/Android.bp +++ b/services/core/Android.bp @@ -211,7 +211,6 @@ java_library_static { "com_android_wm_shell_flags_lib", "com.android.server.utils_aconfig-java", "service-jobscheduler-deviceidle.flags-aconfig-java", - "backup_flags_lib", "policy_flags_lib", "net_flags_lib", "stats_flags_lib", diff --git a/services/core/java/com/android/server/BinderCallsStatsService.java b/services/core/java/com/android/server/BinderCallsStatsService.java index b87184aa5582..416b36feb94d 100644 --- a/services/core/java/com/android/server/BinderCallsStatsService.java +++ b/services/core/java/com/android/server/BinderCallsStatsService.java @@ -288,22 +288,23 @@ public class BinderCallsStatsService extends Binder { CachedDeviceState.Readonly.class); mBinderCallsStats.setDeviceState(deviceState); - BatteryStatsInternal batteryStatsInternal = getLocalService( - BatteryStatsInternal.class); - mBinderCallsStats.setCallStatsObserver(new BinderInternal.CallStatsObserver() { - @Override - public void noteCallStats(int workSourceUid, long incrementalCallCount, - Collection<BinderCallsStats.CallStat> callStats) { - batteryStatsInternal.noteBinderCallStats(workSourceUid, - incrementalCallCount, callStats); - } - - @Override - public void noteBinderThreadNativeIds(int[] binderThreadNativeTids) { - batteryStatsInternal.noteBinderThreadNativeIds(binderThreadNativeTids); - } - }); - + if (!com.android.server.power.optimization.Flags.disableSystemServicePowerAttr()) { + BatteryStatsInternal batteryStatsInternal = getLocalService( + BatteryStatsInternal.class); + mBinderCallsStats.setCallStatsObserver(new BinderInternal.CallStatsObserver() { + @Override + public void noteCallStats(int workSourceUid, long incrementalCallCount, + Collection<BinderCallsStats.CallStat> callStats) { + batteryStatsInternal.noteBinderCallStats(workSourceUid, + incrementalCallCount, callStats); + } + + @Override + public void noteBinderThreadNativeIds(int[] binderThreadNativeTids) { + batteryStatsInternal.noteBinderThreadNativeIds(binderThreadNativeTids); + } + }); + } // It needs to be called before mService.systemReady to make sure the observer is // initialized before installing it. mWorkSourceProvider.systemReady(getContext()); diff --git a/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java b/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java index c3916422159e..f619ca3f66a2 100644 --- a/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java +++ b/services/core/java/com/android/server/SensitiveContentProtectionManagerService.java @@ -27,6 +27,7 @@ import android.media.projection.MediaProjectionManager; import android.os.Handler; import android.os.Looper; import android.os.RemoteException; +import android.os.Trace; import android.os.UserHandle; import android.service.notification.NotificationListenerService; import android.service.notification.NotificationListenerService.RankingMap; @@ -49,12 +50,12 @@ public final class SensitiveContentProtectionManagerService extends SystemServic private static final String TAG = "SensitiveContentProtect"; private static final boolean DEBUG = false; - @VisibleForTesting - NotificationListener mNotificationListener; + @VisibleForTesting NotificationListener mNotificationListener; private @Nullable MediaProjectionManager mProjectionManager; private @Nullable WindowManagerInternal mWindowManager; final Object mSensitiveContentProtectionLock = new Object(); + @GuardedBy("mSensitiveContentProtectionLock") private boolean mProjectionActive = false; @@ -63,13 +64,24 @@ public final class SensitiveContentProtectionManagerService extends SystemServic @Override public void onStart(MediaProjectionInfo info) { if (DEBUG) Log.d(TAG, "onStart projection: " + info); - onProjectionStart(); + Trace.beginSection( + "SensitiveContentProtectionManagerService.onProjectionStart"); + try { + onProjectionStart(); + } finally { + Trace.endSection(); + } } @Override public void onStop(MediaProjectionInfo info) { if (DEBUG) Log.d(TAG, "onStop projection: " + info); - onProjectionEnd(); + Trace.beginSection("SensitiveContentProtectionManagerService.onProjectionStop"); + try { + onProjectionEnd(); + } finally { + Trace.endSection(); + } } }; @@ -94,8 +106,7 @@ public final class SensitiveContentProtectionManagerService extends SystemServic } @VisibleForTesting - void init(MediaProjectionManager projectionManager, - WindowManagerInternal windowManager) { + void init(MediaProjectionManager projectionManager, WindowManagerInternal windowManager) { if (DEBUG) Log.d(TAG, "init"); checkNotNull(projectionManager, "Failed to get valid MediaProjectionManager"); @@ -109,7 +120,8 @@ public final class SensitiveContentProtectionManagerService extends SystemServic mProjectionManager.addCallback(mProjectionCallback, new Handler(Looper.getMainLooper())); try { - mNotificationListener.registerAsSystemService(getContext(), + mNotificationListener.registerAsSystemService( + getContext(), new ComponentName(getContext(), NotificationListener.class), UserHandle.USER_ALL); } catch (RemoteException e) { @@ -174,8 +186,8 @@ public final class SensitiveContentProtectionManagerService extends SystemServic } // notify windowmanager of any currently posted sensitive content notifications - ArraySet<PackageInfo> packageInfos = getSensitivePackagesFromNotifications( - notifications, rankingMap); + ArraySet<PackageInfo> packageInfos = + getSensitivePackagesFromNotifications(notifications, rankingMap); mWindowManager.addBlockScreenCaptureForApps(packageInfos); } @@ -197,8 +209,8 @@ public final class SensitiveContentProtectionManagerService extends SystemServic return sensitivePackages; } - private PackageInfo getSensitivePackageFromNotification(StatusBarNotification sbn, - RankingMap rankingMap) { + private PackageInfo getSensitivePackageFromNotification( + StatusBarNotification sbn, RankingMap rankingMap) { if (sbn == null) { Log.w(TAG, "Unable to protect null notification"); return null; @@ -220,38 +232,55 @@ public final class SensitiveContentProtectionManagerService extends SystemServic @Override public void onListenerConnected() { super.onListenerConnected(); - // Projection started before notification listener was connected - synchronized (mSensitiveContentProtectionLock) { - if (mProjectionActive) { - updateAppsThatShouldBlockScreenCapture(); + Trace.beginSection("SensitiveContentProtectionManagerService.onListenerConnected"); + try { + // Projection started before notification listener was connected + synchronized (mSensitiveContentProtectionLock) { + if (mProjectionActive) { + updateAppsThatShouldBlockScreenCapture(); + } } + } finally { + Trace.endSection(); } } @Override public void onNotificationPosted(StatusBarNotification sbn, RankingMap rankingMap) { super.onNotificationPosted(sbn, rankingMap); - synchronized (mSensitiveContentProtectionLock) { - if (!mProjectionActive) { - return; - } - - // notify windowmanager of any currently posted sensitive content notifications - PackageInfo packageInfo = getSensitivePackageFromNotification(sbn, rankingMap); - - if (packageInfo != null) { - mWindowManager.addBlockScreenCaptureForApps(new ArraySet(Set.of(packageInfo))); + Trace.beginSection("SensitiveContentProtectionManagerService.onNotificationPosted"); + try { + synchronized (mSensitiveContentProtectionLock) { + if (!mProjectionActive) { + return; + } + + // notify windowmanager of any currently posted sensitive content notifications + PackageInfo packageInfo = getSensitivePackageFromNotification(sbn, rankingMap); + + if (packageInfo != null) { + mWindowManager.addBlockScreenCaptureForApps( + new ArraySet(Set.of(packageInfo))); + } } + } finally { + Trace.endSection(); } } @Override public void onNotificationRankingUpdate(RankingMap rankingMap) { super.onNotificationRankingUpdate(rankingMap); - synchronized (mSensitiveContentProtectionLock) { - if (mProjectionActive) { - updateAppsThatShouldBlockScreenCapture(rankingMap); + Trace.beginSection( + "SensitiveContentProtectionManagerService.onNotificationRankingUpdate"); + try { + synchronized (mSensitiveContentProtectionLock) { + if (mProjectionActive) { + updateAppsThatShouldBlockScreenCapture(rankingMap); + } } + } finally { + Trace.endSection(); } } } diff --git a/services/core/java/com/android/server/am/BatteryStatsService.java b/services/core/java/com/android/server/am/BatteryStatsService.java index 3487ae3c6e6c..4f46ecdb9228 100644 --- a/services/core/java/com/android/server/am/BatteryStatsService.java +++ b/services/core/java/com/android/server/am/BatteryStatsService.java @@ -422,7 +422,9 @@ public final class BatteryStatsService extends IBatteryStats.Stub mStats.setExternalStatsSyncLocked(mWorker); mStats.setRadioScanningTimeoutLocked(mContext.getResources().getInteger( com.android.internal.R.integer.config_radioScanningTimeout) * 1000L); - mStats.startTrackingSystemServerCpuTime(); + if (!Flags.disableSystemServicePowerAttr()) { + mStats.startTrackingSystemServerCpuTime(); + } mAggregatedPowerStatsConfig = createAggregatedPowerStatsConfig(); mPowerStatsStore = new PowerStatsStore(systemDir, mHandler, mAggregatedPowerStatsConfig); diff --git a/services/core/java/com/android/server/am/PendingIntentController.java b/services/core/java/com/android/server/am/PendingIntentController.java index a20623cd1ee9..5df910716ba6 100644 --- a/services/core/java/com/android/server/am/PendingIntentController.java +++ b/services/core/java/com/android/server/am/PendingIntentController.java @@ -30,6 +30,7 @@ import android.app.ActivityOptions; import android.app.AppGlobals; import android.app.PendingIntent; import android.app.PendingIntentStats; +import android.app.compat.CompatChanges; import android.content.IIntentSender; import android.content.Intent; import android.os.Binder; @@ -136,6 +137,11 @@ public class PendingIntentController { + "intent creator (" + packageName + ") because this option is meant for the pending intent sender"); + if (CompatChanges.isChangeEnabled(PendingIntent.PENDING_INTENT_OPTIONS_CHECK, + callingUid)) { + throw new IllegalArgumentException("pendingIntentBackgroundActivityStartMode " + + "must not be set when creating a PendingIntent"); + } opts.setPendingIntentBackgroundActivityStartMode( ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED); } diff --git a/services/core/java/com/android/server/am/PendingIntentRecord.java b/services/core/java/com/android/server/am/PendingIntentRecord.java index 10d5fd3f77b6..95e130ed1194 100644 --- a/services/core/java/com/android/server/am/PendingIntentRecord.java +++ b/services/core/java/com/android/server/am/PendingIntentRecord.java @@ -406,6 +406,9 @@ public final class PendingIntentRecord extends IIntentSender.Stub { String resolvedType, IBinder allowlistToken, IIntentReceiver finishedReceiver, String requiredPermission, IBinder resultTo, String resultWho, int requestCode, int flagsMask, int flagsValues, Bundle options) { + final int callingUid = Binder.getCallingUid(); + final int callingPid = Binder.getCallingPid(); + if (intent != null) intent.setDefusable(true); if (options != null) options.setDefusable(true); @@ -458,6 +461,12 @@ public final class PendingIntentRecord extends IIntentSender.Stub { + key.packageName + ") because this option is meant for the pending intent " + "creator"); + if (CompatChanges.isChangeEnabled(PendingIntent.PENDING_INTENT_OPTIONS_CHECK, + callingUid)) { + throw new IllegalArgumentException( + "pendingIntentCreatorBackgroundActivityStartMode " + + "must not be set when sending a PendingIntent"); + } opts.setPendingIntentCreatorBackgroundActivityStartMode( ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED); } @@ -494,9 +503,6 @@ public final class PendingIntentRecord extends IIntentSender.Stub { } // We don't hold the controller lock beyond this point as we will be calling into AM and WM. - final int callingUid = Binder.getCallingUid(); - final int callingPid = Binder.getCallingPid(); - // Only system senders can declare a broadcast to be alarm-originated. We check // this here rather than in the general case handling below to fail before the other // invocation side effects such as allowlisting. diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java index 7aafda59298c..1207616dcb8d 100644 --- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java +++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java @@ -162,6 +162,7 @@ public class SettingsToPropertiesMapper { "nfc", "pdf_viewer", "pixel_audio_android", + "pixel_biometrics_face", "pixel_bluetooth", "pixel_connectivity_gps", "pixel_system_sw_video", diff --git a/services/core/java/com/android/server/biometrics/sensors/AcquisitionClient.java b/services/core/java/com/android/server/biometrics/sensors/AcquisitionClient.java index aa6a0f1bb55f..fbd32a67fe6c 100644 --- a/services/core/java/com/android/server/biometrics/sensors/AcquisitionClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/AcquisitionClient.java @@ -112,10 +112,8 @@ public abstract class AcquisitionClient<T> extends HalClientMonitor<T> implement getLogger().logOnError(getContext(), getOperationContext(), errorCode, vendorCode, getTargetUserId()); try { - if (getListener() != null) { - mShouldSendErrorToClient = false; - getListener().onError(getSensorId(), getCookie(), errorCode, vendorCode); - } + mShouldSendErrorToClient = false; + getListener().onError(getSensorId(), getCookie(), errorCode, vendorCode); } catch (RemoteException e) { Slog.w(TAG, "Failed to invoke sendError", e); } @@ -147,9 +145,7 @@ public abstract class AcquisitionClient<T> extends HalClientMonitor<T> implement final int errorCode = BiometricConstants.BIOMETRIC_ERROR_CANCELED; try { - if (getListener() != null) { - getListener().onError(getSensorId(), getCookie(), errorCode, 0 /* vendorCode */); - } + getListener().onError(getSensorId(), getCookie(), errorCode, 0 /* vendorCode */); } catch (RemoteException e) { Slog.w(TAG, "Failed to invoke sendError", e); } @@ -181,7 +177,7 @@ public abstract class AcquisitionClient<T> extends HalClientMonitor<T> implement } try { - if (getListener() != null && shouldSend) { + if (shouldSend) { getListener().onAcquired(getSensorId(), acquiredInfo, vendorCode); } } catch (RemoteException e) { diff --git a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java index f9568ea9d72b..506b4562b43d 100644 --- a/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/AuthenticationClient.java @@ -265,17 +265,13 @@ public abstract class AuthenticationClient<T, O extends AuthenticateOptions> Slog.d(TAG, "Skipping addAuthToken"); } try { - if (listener != null) { - if (!mIsRestricted) { - listener.onAuthenticationSucceeded(getSensorId(), identifier, byteToken, - getTargetUserId(), mIsStrongBiometric); - } else { - listener.onAuthenticationSucceeded(getSensorId(), null /* identifier */, - byteToken, - getTargetUserId(), mIsStrongBiometric); - } + if (!mIsRestricted) { + listener.onAuthenticationSucceeded(getSensorId(), identifier, byteToken, + getTargetUserId(), mIsStrongBiometric); } else { - Slog.e(TAG, "Received successful auth, but client was not listening"); + listener.onAuthenticationSucceeded(getSensorId(), null /* identifier */, + byteToken, + getTargetUserId(), mIsStrongBiometric); } } catch (RemoteException e) { Slog.e(TAG, "Unable to notify listener", e); @@ -301,11 +297,7 @@ public abstract class AuthenticationClient<T, O extends AuthenticateOptions> } try { - if (listener != null) { - listener.onAuthenticationFailed(getSensorId()); - } else { - Slog.e(TAG, "Received failed auth, but client was not listening"); - } + listener.onAuthenticationFailed(getSensorId()); } catch (RemoteException e) { Slog.e(TAG, "Unable to notify listener", e); mCallback.onClientFinished(this, false); diff --git a/services/core/java/com/android/server/biometrics/sensors/BaseClientMonitor.java b/services/core/java/com/android/server/biometrics/sensors/BaseClientMonitor.java index 0216e49b531b..a4088524117c 100644 --- a/services/core/java/com/android/server/biometrics/sensors/BaseClientMonitor.java +++ b/services/core/java/com/android/server/biometrics/sensors/BaseClientMonitor.java @@ -22,6 +22,7 @@ import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.hardware.biometrics.BiometricConstants; +import android.hardware.biometrics.IBiometricSensorReceiver; import android.os.IBinder; import android.os.RemoteException; import android.util.Slog; @@ -55,7 +56,7 @@ public abstract class BaseClientMonitor implements IBinder.DeathRecipient { @Nullable private IBinder mToken; private long mRequestId; - @Nullable private ClientMonitorCallbackConverter mListener; + @NonNull private ClientMonitorCallbackConverter mListener; // Currently only used for authentication client. The cookie generated by BiometricService // is never 0. private final int mCookie; @@ -95,7 +96,8 @@ public abstract class BaseClientMonitor implements IBinder.DeathRecipient { mContext = context; mToken = token; mRequestId = -1; - mListener = listener; + mListener = listener == null ? new ClientMonitorCallbackConverter( + new IBiometricSensorReceiver.Default()) : listener; mTargetUserId = userId; mOwner = owner; mCookie = cookie; @@ -199,7 +201,7 @@ public abstract class BaseClientMonitor implements IBinder.DeathRecipient { } mToken = null; if (clearListener) { - mListener = null; + mListener = new ClientMonitorCallbackConverter(new IBiometricSensorReceiver.Default()); } } @@ -233,8 +235,8 @@ public abstract class BaseClientMonitor implements IBinder.DeathRecipient { return mOwner; } - @Nullable - public final ClientMonitorCallbackConverter getListener() { + @NonNull + protected ClientMonitorCallbackConverter getListener() { return mListener; } @@ -312,9 +314,7 @@ public abstract class BaseClientMonitor implements IBinder.DeathRecipient { final int errorCode = BiometricConstants.BIOMETRIC_ERROR_CANCELED; try { ClientMonitorCallbackConverter listener = getListener(); - if (listener != null) { - listener.onError(getSensorId(), getCookie(), errorCode, 0 /* vendorCode */); - } + listener.onError(getSensorId(), getCookie(), errorCode, 0 /* vendorCode */); } catch (RemoteException e) { Slog.w(TAG, "Failed to invoke sendError", e); } diff --git a/services/core/java/com/android/server/biometrics/sensors/EnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/EnrollClient.java index 2c4d30b622ad..8e7004d8fde5 100644 --- a/services/core/java/com/android/server/biometrics/sensors/EnrollClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/EnrollClient.java @@ -82,9 +82,7 @@ public abstract class EnrollClient<T> extends AcquisitionClient<T> implements En final ClientMonitorCallbackConverter listener = getListener(); try { - if (listener != null) { - listener.onEnrollResult(identifier, remaining); - } + listener.onEnrollResult(identifier, remaining); } catch (RemoteException e) { Slog.e(TAG, "Remote exception", e); } diff --git a/services/core/java/com/android/server/biometrics/sensors/RemovalClient.java b/services/core/java/com/android/server/biometrics/sensors/RemovalClient.java index 45ffa23dc66a..d2ef2786c252 100644 --- a/services/core/java/com/android/server/biometrics/sensors/RemovalClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/RemovalClient.java @@ -74,13 +74,9 @@ public abstract class RemovalClient<S extends BiometricAuthenticator.Identifier, if (identifier == null) { Slog.e(TAG, "identifier was null, skipping onRemove()"); try { - if (getListener() != null) { - getListener().onError(getSensorId(), getCookie(), - BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_REMOVE, - 0 /* vendorCode */); - } else { - Slog.e(TAG, "Error, listener was null, not sending onError callback"); - } + getListener().onError(getSensorId(), getCookie(), + BiometricConstants.BIOMETRIC_ERROR_UNABLE_TO_REMOVE, + 0 /* vendorCode */); } catch (RemoteException e) { Slog.w(TAG, "Failed to send error to client for onRemoved", e); } @@ -93,9 +89,7 @@ public abstract class RemovalClient<S extends BiometricAuthenticator.Identifier, identifier.getBiometricId()); try { - if (getListener() != null) { - getListener().onRemoved(identifier, remaining); - } + getListener().onRemoved(identifier, remaining); } catch (RemoteException e) { Slog.w(TAG, "Failed to notify Removed:", e); } diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java index f35de93af625..415d2942be5c 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceAuthenticationClient.java @@ -337,7 +337,7 @@ public class FaceAuthenticationClient onAcquiredInternal(acquireInfo, vendorCode, false /* shouldSend */); final boolean shouldSend = shouldSendAcquiredMessage(acquireInfo, vendorCode); - if (shouldSend && getListener() != null) { + if (shouldSend) { try { getListener().onAuthenticationFrame(frame); } catch (RemoteException e) { diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java index f5c452992674..5f370f23134c 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceEnrollClient.java @@ -152,7 +152,7 @@ public class FaceEnrollClient extends EnrollClient<AidlSession> { onAcquiredInternal(acquireInfo, vendorCode, false /* shouldSend */); final boolean shouldSend = shouldSendAcquiredMessage(acquireInfo, vendorCode); - if (shouldSend && getListener() != null) { + if (shouldSend) { try { getListener().onEnrollmentFrame(frame); } catch (RemoteException e) { diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceGenerateChallengeClient.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceGenerateChallengeClient.java index e404bd2be31e..cf45eb8b3178 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceGenerateChallengeClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/FaceGenerateChallengeClient.java @@ -58,11 +58,6 @@ public class FaceGenerateChallengeClient extends GenerateChallengeClient<AidlSes void onChallengeGenerated(int sensorId, int userId, long challenge) { try { final ClientMonitorCallbackConverter listener = getListener(); - if (listener == null) { - Slog.e(TAG, "Listener is null in onChallengeGenerated"); - mCallback.onClientFinished(this, false /* success */); - return; - } listener.onChallengeGenerated(sensorId, userId, challenge); mCallback.onClientFinished(this, true /* success */); } catch (RemoteException e) { diff --git a/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceGetFeatureClient.java b/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceGetFeatureClient.java index 981253699322..47aaeec81978 100644 --- a/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceGetFeatureClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/face/hidl/FaceGetFeatureClient.java @@ -61,9 +61,7 @@ public class FaceGetFeatureClient extends HalClientMonitor<IBiometricsFace> { @Override public void unableToStart() { try { - if (getListener() != null) { - getListener().onFeatureGet(false /* success */, new int[0], new boolean[0]); - } + getListener().onFeatureGet(false /* success */, new int[0], new boolean[0]); } catch (RemoteException e) { Slog.e(TAG, "Unable to send error", e); } @@ -85,9 +83,7 @@ public class FaceGetFeatureClient extends HalClientMonitor<IBiometricsFace> { featureState[0] = result.value; mValue = result.value; - if (getListener() != null) { - getListener().onFeatureGet(result.status == Status.OK, features, featureState); - } + getListener().onFeatureGet(result.status == Status.OK, features, featureState); mCallback.onClientFinished(this, true /* success */); } catch (RemoteException e) { Slog.e(TAG, "Unable to getFeature", e); diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java index 6912961ab94b..e0fd44b9f6bb 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintAuthenticationClient.java @@ -450,9 +450,7 @@ public class FingerprintAuthenticationClient pc.major); } - if (getListener() != null) { - getListener().onUdfpsPointerDown(getSensorId()); - } + getListener().onUdfpsPointerDown(getSensorId()); } catch (RemoteException e) { Slog.e(TAG, "Remote exception", e); } @@ -471,9 +469,7 @@ public class FingerprintAuthenticationClient session.getSession().onPointerUp(pc.pointerId); } - if (getListener() != null) { - getListener().onUdfpsPointerUp(getSensorId()); - } + getListener().onUdfpsPointerUp(getSensorId()); } catch (RemoteException e) { Slog.e(TAG, "Remote exception", e); } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java index a7fb7741fee1..cb220b9e1c34 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintDetectClient.java @@ -175,11 +175,7 @@ public class FingerprintDetectClient extends AcquisitionClient<AidlSession> vibrateSuccess(); try { - if (getListener() != null) { - getListener().onDetected(getSensorId(), getTargetUserId(), mIsStrongBiometric); - } else { - Slog.e(TAG, "Listener is null!"); - } + getListener().onDetected(getSensorId(), getTargetUserId(), mIsStrongBiometric); mCallback.onClientFinished(this, true /* success */); } catch (RemoteException e) { Slog.e(TAG, "Remote exception when sending onDetected", e); diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java index 3fb9223249b4..225bd594adc6 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintEnrollClient.java @@ -305,9 +305,7 @@ public class FingerprintEnrollClient extends EnrollClient<AidlSession> implement pc.major); } - if (getListener() != null) { - getListener().onUdfpsPointerDown(getSensorId()); - } + getListener().onUdfpsPointerDown(getSensorId()); } catch (RemoteException e) { Slog.e(TAG, "Unable to send pointer down", e); } @@ -325,9 +323,7 @@ public class FingerprintEnrollClient extends EnrollClient<AidlSession> implement session.getSession().onPointerUp(pc.pointerId); } - if (getListener() != null) { - getListener().onUdfpsPointerUp(getSensorId()); - } + getListener().onUdfpsPointerUp(getSensorId()); } catch (RemoteException e) { Slog.e(TAG, "Unable to send pointer up", e); } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintGenerateChallengeClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintGenerateChallengeClient.java index ce693ff58e19..d2f36ce26b0e 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintGenerateChallengeClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/FingerprintGenerateChallengeClient.java @@ -59,11 +59,6 @@ public class FingerprintGenerateChallengeClient extends GenerateChallengeClient< void onChallengeGenerated(int sensorId, int userId, long challenge) { try { final ClientMonitorCallbackConverter listener = getListener(); - if (listener == null) { - Slog.e(TAG, "Listener is null in onChallengeGenerated"); - mCallback.onClientFinished(this, false /* success */); - return; - } listener.onChallengeGenerated(sensorId, userId, challenge); mCallback.onClientFinished(this, true /* success */); } catch (RemoteException e) { diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21UdfpsMock.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21UdfpsMock.java index 9232e11a05ee..f85794673476 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21UdfpsMock.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/Fingerprint21UdfpsMock.java @@ -28,6 +28,7 @@ import android.hardware.fingerprint.FingerprintManager.AuthenticationCallback; import android.hardware.fingerprint.FingerprintManager.AuthenticationResult; import android.hardware.fingerprint.FingerprintSensorProperties; import android.hardware.fingerprint.FingerprintSensorPropertiesInternal; +import android.hardware.fingerprint.IFingerprintServiceReceiver; import android.hardware.fingerprint.IUdfpsOverlayController; import android.os.Handler; import android.os.IBinder; @@ -367,7 +368,8 @@ public class Fingerprint21UdfpsMock extends Fingerprint21 implements TrustManage final IBinder token = client.getToken(); final long operationId = authClient.getOperationId(); final int cookie = client.getCookie(); - final ClientMonitorCallbackConverter listener = client.getListener(); + final ClientMonitorCallbackConverter listener = new ClientMonitorCallbackConverter( + new IFingerprintServiceReceiver.Default()); final boolean restricted = authClient.isRestricted(); final int statsClient = client.getLogger().getStatsClient(); final boolean isKeyguard = authClient.isKeyguard(); diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java index 7a329e9d69e9..60c532c26f5d 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintAuthenticationClient.java @@ -279,12 +279,10 @@ class FingerprintAuthenticationClient mALSProbeCallback.getProbe().enable(); UdfpsHelper.onFingerDown(getFreshDaemon(), (int) pc.x, (int) pc.y, pc.minor, pc.major); - if (getListener() != null) { - try { - getListener().onUdfpsPointerDown(getSensorId()); - } catch (RemoteException e) { - Slog.e(TAG, "Remote exception", e); - } + try { + getListener().onUdfpsPointerDown(getSensorId()); + } catch (RemoteException e) { + Slog.e(TAG, "Remote exception", e); } } @@ -295,12 +293,10 @@ class FingerprintAuthenticationClient mALSProbeCallback.getProbe().disable(); UdfpsHelper.onFingerUp(getFreshDaemon()); - if (getListener() != null) { - try { - getListener().onUdfpsPointerUp(getSensorId()); - } catch (RemoteException e) { - Slog.e(TAG, "Remote exception", e); - } + try { + getListener().onUdfpsPointerUp(getSensorId()); + } catch (RemoteException e) { + Slog.e(TAG, "Remote exception", e); } } diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintDetectClient.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintDetectClient.java index 6e029d2268e2..50e48fe91c56 100644 --- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintDetectClient.java +++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/hidl/FingerprintDetectClient.java @@ -153,12 +153,10 @@ class FingerprintDetectClient extends AcquisitionClient<IBiometricsFingerprint> final PerformanceTracker pm = PerformanceTracker.getInstanceForSensorId(getSensorId()); pm.incrementAuthForUser(getTargetUserId(), authenticated); - if (getListener() != null) { - try { - getListener().onDetected(getSensorId(), getTargetUserId(), mIsStrongBiometric); - } catch (RemoteException e) { - Slog.e(TAG, "Remote exception when sending onDetected", e); - } + try { + getListener().onDetected(getSensorId(), getTargetUserId(), mIsStrongBiometric); + } catch (RemoteException e) { + Slog.e(TAG, "Remote exception when sending onDetected", e); } } diff --git a/services/core/java/com/android/server/content/SyncManager.java b/services/core/java/com/android/server/content/SyncManager.java index df179a9b6d65..5b23364cf546 100644 --- a/services/core/java/com/android/server/content/SyncManager.java +++ b/services/core/java/com/android/server/content/SyncManager.java @@ -138,6 +138,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.NoSuchElementException; import java.util.Objects; import java.util.Random; import java.util.Set; @@ -3870,8 +3871,12 @@ public class SyncManager { final SyncStorageEngine.EndPoint info = syncOperation.target; if (activeSyncContext.mIsLinkedToDeath) { - activeSyncContext.mSyncAdapter.asBinder().unlinkToDeath(activeSyncContext, 0); - activeSyncContext.mIsLinkedToDeath = false; + try { + activeSyncContext.mSyncAdapter.asBinder().unlinkToDeath(activeSyncContext, 0); + activeSyncContext.mIsLinkedToDeath = false; + } catch (NoSuchElementException e) { + Slog.wtf(TAG, "Failed to unlink active sync adapter to death", e); + } } final long elapsedTime = SystemClock.elapsedRealtime() - activeSyncContext.mStartTime; String historyMessage; diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 245fcea06eac..93addcd84d12 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -1656,14 +1656,16 @@ public final class DisplayManagerService extends SystemService { ContentRecordingSession session = null; try { if (projection != null) { - IBinder launchCookie = projection.getLaunchCookie(); - if (launchCookie == null) { + IBinder taskWindowContainerToken = projection.getLaunchCookie() == null ? null + : projection.getLaunchCookie().binder; + if (taskWindowContainerToken == null) { // Record a particular display. session = ContentRecordingSession.createDisplaySession( virtualDisplayConfig.getDisplayIdToMirror()); } else { // Record a single task indicated by the launch cookie. - session = ContentRecordingSession.createTaskSession(launchCookie); + session = ContentRecordingSession.createTaskSession( + taskWindowContainerToken); } } } catch (RemoteException e) { diff --git a/services/core/java/com/android/server/input/InputSettingsObserver.java b/services/core/java/com/android/server/input/InputSettingsObserver.java index 165dfe445751..5ffc3809ec98 100644 --- a/services/core/java/com/android/server/input/InputSettingsObserver.java +++ b/services/core/java/com/android/server/input/InputSettingsObserver.java @@ -64,6 +64,8 @@ class InputSettingsObserver extends ContentObserver { (reason) -> updateTouchpadNaturalScrollingEnabled()), Map.entry(Settings.System.getUriFor(Settings.System.TOUCHPAD_TAP_TO_CLICK), (reason) -> updateTouchpadTapToClickEnabled()), + Map.entry(Settings.System.getUriFor(Settings.System.TOUCHPAD_TAP_DRAGGING), + (reason) -> updateTouchpadTapDraggingEnabled()), Map.entry(Settings.System.getUriFor(Settings.System.TOUCHPAD_RIGHT_CLICK_ZONE), (reason) -> updateTouchpadRightClickZoneEnabled()), Map.entry(Settings.System.getUriFor(Settings.System.SHOW_TOUCHES), @@ -158,6 +160,10 @@ class InputSettingsObserver extends ContentObserver { mNative.setTouchpadTapToClickEnabled(InputSettings.useTouchpadTapToClick(mContext)); } + private void updateTouchpadTapDraggingEnabled() { + mNative.setTouchpadTapDraggingEnabled(InputSettings.useTouchpadTapDragging(mContext)); + } + private void updateTouchpadRightClickZoneEnabled() { mNative.setTouchpadRightClickZoneEnabled(InputSettings.useTouchpadRightClickZone(mContext)); } diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java index bc8207835a6e..e5f3484b4825 100644 --- a/services/core/java/com/android/server/input/NativeInputManagerService.java +++ b/services/core/java/com/android/server/input/NativeInputManagerService.java @@ -129,6 +129,8 @@ interface NativeInputManagerService { void setTouchpadTapToClickEnabled(boolean enabled); + void setTouchpadTapDraggingEnabled(boolean enabled); + void setTouchpadRightClickZoneEnabled(boolean enabled); void setShowTouches(boolean enabled); @@ -377,6 +379,9 @@ interface NativeInputManagerService { public native void setTouchpadTapToClickEnabled(boolean enabled); @Override + public native void setTouchpadTapDraggingEnabled(boolean enabled); + + @Override public native void setTouchpadRightClickZoneEnabled(boolean enabled); @Override diff --git a/services/core/java/com/android/server/inputmethod/ClientController.java b/services/core/java/com/android/server/inputmethod/ClientController.java index ece236a5f18c..86f4db959409 100644 --- a/services/core/java/com/android/server/inputmethod/ClientController.java +++ b/services/core/java/com/android/server/inputmethod/ClientController.java @@ -17,6 +17,7 @@ package com.android.server.inputmethod; import android.annotation.NonNull; +import android.annotation.Nullable; import android.content.pm.PackageManagerInternal; import android.os.IBinder; import android.os.RemoteException; @@ -29,6 +30,7 @@ import com.android.internal.inputmethod.IRemoteInputConnection; import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; /** * Store and manage {@link InputMethodManagerService} clients. This class was designed to be a @@ -62,7 +64,7 @@ final class ClientController { // TODO(b/314150112): Make this field private when breaking the cycle with IMMS. @GuardedBy("ImfLock.class") - final ArrayMap<IBinder, ClientState> mClients = new ArrayMap<>(); + private final ArrayMap<IBinder, ClientState> mClients = new ArrayMap<>(); @GuardedBy("ImfLock.class") private final List<ClientControllerCallback> mCallbacks = new ArrayList<>(); @@ -145,6 +147,19 @@ final class ClientController { } @GuardedBy("ImfLock.class") + @Nullable + ClientState getClient(IBinder binder) { + return mClients.get(binder); + } + + @GuardedBy("ImfLock.class") + void forAllClients(Consumer<ClientState> consumer) { + for (int i = 0; i < mClients.size(); i++) { + consumer.accept(mClients.valueAt(i)); + } + } + + @GuardedBy("ImfLock.class") boolean verifyClientAndPackageMatch( @NonNull IInputMethodClient client, @NonNull String packageName) { final ClientState cs = mClients.get(client.asBinder()); diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java index 4767ebd0aab0..f031b7b677ac 100644 --- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java +++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java @@ -205,6 +205,7 @@ import java.util.WeakHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; import java.util.function.IntConsumer; /** @@ -270,7 +271,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @NonNull private final String[] mNonPreemptibleInputMethods; - // TODO(b/314150112): Move this to ClientController. @UserIdInt private int mLastSwitchUserId; @@ -1819,10 +1819,8 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } mLastSwitchUserId = newUserId; - if (mIsInteractive && clientToBeReset != null) { - final ClientState cs = - mClientController.mClients.get(clientToBeReset.asBinder()); + final ClientState cs = mClientController.getClient(clientToBeReset.asBinder()); if (cs == null) { // The client is already gone. return; @@ -2165,26 +2163,25 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub /** * Hide the IME if the removed user is the current user. */ + @GuardedBy("ImfLock.class") private void onClientRemoved(ClientState client) { - synchronized (ImfLock.class) { - clearClientSessionLocked(client); - clearClientSessionForAccessibilityLocked(client); - if (mCurClient == client) { - hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */, - null /* resultReceiver */, SoftInputShowHideReason.HIDE_REMOVE_CLIENT); - if (mBoundToMethod) { - mBoundToMethod = false; - IInputMethodInvoker curMethod = getCurMethodLocked(); - if (curMethod != null) { - // When we unbind input, we are unbinding the client, so we always - // unbind ime and a11y together. - curMethod.unbindInput(); - AccessibilityManagerInternal.get().unbindInput(); - } + clearClientSessionLocked(client); + clearClientSessionForAccessibilityLocked(client); + if (mCurClient == client) { + hideCurrentInputLocked(mCurFocusedWindow, null /* statsToken */, 0 /* flags */, + null /* resultReceiver */, SoftInputShowHideReason.HIDE_REMOVE_CLIENT); + if (mBoundToMethod) { + mBoundToMethod = false; + IInputMethodInvoker curMethod = getCurMethodLocked(); + if (curMethod != null) { + // When we unbind input, we are unbinding the client, so we always + // unbind ime and a11y together. + curMethod.unbindInput(); + AccessibilityManagerInternal.get().unbindInput(); } - mBoundToAccessibility = false; - mCurClient = null; } + mBoundToAccessibility = false; + mCurClient = null; if (mCurFocusedWindowClient == client) { mCurFocusedWindowClient = null; mCurFocusedWindowEditorInfo = null; @@ -2192,7 +2189,6 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } } - // TODO(b/314150112): Move this to ClientController. @GuardedBy("ImfLock.class") void unbindCurrentClientLocked(@UnbindReason int unbindClientReason) { if (mCurClient != null) { @@ -2883,11 +2879,16 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @GuardedBy("ImfLock.class") void clearClientSessionsLocked() { if (getCurMethodLocked() != null) { - final int numClients = mClientController.mClients.size(); - for (int i = 0; i < numClients; ++i) { - clearClientSessionLocked(mClientController.mClients.valueAt(i)); - clearClientSessionForAccessibilityLocked(mClientController.mClients.valueAt(i)); - } + // TODO(b/322816970): Replace this with lambda. + mClientController.forAllClients(new Consumer<ClientState>() { + + @GuardedBy("ImfLock.class") + @Override + public void accept(ClientState c) { + clearClientSessionLocked(c); + clearClientSessionForAccessibilityLocked(c); + } + }); finishSessionLocked(mEnabledSession); for (int i = 0; i < mEnabledAccessibilitySessions.size(); i++) { @@ -3732,9 +3733,9 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub return InputBindResult.INVALID_USER; } - final ClientState cs = mClientController.mClients.get(client.asBinder()); + final ClientState cs = mClientController.getClient(client.asBinder()); if (cs == null) { - throw new IllegalArgumentException("unknown client " + client.asBinder()); + throw new IllegalArgumentException("Unknown client " + client.asBinder()); } final int imeClientFocus = mWindowManagerInternal.hasInputMethodClientFocus( @@ -3906,8 +3907,7 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // We need to check if this is the current client with // focus in the window manager, to allow this call to // be made before input is started in it. - final ClientState cs = - mClientController.mClients.get(client.asBinder()); + final ClientState cs = mClientController.getClient(client.asBinder()); if (cs == null) { ImeTracker.forLogging().onFailed(statsToken, ImeTracker.PHASE_SERVER_CLIENT_KNOWN); throw new IllegalArgumentException("unknown client " + client.asBinder()); @@ -4518,16 +4518,17 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub @Override public void startImeTrace() { super.startImeTrace_enforcePermission(); - ImeTracing.getInstance().startTrace(null /* printwriter */); - ArrayMap<IBinder, ClientState> clients; synchronized (ImfLock.class) { - clients = new ArrayMap<>(mClientController.mClients); - } - for (ClientState state : clients.values()) { - if (state != null) { - state.mClient.setImeTraceEnabled(true /* enabled */); - } + // TODO(b/322816970): Replace this with lambda. + mClientController.forAllClients(new Consumer<ClientState>() { + + @GuardedBy("ImfLock.class") + @Override + public void accept(ClientState c) { + c.mClient.setImeTraceEnabled(true /* enabled */); + } + }); } } @@ -4538,14 +4539,16 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub super.stopImeTrace_enforcePermission(); ImeTracing.getInstance().stopTrace(null /* printwriter */); - ArrayMap<IBinder, ClientState> clients; synchronized (ImfLock.class) { - clients = new ArrayMap<>(mClientController.mClients); - } - for (ClientState state : clients.values()) { - if (state != null) { - state.mClient.setImeTraceEnabled(false /* enabled */); - } + // TODO(b/322816970): Replace this with lambda. + mClientController.forAllClients(new Consumer<ClientState>() { + + @GuardedBy("ImfLock.class") + @Override + public void accept(ClientState c) { + c.mClient.setImeTraceEnabled(false /* enabled */); + } + }); } } @@ -5779,11 +5782,15 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub // We only have sessions when we bound to an input method. Remove this session // from all clients. if (getCurMethodLocked() != null) { - final int numClients = mClientController.mClients.size(); - for (int i = 0; i < numClients; ++i) { - clearClientSessionForAccessibilityLocked( - mClientController.mClients.valueAt(i), accessibilityConnectionId); - } + // TODO(b/322816970): Replace this with lambda. + mClientController.forAllClients(new Consumer<ClientState>() { + + @GuardedBy("ImfLock.class") + @Override + public void accept(ClientState c) { + clearClientSessionForAccessibilityLocked(c, accessibilityConnectionId); + } + }); AccessibilitySessionState session = mEnabledAccessibilitySessions.get( accessibilityConnectionId); if (session != null) { @@ -5967,19 +5974,26 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub p.println(" InputMethod #" + i + ":"); info.dump(p, " "); } + // Dump ClientController#mClients p.println(" ClientStates:"); - // TODO(b/314150112): move client related dump info to ClientController#dump - final int numClients = mClientController.mClients.size(); - for (int i = 0; i < numClients; ++i) { - final ClientState ci = mClientController.mClients.valueAt(i); - p.println(" " + ci + ":"); - p.println(" client=" + ci.mClient); - p.println(" fallbackInputConnection=" + ci.mFallbackInputConnection); - p.println(" sessionRequested=" + ci.mSessionRequested); - p.println(" sessionRequestedForAccessibility=" - + ci.mSessionRequestedForAccessibility); - p.println(" curSession=" + ci.mCurSession); - } + // TODO(b/322816970): Replace this with lambda. + mClientController.forAllClients(new Consumer<ClientState>() { + + @GuardedBy("ImfLock.class") + @Override + public void accept(ClientState c) { + p.println(" " + c + ":"); + p.println(" client=" + c.mClient); + p.println(" fallbackInputConnection=" + + c.mFallbackInputConnection); + p.println(" sessionRequested=" + + c.mSessionRequested); + p.println( + " sessionRequestedForAccessibility=" + + c.mSessionRequestedForAccessibility); + p.println(" curSession=" + c.mCurSession); + } + }); p.println(" mCurMethodId=" + getSelectedMethodIdLocked()); client = mCurClient; p.println(" mCurClient=" + client + " mCurSeq=" + getSequenceNumberLocked()); @@ -6583,14 +6597,16 @@ public final class InputMethodManagerService extends IInputMethodManager.Stub } } boolean isImeTraceEnabled = ImeTracing.getInstance().isEnabled(); - ArrayMap<IBinder, ClientState> clients; synchronized (ImfLock.class) { - clients = new ArrayMap<>(mClientController.mClients); - } - for (ClientState state : clients.values()) { - if (state != null) { - state.mClient.setImeTraceEnabled(isImeTraceEnabled); - } + // TODO(b/322816970): Replace this with lambda. + mClientController.forAllClients(new Consumer<ClientState>() { + + @GuardedBy("ImfLock.class") + @Override + public void accept(ClientState c) { + c.mClient.setImeTraceEnabled(isImeTraceEnabled); + } + }); } return ShellCommandResult.SUCCESS; } diff --git a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java index cc205d4a53bd..cc58f38db65a 100644 --- a/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java +++ b/services/core/java/com/android/server/locksettings/SyntheticPasswordManager.java @@ -1541,8 +1541,14 @@ class SyntheticPasswordManager { */ public @NonNull AuthenticationResult unlockTokenBasedProtector( IGateKeeperService gatekeeper, long protectorId, byte[] token, int userId) { - SyntheticPasswordBlob blob = SyntheticPasswordBlob.fromBytes(loadState(SP_BLOB_NAME, - protectorId, userId)); + byte[] data = loadState(SP_BLOB_NAME, protectorId, userId); + if (data == null) { + AuthenticationResult result = new AuthenticationResult(); + result.gkResponse = VerifyCredentialResponse.ERROR; + Slogf.w(TAG, "spblob not found for protector %016x, user %d", protectorId, userId); + return result; + } + SyntheticPasswordBlob blob = SyntheticPasswordBlob.fromBytes(data); return unlockTokenBasedProtectorInternal(gatekeeper, protectorId, blob.mProtectorType, token, userId); } diff --git a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java index 978f46808e3b..bbb19e351b5d 100644 --- a/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java +++ b/services/core/java/com/android/server/media/projection/MediaProjectionManagerService.java @@ -35,6 +35,7 @@ import android.annotation.EnforcePermission; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManagerInternal; +import android.app.ActivityOptions.LaunchCookie; import android.app.AppOpsManager; import android.app.IProcessObserver; import android.app.compat.CompatChanges; @@ -548,8 +549,11 @@ public final class MediaProjectionManagerService extends SystemService DEFAULT_DISPLAY)); break; case RECORD_CONTENT_TASK: - setReviewedConsentSessionLocked(ContentRecordingSession.createTaskSession( - mProjectionGrant.getLaunchCookie())); + IBinder taskWindowContainerToken = + mProjectionGrant.getLaunchCookie() == null ? null + : mProjectionGrant.getLaunchCookie().binder; + setReviewedConsentSessionLocked( + ContentRecordingSession.createTaskSession(taskWindowContainerToken)); break; } } @@ -973,7 +977,7 @@ public final class MediaProjectionManagerService extends SystemService private IBinder mToken; private IBinder.DeathRecipient mDeathEater; private boolean mRestoreSystemAlertWindow; - private IBinder mLaunchCookie = null; + private LaunchCookie mLaunchCookie = null; // Values for tracking token validity. // Timeout value to compare creation time against. @@ -1186,14 +1190,14 @@ public final class MediaProjectionManagerService extends SystemService @android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_MEDIA_PROJECTION) @Override // Binder call - public void setLaunchCookie(IBinder launchCookie) { + public void setLaunchCookie(LaunchCookie launchCookie) { setLaunchCookie_enforcePermission(); mLaunchCookie = launchCookie; } @android.annotation.EnforcePermission(android.Manifest.permission.MANAGE_MEDIA_PROJECTION) @Override // Binder call - public IBinder getLaunchCookie() { + public LaunchCookie getLaunchCookie() { getLaunchCookie_enforcePermission(); return mLaunchCookie; } diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java index 425659e38492..4da2cc9bbe20 100755 --- a/services/core/java/com/android/server/notification/NotificationManagerService.java +++ b/services/core/java/com/android/server/notification/NotificationManagerService.java @@ -10869,6 +10869,7 @@ public class NotificationManagerService extends SystemService { ArrayList<Notification.Action> smartActions = record.getSystemGeneratedSmartActions(); ArrayList<CharSequence> smartReplies = record.getSmartReplies(); if (redactSensitiveNotificationsFromUntrustedListeners() + && info != null && !mListeners.isUidTrusted(info.uid) && mListeners.hasSensitiveContent(record)) { smartActions = null; diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java index b5346a351f38..bc0173a90989 100644 --- a/services/core/java/com/android/server/pm/LauncherAppsService.java +++ b/services/core/java/com/android/server/pm/LauncherAppsService.java @@ -18,6 +18,10 @@ package com.android.server.pm; import static android.Manifest.permission.READ_FRAME_BUFFER; import static android.app.ActivityOptions.KEY_SPLASH_SCREEN_THEME; +import static android.app.AppOpsManager.MODE_ALLOWED; +import static android.app.AppOpsManager.MODE_IGNORED; +import static android.app.AppOpsManager.OP_ARCHIVE_ICON_OVERLAY; +import static android.app.AppOpsManager.OP_UNARCHIVAL_CONFIRMATION; import static android.app.ComponentOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED; import static android.app.ComponentOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED; import static android.app.PendingIntent.FLAG_IMMUTABLE; @@ -43,6 +47,7 @@ import android.app.ActivityManager; import android.app.ActivityManagerInternal; import android.app.ActivityOptions; import android.app.AppGlobals; +import android.app.AppOpsManager; import android.app.IApplicationThread; import android.app.PendingIntent; import android.app.admin.DevicePolicyCache; @@ -213,6 +218,7 @@ public class LauncherAppsService extends SystemService { private final ActivityTaskManagerInternal mActivityTaskManagerInternal; private final ShortcutServiceInternal mShortcutServiceInternal; private final PackageManagerInternal mPackageManagerInternal; + private final AppOpsManager mAppOpsManager; private final PackageCallbackList<IOnAppsChangedListener> mListeners = new PackageCallbackList<IOnAppsChangedListener>(); private final DevicePolicyManager mDpm; @@ -253,6 +259,7 @@ public class LauncherAppsService extends SystemService { LocalServices.getService(ShortcutServiceInternal.class)); mPackageManagerInternal = Objects.requireNonNull( LocalServices.getService(PackageManagerInternal.class)); + mAppOpsManager = mContext.getSystemService(AppOpsManager.class); mShortcutServiceInternal.addListener(mPackageMonitor); mShortcutChangeHandler = new ShortcutChangeHandler(mUserManagerInternal); mShortcutServiceInternal.addShortcutChangeCallback(mShortcutChangeHandler); @@ -1997,6 +2004,23 @@ public class LauncherAppsService extends SystemService { } } + @Override + public void setArchiveCompatibilityOptions(boolean enableIconOverlay, + boolean enableUnarchivalConfirmation) { + int callingUid = Binder.getCallingUid(); + Binder.withCleanCallingIdentity( + () -> { + mAppOpsManager.setUidMode( + OP_ARCHIVE_ICON_OVERLAY, + callingUid, + enableIconOverlay ? MODE_ALLOWED : MODE_IGNORED); + mAppOpsManager.setUidMode( + OP_UNARCHIVAL_CONFIRMATION, + callingUid, + enableUnarchivalConfirmation ? MODE_ALLOWED : MODE_IGNORED); + }); + } + /** Checks if user is a profile of or same as listeningUser. * and the user is enabled. */ private boolean isEnabledProfileOf(UserHandle listeningUser, UserHandle user, diff --git a/services/core/java/com/android/server/pm/PackageArchiver.java b/services/core/java/com/android/server/pm/PackageArchiver.java index 32f56463c8de..474b5907524c 100644 --- a/services/core/java/com/android/server/pm/PackageArchiver.java +++ b/services/core/java/com/android/server/pm/PackageArchiver.java @@ -20,6 +20,7 @@ import static android.app.ActivityManager.START_ABORTED; import static android.app.ActivityManager.START_CLASS_NOT_FOUND; import static android.app.ActivityManager.START_PERMISSION_DENIED; import static android.app.ActivityManager.START_SUCCESS; +import static android.app.AppOpsManager.MODE_ALLOWED; import static android.app.AppOpsManager.MODE_IGNORED; import static android.app.ComponentOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED; import static android.content.pm.ArchivedActivityInfo.bytesFromBitmap; @@ -275,11 +276,12 @@ public class PackageArchiver { Slog.i(TAG, TextUtils.formatSimple("Unarchival is starting for: %s", packageName)); try { - // TODO(b/311709794) Make showUnarchivalConfirmation dependent on the compat options. requestUnarchive(packageName, callerPackageName, getOrCreateLauncherListener(userId, packageName), UserHandle.of(userId), - false /* showUnarchivalConfirmation= */); + getAppOpsManager().checkOp( + AppOpsManager.OP_UNARCHIVAL_CONFIRMATION, callingUid, callerPackageName) + == MODE_ALLOWED); } catch (Throwable t) { Slog.e(TAG, TextUtils.formatSimple( "Unexpected error occurred while unarchiving package %s: %s.", packageName, @@ -796,7 +798,8 @@ public class PackageArchiver { * <p> The icon is returned without any treatment/overlay. In the rare case the app had multiple * launcher activities, only one of the icons is returned arbitrarily. */ - public Bitmap getArchivedAppIcon(@NonNull String packageName, @NonNull UserHandle user) { + public Bitmap getArchivedAppIcon(@NonNull String packageName, @NonNull UserHandle user, + String callingPackageName) { Objects.requireNonNull(packageName); Objects.requireNonNull(user); @@ -819,7 +822,13 @@ public class PackageArchiver { // TODO(b/298452477) Handle monochrome icons. // In the rare case the archived app defined more than two launcher activities, we choose // the first one arbitrarily. - return includeCloudOverlay(decodeIcon(archiveState.getActivityInfos().get(0))); + Bitmap icon = decodeIcon(archiveState.getActivityInfos().get(0)); + if (getAppOpsManager().checkOp( + AppOpsManager.OP_ARCHIVE_ICON_OVERLAY, callingUid, callingPackageName) + == MODE_ALLOWED) { + icon = includeCloudOverlay(icon); + } + return icon; } /** diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java index 135bd4f911f9..dfe705a7a065 100644 --- a/services/core/java/com/android/server/pm/PackageManagerService.java +++ b/services/core/java/com/android/server/pm/PackageManagerService.java @@ -6414,8 +6414,10 @@ public class PackageManagerService implements PackageSender, TestUtilityService } @Override - public Bitmap getArchivedAppIcon(@NonNull String packageName, @NonNull UserHandle user) { - return mInstallerService.mPackageArchiver.getArchivedAppIcon(packageName, user); + public Bitmap getArchivedAppIcon(@NonNull String packageName, @NonNull UserHandle user, + @NonNull String callingPackageName) { + return mInstallerService.mPackageArchiver.getArchivedAppIcon(packageName, user, + callingPackageName); } @Override @@ -7807,7 +7809,8 @@ public class PackageManagerService implements PackageSender, TestUtilityService mResolveActivity.launchMode = ActivityInfo.LAUNCH_MULTIPLE; mResolveActivity.flags = ActivityInfo.FLAG_EXCLUDE_FROM_RECENTS | ActivityInfo.FLAG_FINISH_ON_CLOSE_SYSTEM_DIALOGS - | ActivityInfo.FLAG_CAN_DISPLAY_ON_REMOTE_DEVICES; + | ActivityInfo.FLAG_CAN_DISPLAY_ON_REMOTE_DEVICES + | ActivityInfo.FLAG_HARDWARE_ACCELERATED; mResolveActivity.theme = 0; mResolveActivity.exported = true; mResolveActivity.enabled = true; @@ -7841,7 +7844,8 @@ public class PackageManagerService implements PackageSender, TestUtilityService mResolveActivity.documentLaunchMode = ActivityInfo.DOCUMENT_LAUNCH_NEVER; mResolveActivity.flags = ActivityInfo.FLAG_EXCLUDE_FROM_RECENTS | ActivityInfo.FLAG_RELINQUISH_TASK_IDENTITY - | ActivityInfo.FLAG_CAN_DISPLAY_ON_REMOTE_DEVICES; + | ActivityInfo.FLAG_CAN_DISPLAY_ON_REMOTE_DEVICES + | ActivityInfo.FLAG_HARDWARE_ACCELERATED; mResolveActivity.theme = R.style.Theme_Material_Dialog_Alert; mResolveActivity.exported = true; mResolveActivity.enabled = true; diff --git a/services/core/java/com/android/server/pm/Settings.java b/services/core/java/com/android/server/pm/Settings.java index 7cf1d3323f05..1ec37f4909a7 100644 --- a/services/core/java/com/android/server/pm/Settings.java +++ b/services/core/java/com/android/server/pm/Settings.java @@ -4537,6 +4537,8 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile t.traceBegin("createNewUser-" + userHandle); Installer.Batch batch = new Installer.Batch(); final boolean skipPackageAllowList = userTypeInstallablePackages == null; + // Use the same timestamp for all system apps that are to be installed on the new user + final long currentTimeMillis = System.currentTimeMillis(); synchronized (mLock) { final int size = mPackages.size(); for (int i = 0; i < size; i++) { @@ -4552,6 +4554,9 @@ public final class Settings implements Watchable, Snappable, ResilientAtomicFile ps.getPackageName())); // Only system apps are initially installed. ps.setInstalled(shouldReallyInstall, userHandle); + if (Flags.fixSystemAppsFirstInstallTime() && shouldReallyInstall) { + ps.setFirstInstallTime(currentTimeMillis, userHandle); + } // Non-Apex system apps, that are not included in the allowlist in // initialNonStoppedSystemPackages, should be marked as stopped by default. diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java index 1fdcc64a90c8..51790b875465 100644 --- a/services/core/java/com/android/server/policy/PhoneWindowManager.java +++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java @@ -75,6 +75,7 @@ import static android.view.WindowManagerGlobal.ADD_OKAY; import static android.view.WindowManagerGlobal.ADD_PERMISSION_DENIED; import static android.view.contentprotection.flags.Flags.createAccessibilityOverlayAppOpEnabled; +import static com.android.hardware.input.Flags.emojiAndScreenshotKeycodesAvailable; import static com.android.internal.config.sysui.SystemUiDeviceConfigFlags.SCREENSHOT_KEYCHORD_DELAY; import static com.android.server.policy.WindowManagerPolicy.WindowManagerFuncs.CAMERA_LENS_COVERED; import static com.android.server.policy.WindowManagerPolicy.WindowManagerFuncs.CAMERA_LENS_COVER_ABSENT; @@ -3779,6 +3780,11 @@ public class PhoneWindowManager implements WindowManagerPolicy { sendSystemKeyToStatusBarAsync(event); return true; } + case KeyEvent.KEYCODE_SCREENSHOT: + if (emojiAndScreenshotKeycodesAvailable() && down && repeatCount == 0) { + interceptScreenshotChord(SCREENSHOT_KEY_OTHER, 0 /*pressDelay*/); + } + return true; } if (isValidGlobalKey(keyCode) && mGlobalKeyManager.handleGlobalKey(mContext, keyCode, event)) { @@ -5022,6 +5028,12 @@ public class PhoneWindowManager implements WindowManagerPolicy { case KeyEvent.KEYCODE_MACRO_4: result &= ~ACTION_PASS_TO_USER; break; + case KeyEvent.KEYCODE_EMOJI_PICKER: + if (!emojiAndScreenshotKeycodesAvailable()) { + // Don't allow EMOJI_PICKER key to be dispatched until flag is released. + result &= ~ACTION_PASS_TO_USER; + } + break; } if (useHapticFeedback) { diff --git a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java index 09b19e6196a1..25e749f08782 100644 --- a/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java +++ b/services/core/java/com/android/server/power/stats/BatteryStatsImpl.java @@ -138,6 +138,7 @@ import com.android.internal.util.XmlUtils; import com.android.modules.utils.TypedXmlPullParser; import com.android.modules.utils.TypedXmlSerializer; import com.android.net.module.util.NetworkCapabilitiesUtils; +import com.android.server.power.optimization.Flags; import com.android.server.power.stats.SystemServerCpuThreadReader.SystemServiceCpuThreadTimes; import libcore.util.EmptyArray; @@ -185,7 +186,8 @@ public class BatteryStatsImpl extends BatteryStats { // TODO: remove "tcp" from network methods, since we measure total stats. // Current on-disk Parcel version. Must be updated when the format of the parcelable changes - public static final int VERSION = 214; + public static final int VERSION = + !Flags.disableSystemServicePowerAttr() ? 214 : 215; // The maximum number of names wakelocks we will keep track of // per uid; once the limit is reached, we batch the remaining wakelocks @@ -1753,7 +1755,9 @@ public class BatteryStatsImpl extends BatteryStats { mCpuUidActiveTimeReader = new KernelCpuUidActiveTimeReader(true, mClock); mCpuUidClusterTimeReader = new KernelCpuUidClusterTimeReader(true, mClock); mKernelWakelockReader = new KernelWakelockReader(); - mSystemServerCpuThreadReader = SystemServerCpuThreadReader.create(); + if (!Flags.disableSystemServicePowerAttr()) { + mSystemServerCpuThreadReader = SystemServerCpuThreadReader.create(); + } mKernelMemoryBandwidthStats = new KernelMemoryBandwidthStats(); mTmpRailStats = new RailStats(); } @@ -11459,7 +11463,7 @@ public class BatteryStatsImpl extends BatteryStats { @Override public BatteryStatsHistoryIterator iterateBatteryStatsHistory(long startTimeMs, long endTimeMs) { - return mHistory.copy().iterate(startTimeMs, endTimeMs); + return mHistory.iterate(startTimeMs, endTimeMs); } @Override @@ -11702,7 +11706,9 @@ public class BatteryStatsImpl extends BatteryStats { EnergyConsumerStats.resetIfNotNull(mGlobalEnergyConsumerStats); - resetIfNotNull(mBinderThreadCpuTimesUs, false, elapsedRealtimeUs); + if (!Flags.disableSystemServicePowerAttr()) { + resetIfNotNull(mBinderThreadCpuTimesUs, false, elapsedRealtimeUs); + } mNumAllUidCpuTimeReads = 0; mNumUidsRemoved = 0; @@ -13676,7 +13682,9 @@ public class BatteryStatsImpl extends BatteryStats { mKernelCpuSpeedReaders[i].readDelta(); } } - mSystemServerCpuThreadReader.readDelta(); + if (!Flags.disableSystemServicePowerAttr()) { + mSystemServerCpuThreadReader.readDelta(); + } return; } @@ -15696,23 +15704,25 @@ public class BatteryStatsImpl extends BatteryStats { } } - updateSystemServiceCallStats(); - if (mBinderThreadCpuTimesUs != null) { - pw.println("Per UID System server binder time in ms:"); - long[] systemServiceTimeAtCpuSpeeds = getSystemServiceTimeAtCpuSpeeds(); - for (int i = 0; i < size; i++) { - int u = mUidStats.keyAt(i); - Uid uid = mUidStats.get(u); - double proportionalSystemServiceUsage = uid.getProportionalSystemServiceUsage(); - long timeUs = 0; - for (int j = systemServiceTimeAtCpuSpeeds.length - 1; j >= 0; j--) { - timeUs += systemServiceTimeAtCpuSpeeds[j] * proportionalSystemServiceUsage; - } + if (!Flags.disableSystemServicePowerAttr()) { + updateSystemServiceCallStats(); + if (mBinderThreadCpuTimesUs != null) { + pw.println("Per UID System server binder time in ms:"); + long[] systemServiceTimeAtCpuSpeeds = getSystemServiceTimeAtCpuSpeeds(); + for (int i = 0; i < size; i++) { + int u = mUidStats.keyAt(i); + Uid uid = mUidStats.get(u); + double proportionalSystemServiceUsage = uid.getProportionalSystemServiceUsage(); + long timeUs = 0; + for (int j = systemServiceTimeAtCpuSpeeds.length - 1; j >= 0; j--) { + timeUs += systemServiceTimeAtCpuSpeeds[j] * proportionalSystemServiceUsage; + } - pw.print(" "); - pw.print(u); - pw.print(": "); - pw.println(timeUs / 1000); + pw.print(" "); + pw.print(u); + pw.print(": "); + pw.println(timeUs / 1000); + } } } } @@ -16428,8 +16438,10 @@ public class BatteryStatsImpl extends BatteryStats { } } - mBinderThreadCpuTimesUs = - LongSamplingCounterArray.readSummaryFromParcelLocked(in, mOnBatteryTimeBase); + if (!Flags.disableSystemServicePowerAttr()) { + mBinderThreadCpuTimesUs = + LongSamplingCounterArray.readSummaryFromParcelLocked(in, mOnBatteryTimeBase); + } } /** @@ -16973,7 +16985,9 @@ public class BatteryStatsImpl extends BatteryStats { } } - LongSamplingCounterArray.writeSummaryToParcelLocked(out, mBinderThreadCpuTimesUs); + if (!Flags.disableSystemServicePowerAttr()) { + LongSamplingCounterArray.writeSummaryToParcelLocked(out, mBinderThreadCpuTimesUs); + } } @GuardedBy("this") @@ -16985,7 +16999,9 @@ public class BatteryStatsImpl extends BatteryStats { // if we had originally pulled a time before the RTC was set. getStartClockTime(); - updateSystemServiceCallStats(); + if (!Flags.disableSystemServicePowerAttr()) { + updateSystemServiceCallStats(); + } } @GuardedBy("this") diff --git a/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java b/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java index c3221e4929bd..30b80ae781ff 100644 --- a/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java +++ b/services/core/java/com/android/server/power/stats/BatteryUsageStatsProvider.java @@ -96,11 +96,13 @@ public class BatteryUsageStatsProvider { mPowerCalculators.add(new CustomEnergyConsumerPowerCalculator(mPowerProfile)); mPowerCalculators.add(new UserPowerCalculator()); - // It is important that SystemServicePowerCalculator be applied last, - // because it re-attributes some of the power estimated by the other - // calculators. - mPowerCalculators.add( - new SystemServicePowerCalculator(mCpuScalingPolicies, mPowerProfile)); + if (!com.android.server.power.optimization.Flags.disableSystemServicePowerAttr()) { + // It is important that SystemServicePowerCalculator be applied last, + // because it re-attributes some of the power estimated by the other + // calculators. + mPowerCalculators.add( + new SystemServicePowerCalculator(mCpuScalingPolicies, mPowerProfile)); + } } } return mPowerCalculators; diff --git a/services/core/java/com/android/server/power/stats/PowerStatsAggregator.java b/services/core/java/com/android/server/power/stats/PowerStatsAggregator.java index cd3db36662d6..ba4c127ac3d0 100644 --- a/services/core/java/com/android/server/power/stats/PowerStatsAggregator.java +++ b/services/core/java/com/android/server/power/stats/PowerStatsAggregator.java @@ -74,8 +74,7 @@ public class PowerStatsAggregator { boolean clockUpdateAdded = false; long baseTime = startTimeMs > 0 ? startTimeMs : UNINITIALIZED; long lastTime = 0; - try (BatteryStatsHistoryIterator iterator = - mHistory.copy().iterate(startTimeMs, endTimeMs)) { + try (BatteryStatsHistoryIterator iterator = mHistory.iterate(startTimeMs, endTimeMs)) { while (iterator.hasNext()) { BatteryStats.HistoryItem item = iterator.next(); diff --git a/services/core/java/com/android/server/power/stats/flags.aconfig b/services/core/java/com/android/server/power/stats/flags.aconfig index 0f135715ebc3..65466461c82e 100644 --- a/services/core/java/com/android/server/power/stats/flags.aconfig +++ b/services/core/java/com/android/server/power/stats/flags.aconfig @@ -13,3 +13,11 @@ flag { description: "Feature flag for streamlined battery stats" bug: "285646152" } + +flag { + name: "disable_system_service_power_attr" + namespace: "backstage_power" + description: "Deprecation of system service power re-attribution" + bug: "311793616" + is_fixed_read_only: true +} diff --git a/services/core/java/com/android/server/servicewatcher/CurrentUserServiceSupplier.java b/services/core/java/com/android/server/servicewatcher/CurrentUserServiceSupplier.java index 6677e7eb320c..152623090314 100644 --- a/services/core/java/com/android/server/servicewatcher/CurrentUserServiceSupplier.java +++ b/services/core/java/com/android/server/servicewatcher/CurrentUserServiceSupplier.java @@ -36,10 +36,12 @@ import android.content.IntentFilter; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.content.res.Resources; +import android.location.flags.Flags; import android.os.Bundle; import android.os.Process; import android.os.UserHandle; import android.util.Log; +import android.util.TypedValue; import com.android.internal.util.Preconditions; import com.android.server.FgThread; @@ -70,6 +72,10 @@ public final class CurrentUserServiceSupplier extends BroadcastReceiver implemen private static final String EXTRA_SERVICE_VERSION = "serviceVersion"; private static final String EXTRA_SERVICE_IS_MULTIUSER = "serviceIsMultiuser"; + // a package value that will never match against any package (we can't use null since this will + // match against any package). + private static final String NO_MATCH_PACKAGE = ""; + private static final Comparator<BoundServiceInfo> sBoundServiceInfoComparator = (o1, o2) -> { if (o1 == o2) { return 0; @@ -196,7 +202,19 @@ public final class CurrentUserServiceSupplier extends BroadcastReceiver implemen Resources resources = context.getResources(); boolean enableOverlay = resources.getBoolean(enableOverlayResId); if (!enableOverlay) { - return resources.getString(nonOverlayPackageResId); + if (Flags.fixServiceWatcher()) { + // we don't use getText() or similar because it won't return null values + TypedValue out = new TypedValue(); + resources.getValue(nonOverlayPackageResId, out, true); + CharSequence explicitPackage = out.coerceToString(); + if (explicitPackage == null) { + return NO_MATCH_PACKAGE; + } else { + return explicitPackage.toString(); + } + } else { + return resources.getString(nonOverlayPackageResId); + } } else { return null; } @@ -233,6 +251,10 @@ public final class CurrentUserServiceSupplier extends BroadcastReceiver implemen @Override public boolean hasMatchingService() { + if (Flags.fixServiceWatcher() && NO_MATCH_PACKAGE.equals(mIntent.getPackage())) { + return false; + } + int intentQueryFlags = MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE; if (mMatchSystemAppsOnly) { intentQueryFlags |= MATCH_SYSTEM_ONLY; @@ -268,6 +290,10 @@ public final class CurrentUserServiceSupplier extends BroadcastReceiver implemen @Override public BoundServiceInfo getServiceInfo() { + if (Flags.fixServiceWatcher() && NO_MATCH_PACKAGE.equals(mIntent.getPackage())) { + return null; + } + BoundServiceInfo bestServiceInfo = null; // only allow services in the correct direct boot state to match diff --git a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java index 285bcc328c0c..0ffd002197c4 100644 --- a/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java +++ b/services/core/java/com/android/server/stats/pull/StatsPullAtomService.java @@ -2058,7 +2058,8 @@ public class StatsPullAtomService extends SystemService { } private void registerCpuCyclesPerThreadGroupCluster() { - if (KernelCpuBpfTracking.isSupported()) { + if (KernelCpuBpfTracking.isSupported() + && !com.android.server.power.optimization.Flags.disableSystemServicePowerAttr()) { int tagId = FrameworkStatsLog.CPU_CYCLES_PER_THREAD_GROUP_CLUSTER; PullAtomMetadata metadata = new PullAtomMetadata.Builder() .setAdditiveFields(new int[]{3, 4}) @@ -2073,6 +2074,10 @@ public class StatsPullAtomService extends SystemService { } int pullCpuCyclesPerThreadGroupCluster(int atomTag, List<StatsEvent> pulledData) { + if (com.android.server.power.optimization.Flags.disableSystemServicePowerAttr()) { + return StatsManager.PULL_SKIP; + } + SystemServiceCpuThreadTimes times = LocalServices.getService(BatteryStatsInternal.class) .getSystemServiceCpuThreadTimes(); if (times == null) { diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java index b271a03c109d..a4c6959f8ad5 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerInternal.java @@ -18,6 +18,7 @@ package com.android.server.statusbar; import android.annotation.Nullable; import android.app.ITransientNotificationCallback; +import android.content.ComponentName; import android.hardware.fingerprint.IUdfpsRefreshRateRequestCallback; import android.os.Bundle; import android.os.IBinder; @@ -241,4 +242,17 @@ public interface StatusBarManagerInternal { * @see com.android.internal.statusbar.IStatusBar#showMediaOutputSwitcher */ void showMediaOutputSwitcher(String packageName); + + /** + * Add a tile to the Quick Settings Panel + * @param tile the ComponentName of the {@link android.service.quicksettings.TileService} + * @param end if true, the tile will be added at the end. If false, at the beginning. + */ + void addQsTileToFrontOrEnd(ComponentName tile, boolean end); + + /** + * Remove the tile from the Quick Settings Panel + * @param tile the ComponentName of the {@link android.service.quicksettings.TileService} + */ + void removeQsTile(ComponentName tile); } diff --git a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java index b21721ad7b45..49553586ccd7 100644 --- a/services/core/java/com/android/server/statusbar/StatusBarManagerService.java +++ b/services/core/java/com/android/server/statusbar/StatusBarManagerService.java @@ -89,6 +89,7 @@ import android.view.WindowInsets; import android.view.WindowInsets.Type.InsetsType; import android.view.WindowInsetsController.Appearance; import android.view.WindowInsetsController.Behavior; +import android.view.accessibility.Flags; import com.android.internal.R; import com.android.internal.annotations.GuardedBy; @@ -834,6 +835,20 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D } } } + + @Override + public void addQsTileToFrontOrEnd(ComponentName tile, boolean end) { + if (Flags.a11yQsShortcut()) { + StatusBarManagerService.this.addQsTileToFrontOrEnd(tile, end); + } + } + + @Override + public void removeQsTile(ComponentName tile) { + if (Flags.a11yQsShortcut()) { + StatusBarManagerService.this.remTile(tile); + } + } }; private final GlobalActionsProvider mGlobalActionsProvider = new GlobalActionsProvider() { @@ -934,11 +949,26 @@ public class StatusBarManagerService extends IStatusBarService.Stub implements D } public void addTile(ComponentName component) { + if (Flags.a11yQsShortcut()) { + addQsTileToFrontOrEnd(component, false); + } else { + enforceStatusBarOrShell(); + + if (mBar != null) { + try { + mBar.addQsTile(component); + } catch (RemoteException ex) { + } + } + } + } + + private void addQsTileToFrontOrEnd(ComponentName tile, boolean end) { enforceStatusBarOrShell(); if (mBar != null) { try { - mBar.addQsTile(component); + mBar.addQsTileToFrontOrEnd(tile, end); } catch (RemoteException ex) { } } diff --git a/services/core/java/com/android/server/trust/TrustAgentWrapper.java b/services/core/java/com/android/server/trust/TrustAgentWrapper.java index 3abebf8c381c..d10205401fe7 100644 --- a/services/core/java/com/android/server/trust/TrustAgentWrapper.java +++ b/services/core/java/com/android/server/trust/TrustAgentWrapper.java @@ -443,6 +443,8 @@ public class TrustAgentWrapper { mPendingSuccessfulUnlock = false; } + // It's okay to use the "Inner" version of isDeviceLocked since they differ only for + // profiles, which cannot be switched to and thus don't support trust agents anyway. if (mTrustManagerService.isDeviceLockedInner(mUserId)) { onDeviceLocked(); } else { diff --git a/services/core/java/com/android/server/trust/TrustManagerService.java b/services/core/java/com/android/server/trust/TrustManagerService.java index e5a8a6dd2a3a..2b05993c86ba 100644 --- a/services/core/java/com/android/server/trust/TrustManagerService.java +++ b/services/core/java/com/android/server/trust/TrustManagerService.java @@ -83,7 +83,6 @@ import com.android.internal.infra.AndroidFuture; import com.android.internal.util.DumpUtils; import com.android.internal.widget.LockPatternUtils; import com.android.server.SystemService; -import com.android.server.companion.virtual.VirtualDeviceManagerInternal; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; @@ -160,7 +159,6 @@ public class TrustManagerService extends SystemService { private final ActivityManager mActivityManager; private FingerprintManager mFingerprintManager; private FaceManager mFaceManager; - private VirtualDeviceManagerInternal mVirtualDeviceManager; private enum TrustState { // UNTRUSTED means that TrustManagerService is currently *not* giving permission for the @@ -190,25 +188,30 @@ public class TrustManagerService extends SystemService { new SparseArray<>(); /** - * Stores the locked state for users on the device. There are three different type of users + * Stores the locked state for users on the device. There are several different types of users * which are handled slightly differently: * <ul> - * <li> Users with real keyguard + * <li> Users with real keyguard: * These are users who can be switched to ({@link UserInfo#supportsSwitchToByUser()}). Their * locked state is derived by a combination of user secure state, keyguard state, trust agent * decision and biometric authentication result. These are updated via * {@link #refreshDeviceLockedForUser(int)} and result stored in {@link #mDeviceLockedForUser}. - * <li> Managed profiles with unified challenge - * Managed profile with unified challenge always shares the same locked state as their parent, + * <li> Profiles with unified challenge: + * Profiles with a unified challenge always share the same locked state as their parent, * so their locked state is not recorded in {@link #mDeviceLockedForUser}. Instead, * {@link ITrustManager#isDeviceLocked(int)} always resolves their parent user handle and * queries its locked state instead. - * <li> Managed profiles with separate challenge - * Locked state for profile with separate challenge is determined by other parts of the - * framework (mostly PowerManager) and pushed to TrustManagerService via - * {@link ITrustManager#setDeviceLockedForUser(int, boolean)}. Although in a corner case when - * the profile has a separate but empty challenge, setting its {@link #mDeviceLockedForUser} to - * {@code false} is actually done by {@link #refreshDeviceLockedForUser(int)}. + * <li> Profiles without unified challenge: + * The locked state for profiles that do not have a unified challenge (e.g. they have a + * separate challenge from their parent, or they have no parent at all) is determined by other + * parts of the framework (mostly PowerManager) and pushed to TrustManagerService via + * {@link ITrustManager#setDeviceLockedForUser(int, boolean)}. + * However, in the case where such a profile has an empty challenge, setting its + * {@link #mDeviceLockedForUser} to {@code false} is actually done by + * {@link #refreshDeviceLockedForUser(int)}. + * (This serves as a corner case for managed profiles with a separate but empty challenge. It + * is always currently the case for Communal profiles, for which having a non-empty challenge + * is not currently supported.) * </ul> * TODO: Rename {@link ITrustManager#setDeviceLockedForUser(int, boolean)} to * {@code setDeviceLockedForProfile} to better reflect its purpose. Unifying @@ -796,7 +799,7 @@ public class TrustManagerService extends SystemService { /** * Update the user's locked state. Only applicable to users with a real keyguard - * ({@link UserInfo#supportsSwitchToByUser}) and unsecured managed profiles. + * ({@link UserInfo#supportsSwitchToByUser}) and unsecured profiles. * * If this is called due to an unlock operation set unlockedUser to prevent the lock from * being prematurely reset for that user while keyguard is still in the process of going away. @@ -828,7 +831,11 @@ public class TrustManagerService extends SystemService { boolean secure = mLockPatternUtils.isSecure(id); if (!info.supportsSwitchToByUser()) { - if (info.isManagedProfile() && !secure) { + if (info.isProfile() && !secure + && !mLockPatternUtils.isProfileWithUnifiedChallenge(id)) { + // Unsecured profiles need to be explicitly set to false. + // However, Unified challenge profiles officially shouldn't have a presence in + // mDeviceLockedForUser at all, since that's not how they're tracked. setDeviceLockedForUser(id, false); } continue; @@ -1855,6 +1862,7 @@ public class TrustManagerService extends SystemService { } } + /** If the userId has a parent, returns that parent's userId. Otherwise userId is returned. */ private int resolveProfileParent(int userId) { final long identity = Binder.clearCallingIdentity(); try { diff --git a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java index b6d0ca19d484..eacd3f8d4d86 100644 --- a/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java +++ b/services/core/java/com/android/server/tv/interactive/TvInteractiveAppManagerService.java @@ -4152,6 +4152,25 @@ public class TvInteractiveAppManagerService extends SystemService { } @Override + public void onRequestSigning2(String id, String algorithm, String host, + int port, byte[] data) { + synchronized (mLock) { + if (DEBUG) { + Slogf.d(TAG, "onRequestSigning"); + } + if (mSessionState.mSession == null || mSessionState.mClient == null) { + return; + } + try { + mSessionState.mClient.onRequestSigning2( + id, algorithm, host, port, data, mSessionState.mSeq); + } catch (RemoteException e) { + Slogf.e(TAG, "error in onRequestSigning", e); + } + } + } + + @Override public void onRequestCertificate(String host, int port) { synchronized (mLock) { if (DEBUG) { diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 03d55d9edfda..b1d04c9ddb16 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -545,9 +545,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A boolean launchFailed; // set if a launched failed, to abort on 2nd try boolean delayedResume; // not yet resumed because of stopped app switches? boolean finishing; // activity in pending finish list? - boolean deferRelaunchUntilPaused; // relaunch of activity is being deferred until pause is - // completed - boolean preserveWindowOnDeferredRelaunch; // activity windows are preserved on deferred relaunch int configChangeFlags; // which config values have changed private boolean keysPaused; // has key dispatching been paused for it? int launchMode; // the launch mode activity attribute. @@ -1277,10 +1274,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (mDeferHidingClient) { pw.println(prefix + "mDeferHidingClient=" + mDeferHidingClient); } - if (deferRelaunchUntilPaused || configChangeFlags != 0) { - pw.print(prefix); pw.print("deferRelaunchUntilPaused="); - pw.print(deferRelaunchUntilPaused); - pw.print(" configChangeFlags="); + if (configChangeFlags != 0) { + pw.print(prefix); pw.print(" configChangeFlags="); pw.println(Integer.toHexString(configChangeFlags)); } if (mServiceConnectionsHolder != null) { @@ -2137,7 +2132,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A launchFailed = false; delayedResume = false; finishing = false; - deferRelaunchUntilPaused = false; keysPaused = false; inHistory = false; nowVisible = false; @@ -4096,8 +4090,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // Clean up the splash screen if it was still displayed. cleanUpSplashScreen(); - deferRelaunchUntilPaused = false; - if (setState) { setState(DESTROYED, "cleanUp"); if (DEBUG_APP) Slog.v(TAG_APP, "Clearing app during cleanUp for activity " + this); @@ -6481,9 +6473,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A mAppStopped = true; ProtoLog.v(WM_DEBUG_STATES, "Stop failed; moving to STOPPED: %s", this); setState(STOPPED, "stopIfPossible"); - if (deferRelaunchUntilPaused) { - destroyImmediately("stop-except"); - } } } @@ -6539,12 +6528,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A if (finishing) { abortAndClearOptionsAnimation(); } else { - if (deferRelaunchUntilPaused) { - destroyImmediately("stop-config"); - mRootWindowContainer.resumeFocusedTasksTopActivities(); - } else { - mAtmService.updatePreviousProcess(this); - } + mAtmService.updatePreviousProcess(this); } mTaskSupervisor.checkReadyForSleepLocked(true /* allowDelay */); } @@ -9724,23 +9708,12 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A } else { mRelaunchReason = RELAUNCH_REASON_NONE; } - if (mState == PAUSING) { - // A little annoying: we are waiting for this activity to finish pausing. Let's not - // do anything now, but just flag that it needs to be restarted when done pausing. - ProtoLog.v(WM_DEBUG_CONFIGURATION, - "Config is skipping already pausing %s", this); - deferRelaunchUntilPaused = true; - preserveWindowOnDeferredRelaunch = preserveWindow; - return true; - } else { - ProtoLog.v(WM_DEBUG_CONFIGURATION, "Config is relaunching %s", - this); - if (!mVisibleRequested) { - ProtoLog.v(WM_DEBUG_STATES, "Config is relaunching invisible " - + "activity %s called by %s", this, Debug.getCallers(4)); - } - relaunchActivityLocked(preserveWindow); + ProtoLog.v(WM_DEBUG_CONFIGURATION, "Config is relaunching %s", this); + if (!mVisibleRequested) { + ProtoLog.v(WM_DEBUG_STATES, "Config is relaunching invisible " + + "activity %s called by %s", this, Debug.getCallers(4)); } + relaunchActivityLocked(preserveWindow); // All done... tell the caller we weren't able to keep this activity around. return false; @@ -9958,8 +9931,6 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A mTaskSupervisor.mStoppingActivities.remove(this); configChangeFlags = 0; - deferRelaunchUntilPaused = false; - preserveWindowOnDeferredRelaunch = false; } /** diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java index 83ccbdc1a4d1..13f6a5f1a27b 100644 --- a/services/core/java/com/android/server/wm/BackNavigationController.java +++ b/services/core/java/com/android/server/wm/BackNavigationController.java @@ -1527,6 +1527,12 @@ class BackNavigationController { setLaunchBehind(visibleOpenActivities[i]); } } + // Force update mLastSurfaceShowing for opening activity and its task. + if (mWindowManagerService.mRoot.mTransitionController.isShellTransitionsEnabled()) { + for (int i = visibleOpenActivities.length - 1; i >= 0; --i) { + WindowContainer.enforceSurfaceVisible(visibleOpenActivities[i]); + } + } } @Nullable Runnable build() { diff --git a/services/core/java/com/android/server/wm/DisplayPolicy.java b/services/core/java/com/android/server/wm/DisplayPolicy.java index e2bc59bb6550..a7bbc25d0bb1 100644 --- a/services/core/java/com/android/server/wm/DisplayPolicy.java +++ b/services/core/java/com/android/server/wm/DisplayPolicy.java @@ -1913,12 +1913,9 @@ public class DisplayPolicy { */ final Rect mConfigFrame = new Rect(); - /** The count of insets sources when calculating this info. */ - int mLastInsetsSourceCount; - private boolean mNeedUpdate = true; - void update(DisplayContent dc, int rotation, int w, int h) { + InsetsState update(DisplayContent dc, int rotation, int w, int h) { final DisplayFrames df = new DisplayFrames(); dc.updateDisplayFrames(df, rotation, w, h); dc.getDisplayPolicy().simulateLayoutDisplay(df); @@ -1935,8 +1932,8 @@ public class DisplayPolicy { mNonDecorFrame.inset(mNonDecorInsets); mConfigFrame.set(displayFrame); mConfigFrame.inset(mConfigInsets); - mLastInsetsSourceCount = dc.getDisplayPolicy().mInsetsSourceWindowsExceptIme.size(); mNeedUpdate = false; + return insetsState; } void set(Info other) { @@ -1944,7 +1941,6 @@ public class DisplayPolicy { mConfigInsets.set(other.mConfigInsets); mNonDecorFrame.set(other.mNonDecorFrame); mConfigFrame.set(other.mConfigFrame); - mLastInsetsSourceCount = other.mLastInsetsSourceCount; mNeedUpdate = false; } @@ -1997,6 +1993,29 @@ public class DisplayPolicy { } } + static boolean hasInsetsFrameDiff(InsetsState s1, InsetsState s2, int insetsTypes) { + int insetsCount1 = 0; + for (int i = s1.sourceSize() - 1; i >= 0; i--) { + final InsetsSource source1 = s1.sourceAt(i); + if ((source1.getType() & insetsTypes) == 0) { + continue; + } + insetsCount1++; + final InsetsSource source2 = s2.peekSource(source1.getId()); + if (source2 == null || !source2.getFrame().equals(source1.getFrame())) { + return true; + } + } + int insetsCount2 = 0; + for (int i = s2.sourceSize() - 1; i >= 0; i--) { + final InsetsSource source2 = s2.sourceAt(i); + if ((source2.getType() & insetsTypes) != 0) { + insetsCount2++; + } + } + return insetsCount1 != insetsCount2; + } + private static class Cache { /** * If {@link #mPreserveId} is this value, it is in the middle of updating display @@ -2031,12 +2050,14 @@ public class DisplayPolicy { final int dw = displayFrames.mWidth; final int dh = displayFrames.mHeight; final DecorInsets.Info newInfo = mDecorInsets.mTmpInfo; - newInfo.update(mDisplayContent, rotation, dw, dh); + final InsetsState newInsetsState = newInfo.update(mDisplayContent, rotation, dw, dh); final DecorInsets.Info currentInfo = getDecorInsetsInfo(rotation, dw, dh); if (newInfo.mConfigFrame.equals(currentInfo.mConfigFrame)) { // Even if the config frame is not changed in current rotation, it may change the - // insets in other rotations if the source count is changed. - if (newInfo.mLastInsetsSourceCount != currentInfo.mLastInsetsSourceCount) { + // insets in other rotations if the frame of insets source is changed. + final InsetsState currentInsetsState = mDisplayContent.mDisplayFrames.mInsetsState; + if (DecorInsets.hasInsetsFrameDiff( + newInsetsState, currentInsetsState, mService.mConfigTypes)) { for (int i = mDecorInsets.mInfoForRotation.length - 1; i >= 0; i--) { if (i != rotation) { final boolean flipSize = (i + rotation) % 2 == 1; diff --git a/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java b/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java index 5f488b769885..bdb45884887c 100644 --- a/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java +++ b/services/core/java/com/android/server/wm/ScreenRecordingCallbackController.java @@ -97,7 +97,7 @@ public class ScreenRecordingCallbackController { mRecordedWC = (WindowContainer) mWms.mRoot.getDefaultDisplay(); } else { mRecordedWC = mWms.mRoot.getActivity(activity -> activity.mLaunchCookie - == mediaProjectionInfo.getLaunchCookie()).getTask(); + == mediaProjectionInfo.getLaunchCookie().binder).getTask(); } } diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java index 11e7bb0fb598..838ce86515cd 100644 --- a/services/core/java/com/android/server/wm/TaskFragment.java +++ b/services/core/java/com/android/server/wm/TaskFragment.java @@ -1915,11 +1915,7 @@ class TaskFragment extends WindowContainer<WindowContainer> { ProtoLog.v(WM_DEBUG_STATES, "Enqueue pending stop if needed: %s " + "wasStopping=%b visibleRequested=%b", prev, wasStopping, prev.isVisibleRequested()); - if (prev.deferRelaunchUntilPaused) { - // Complete the deferred relaunch that was waiting for pause to complete. - ProtoLog.v(WM_DEBUG_STATES, "Re-launching after pause: %s", prev); - prev.relaunchActivityLocked(prev.preserveWindowOnDeferredRelaunch); - } else if (wasStopping) { + if (wasStopping) { // We are also stopping, the stop request must have gone soon after the pause. // We can't clobber it, because the stop confirmation will not be handled. // We don't need to schedule another stop, we only need to let it happen. diff --git a/services/core/java/com/android/server/wm/TransitionController.java b/services/core/java/com/android/server/wm/TransitionController.java index 25b5630f16b2..70775530d0e2 100644 --- a/services/core/java/com/android/server/wm/TransitionController.java +++ b/services/core/java/com/android/server/wm/TransitionController.java @@ -994,39 +994,18 @@ class TransitionController { Slog.e(TAG, "Set visible without transition " + wc + " playing=" + isPlaying + " caller=" + caller); if (!isPlaying) { - enforceSurfaceVisible(wc); + WindowContainer.enforceSurfaceVisible(wc); return; } // Update surface visibility after the playing transitions are finished, so the last // visibility won't be replaced by the finish transaction of transition. mStateValidators.add(() -> { if (wc.isVisibleRequested()) { - enforceSurfaceVisible(wc); + WindowContainer.enforceSurfaceVisible(wc); } }); } - private void enforceSurfaceVisible(WindowContainer<?> wc) { - if (wc.mSurfaceControl == null) return; - wc.getSyncTransaction().show(wc.mSurfaceControl); - final ActivityRecord ar = wc.asActivityRecord(); - if (ar != null) { - ar.mLastSurfaceShowing = true; - } - // Force showing the parents because they may be hidden by previous transition. - for (WindowContainer<?> p = wc.getParent(); p != null && p != wc.mDisplayContent; - p = p.getParent()) { - if (p.mSurfaceControl != null) { - p.getSyncTransaction().show(p.mSurfaceControl); - final Task task = p.asTask(); - if (task != null) { - task.mLastSurfaceShowing = true; - } - } - } - wc.scheduleAnimation(); - } - /** * Called when the transition has a complete set of participants for its operation. In other * words, it is when the transition is "ready" but is still waiting for participants to draw. diff --git a/services/core/java/com/android/server/wm/WindowContainer.java b/services/core/java/com/android/server/wm/WindowContainer.java index 286182eedf44..2d2857aba781 100644 --- a/services/core/java/com/android/server/wm/WindowContainer.java +++ b/services/core/java/com/android/server/wm/WindowContainer.java @@ -3625,6 +3625,29 @@ class WindowContainer<E extends WindowContainer> extends ConfigurationContainer< return mSurfaceControl.getHeight(); } + static void enforceSurfaceVisible(@NonNull WindowContainer<?> wc) { + if (wc.mSurfaceControl == null) { + return; + } + wc.getSyncTransaction().show(wc.mSurfaceControl); + final ActivityRecord ar = wc.asActivityRecord(); + if (ar != null) { + ar.mLastSurfaceShowing = true; + } + // Force showing the parents because they may be hidden by previous transition. + for (WindowContainer<?> p = wc.getParent(); p != null && p != wc.mDisplayContent; + p = p.getParent()) { + if (p.mSurfaceControl != null) { + p.getSyncTransaction().show(p.mSurfaceControl); + final Task task = p.asTask(); + if (task != null) { + task.mLastSurfaceShowing = true; + } + } + } + wc.scheduleAnimation(); + } + @CallSuper void dump(PrintWriter pw, String prefix, boolean dumpAll) { if (mSurfaceAnimator.isAnimating()) { diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java index 3f889c01bafb..8cd399f6a12e 100644 --- a/services/core/java/com/android/server/wm/WindowOrganizerController.java +++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java @@ -1471,12 +1471,11 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub final int index = task.mChildren.indexOf(topTaskFragment); task.mChildren.remove(taskFragment); task.mChildren.add(index, taskFragment); - if (taskFragment.hasChild()) { - effects |= TRANSACT_EFFECTS_LIFECYCLE; - } else { + if (!taskFragment.hasChild()) { // Ensure that the child layers are updated if the TaskFragment is empty task.assignChildLayers(); } + effects |= TRANSACT_EFFECTS_LIFECYCLE; } } break; @@ -1491,12 +1490,11 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub if (task != null) { task.mChildren.remove(taskFragment); task.mChildren.add(0, taskFragment); - if (taskFragment.hasChild()) { - effects |= TRANSACT_EFFECTS_LIFECYCLE; - } else { + if (!taskFragment.hasChild()) { // Ensure that the child layers are updated if the TaskFragment is empty. task.assignChildLayers(); } + effects |= TRANSACT_EFFECTS_LIFECYCLE; } break; } @@ -1505,12 +1503,11 @@ class WindowOrganizerController extends IWindowOrganizerController.Stub if (task != null) { task.mChildren.remove(taskFragment); task.mChildren.add(taskFragment); - if (taskFragment.hasChild()) { - effects |= TRANSACT_EFFECTS_LIFECYCLE; - } else { + if (!taskFragment.hasChild()) { // Ensure that the child layers are updated if the TaskFragment is empty. task.assignChildLayers(); } + effects |= TRANSACT_EFFECTS_LIFECYCLE; } break; } diff --git a/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp b/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp index 7b084132ed1c..4403bce484ad 100644 --- a/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp +++ b/services/core/jni/com_android_server_am_CachedAppOptimizer.cpp @@ -571,8 +571,8 @@ static jstring com_android_server_am_CachedAppOptimizer_getFreezerCheckPath(JNIE } static jboolean com_android_server_am_CachedAppOptimizer_isFreezerProfileValid(JNIEnv* env) { - int uid = getuid(); - int pid = getpid(); + uid_t uid = getuid(); + pid_t pid = getpid(); return isProfileValidForProcess("Frozen", uid, pid) && isProfileValidForProcess("Unfrozen", uid, pid); diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp index cbc301b87295..4a6b31c29471 100644 --- a/services/core/jni/com_android_server_input_InputManagerService.cpp +++ b/services/core/jni/com_android_server_input_InputManagerService.cpp @@ -290,6 +290,7 @@ public: void setTouchpadPointerSpeed(int32_t speed); void setTouchpadNaturalScrollingEnabled(bool enabled); void setTouchpadTapToClickEnabled(bool enabled); + void setTouchpadTapDraggingEnabled(bool enabled); void setTouchpadRightClickZoneEnabled(bool enabled); void setInputDeviceEnabled(uint32_t deviceId, bool enabled); void setShowTouches(bool enabled); @@ -440,6 +441,9 @@ private: // True to enable tap-to-click on touchpads. bool touchpadTapToClickEnabled{true}; + // True to enable tap dragging on touchpads. + bool touchpadTapDraggingEnabled{false}; + // True to enable a zone on the right-hand side of touchpads where clicks will be turned // into context (a.k.a. "right") clicks. bool touchpadRightClickZoneEnabled{false}; @@ -697,6 +701,7 @@ void NativeInputManager::getReaderConfiguration(InputReaderConfiguration* outCon outConfig->touchpadPointerSpeed = mLocked.touchpadPointerSpeed; outConfig->touchpadNaturalScrollingEnabled = mLocked.touchpadNaturalScrollingEnabled; outConfig->touchpadTapToClickEnabled = mLocked.touchpadTapToClickEnabled; + outConfig->touchpadTapDraggingEnabled = mLocked.touchpadTapDraggingEnabled; outConfig->touchpadRightClickZoneEnabled = mLocked.touchpadRightClickZoneEnabled; outConfig->disabledDevices = mLocked.disabledInputDevices; @@ -1301,6 +1306,22 @@ void NativeInputManager::setTouchpadTapToClickEnabled(bool enabled) { InputReaderConfiguration::Change::TOUCHPAD_SETTINGS); } +void NativeInputManager::setTouchpadTapDraggingEnabled(bool enabled) { + { // acquire lock + std::scoped_lock _l(mLock); + + if (mLocked.touchpadTapDraggingEnabled == enabled) { + return; + } + + ALOGI("Setting touchpad tap dragging to %s.", toString(enabled)); + mLocked.touchpadTapDraggingEnabled = enabled; + } // release lock + + mInputManager->getReader().requestRefreshConfiguration( + InputReaderConfiguration::Change::TOUCHPAD_SETTINGS); +} + void NativeInputManager::setTouchpadRightClickZoneEnabled(bool enabled) { { // acquire lock std::scoped_lock _l(mLock); @@ -2223,6 +2244,13 @@ static void nativeSetTouchpadTapToClickEnabled(JNIEnv* env, jobject nativeImplOb im->setTouchpadTapToClickEnabled(enabled); } +static void nativeSetTouchpadTapDraggingEnabled(JNIEnv* env, jobject nativeImplObj, + jboolean enabled) { + NativeInputManager* im = getNativeInputManager(env, nativeImplObj); + + im->setTouchpadTapDraggingEnabled(enabled); +} + static void nativeSetTouchpadRightClickZoneEnabled(JNIEnv* env, jobject nativeImplObj, jboolean enabled) { NativeInputManager* im = getNativeInputManager(env, nativeImplObj); @@ -2844,6 +2872,7 @@ static const JNINativeMethod gInputManagerMethods[] = { {"setTouchpadNaturalScrollingEnabled", "(Z)V", (void*)nativeSetTouchpadNaturalScrollingEnabled}, {"setTouchpadTapToClickEnabled", "(Z)V", (void*)nativeSetTouchpadTapToClickEnabled}, + {"setTouchpadTapDraggingEnabled", "(Z)V", (void*)nativeSetTouchpadTapDraggingEnabled}, {"setTouchpadRightClickZoneEnabled", "(Z)V", (void*)nativeSetTouchpadRightClickZoneEnabled}, {"setShowTouches", "(Z)V", (void*)nativeSetShowTouches}, {"setInteractive", "(Z)V", (void*)nativeSetInteractive}, diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java index 667e086c8b40..281fb1c4635b 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java @@ -27,6 +27,7 @@ import android.annotation.Nullable; import android.annotation.UserIdInt; import android.app.ActivityManager; import android.content.ComponentName; +import android.content.ContentResolver; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; @@ -201,7 +202,7 @@ public final class CredentialManagerService @SuppressWarnings("GuardedBy") // ErrorProne requires service.mLock which is the same // this.mLock protected void handlePackageRemovedMultiModeLocked(String packageName, int userId) { - updateProvidersWhenPackageRemoved(mContext, packageName); + updateProvidersWhenPackageRemoved(new SettingsWrapper(mContext), packageName); List<CredentialManagerServiceImpl> services = peekServiceListForUserLocked(userId); if (services == null) { @@ -1134,13 +1135,14 @@ public final class CredentialManagerService } /** Updates the list of providers when an app is uninstalled. */ - public static void updateProvidersWhenPackageRemoved(Context context, String packageName) { + public static void updateProvidersWhenPackageRemoved( + SettingsWrapper settingsWrapper, String packageName) { + Slog.i(TAG, "updateProvidersWhenPackageRemoved"); + // Get the current providers. String rawProviders = - Settings.Secure.getStringForUser( - context.getContentResolver(), - Settings.Secure.CREDENTIAL_SERVICE_PRIMARY, - UserHandle.myUserId()); + settingsWrapper.getStringForUser( + Settings.Secure.CREDENTIAL_SERVICE_PRIMARY, UserHandle.myUserId()); if (rawProviders == null) { Slog.w(TAG, "settings key is null"); return; @@ -1148,44 +1150,44 @@ public final class CredentialManagerService // Remove any providers from the primary setting that contain the package name // being removed. - Set<String> primaryProviders = - getStoredProviders(rawProviders, packageName); - if (!Settings.Secure.putString( - context.getContentResolver(), + Set<String> primaryProviders = getStoredProviders(rawProviders, packageName); + if (!settingsWrapper.putStringForUser( Settings.Secure.CREDENTIAL_SERVICE_PRIMARY, - String.join(":", primaryProviders))) { - Slog.w(TAG, "Failed to remove primary package: " + packageName); + String.join(":", primaryProviders), + UserHandle.myUserId(), + /* overrideableByRestore= */ true)) { + Slog.e(TAG, "Failed to remove primary package: " + packageName); return; } // Read the autofill provider so we don't accidentally erase it. String autofillProvider = - Settings.Secure.getStringForUser( - context.getContentResolver(), - Settings.Secure.AUTOFILL_SERVICE, - UserHandle.myUserId()); + settingsWrapper.getStringForUser( + Settings.Secure.AUTOFILL_SERVICE, UserHandle.myUserId()); // If there is an autofill provider and it is the placeholder indicating // that the currently selected primary provider does not support autofill // then we should wipe the setting to keep it in sync. if (autofillProvider != null && primaryProviders.isEmpty()) { if (autofillProvider.equals(AUTOFILL_PLACEHOLDER_VALUE)) { - if (!Settings.Secure.putString( - context.getContentResolver(), + if (!settingsWrapper.putStringForUser( Settings.Secure.AUTOFILL_SERVICE, - "")) { - Slog.w(TAG, "Failed to remove autofill package: " + packageName); + "", + UserHandle.myUserId(), + /* overrideableByRestore= */ true)) { + Slog.e(TAG, "Failed to remove autofill package: " + packageName); } } else { // If the existing autofill provider is from the app being removed // then erase the autofill service setting. ComponentName cn = ComponentName.unflattenFromString(autofillProvider); if (cn != null && cn.getPackageName().equals(packageName)) { - if (!Settings.Secure.putString( - context.getContentResolver(), + if (!settingsWrapper.putStringForUser( Settings.Secure.AUTOFILL_SERVICE, - "")) { - Slog.w(TAG, "Failed to remove autofill package: " + packageName); + "", + UserHandle.myUserId(), + /* overrideableByRestore= */ true)) { + Slog.e(TAG, "Failed to remove autofill package: " + packageName); } } } @@ -1193,19 +1195,17 @@ public final class CredentialManagerService // Read the credential providers to remove any reference of the removed app. String rawCredentialProviders = - Settings.Secure.getStringForUser( - context.getContentResolver(), - Settings.Secure.CREDENTIAL_SERVICE, - UserHandle.myUserId()); + settingsWrapper.getStringForUser( + Settings.Secure.CREDENTIAL_SERVICE, UserHandle.myUserId()); // Remove any providers that belong to the removed app. - Set<String> credentialProviders = - getStoredProviders(rawCredentialProviders, packageName); - if (!Settings.Secure.putString( - context.getContentResolver(), + Set<String> credentialProviders = getStoredProviders(rawCredentialProviders, packageName); + if (!settingsWrapper.putStringForUser( Settings.Secure.CREDENTIAL_SERVICE, - String.join(":", credentialProviders))) { - Slog.w(TAG, "Failed to remove secondary package: " + packageName); + String.join(":", credentialProviders), + UserHandle.myUserId(), + /* overrideableByRestore= */ true)) { + Slog.e(TAG, "Failed to remove secondary package: " + packageName); } } @@ -1232,4 +1232,38 @@ public final class CredentialManagerService return providers; } + + /** A wrapper class that can be used by tests for intercepting reads/writes. */ + public static class SettingsWrapper { + private final Context mContext; + + public SettingsWrapper(@NonNull Context context) { + this.mContext = context; + } + + ContentResolver getContentResolver() { + return mContext.getContentResolver(); + } + + /** Retrieves the string value of a system setting */ + public String getStringForUser(String name, int userHandle) { + return Settings.Secure.getStringForUser(getContentResolver(), name, userHandle); + } + + /** Updates the string value of a system setting */ + public boolean putStringForUser( + String name, + String value, + int userHandle, + boolean overrideableByRestore) { + return Settings.Secure.putStringForUser( + getContentResolver(), + name, + value, + null, + false, + userHandle, + overrideableByRestore); + } + } } diff --git a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionUpgrade.kt b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionUpgrade.kt index edacda03f277..15c9b9f7a13d 100644 --- a/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionUpgrade.kt +++ b/services/permission/java/com/android/server/permission/access/permission/AppIdPermissionUpgrade.kt @@ -20,7 +20,6 @@ import android.Manifest import android.os.Build import android.util.Slog import com.android.server.permission.access.MutateStateScope -import com.android.server.permission.access.collection.* // ktlint-disable no-wildcard-imports import com.android.server.permission.access.immutable.* // ktlint-disable no-wildcard-imports import com.android.server.permission.access.util.andInv import com.android.server.permission.access.util.hasAnyBit @@ -61,10 +60,11 @@ class AppIdPermissionUpgrade(private val policy: AppIdPermissionPolicy) { if (version <= 12 /*&& SdkLevel.isAtLeastT()*/) { Slog.v( LOG_TAG, - "Upgrading scoped permissions for package: $packageName" + + "Upgrading scoped media and body sensor permissions for package: $packageName" + ", version: $version, user: $userId" ) upgradeAuralVisualMediaPermissions(packageState, userId) + upgradeBodySensorPermissions(packageState, userId) } // TODO Enable isAtLeastU check, when moving subsystem to mainline. if (version <= 14 /*&& SdkLevel.isAtLeastU()*/) { @@ -182,6 +182,50 @@ class AppIdPermissionUpgrade(private val policy: AppIdPermissionPolicy) { } } + private fun MutateStateScope.upgradeBodySensorPermissions( + packageState: PackageState, + userId: Int + ) { + if ( + Manifest.permission.BODY_SENSORS_BACKGROUND !in + packageState.androidPackage!!.requestedPermissions + ) { + return + } + + // Should have been granted when first getting exempt as if the perm was just split + val appId = packageState.appId + val backgroundBodySensorsFlags = + with(policy) { + getPermissionFlags(appId, userId, Manifest.permission.BODY_SENSORS_BACKGROUND) + } + if (backgroundBodySensorsFlags.hasAnyBit(PermissionFlags.MASK_EXEMPT)) { + return + } + + // Add Upgrade Exemption - BODY_SENSORS_BACKGROUND is a restricted permission + with(policy) { + updatePermissionFlags( + appId, + userId, + Manifest.permission.BODY_SENSORS_BACKGROUND, + PermissionFlags.UPGRADE_EXEMPT, + PermissionFlags.UPGRADE_EXEMPT, + ) + } + + val bodySensorsFlags = + with(policy) { getPermissionFlags(appId, userId, Manifest.permission.BODY_SENSORS) } + val isForegroundBodySensorsGranted = PermissionFlags.isAppOpGranted(bodySensorsFlags) + if (isForegroundBodySensorsGranted) { + grantRuntimePermission( + packageState, + userId, + Manifest.permission.BODY_SENSORS_BACKGROUND + ) + } + } + /** Upgrade permission based on the grant in [Manifest.permission_group.READ_MEDIA_VISUAL] */ private fun MutateStateScope.upgradeUserSelectedVisualMediaPermission( packageState: PackageState, diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.java index b9f1ea06aebe..dc9631a8f2e2 100644 --- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.java +++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/ClientControllerTest.java @@ -116,7 +116,7 @@ public final class ClientControllerTest { ANY_CALLER_PID); verify(invoker.asBinder()).linkToDeath(any(IBinder.DeathRecipient.class), eq(0)); - assertThat(mController.mClients).containsEntry(invoker.asBinder(), added); + assertThat(mController.getClient(invoker.asBinder())).isSameInstanceAs(added); } } @@ -133,7 +133,7 @@ public final class ClientControllerTest { var invoker = IInputMethodClientInvoker.create(mClient, mHandler); added = mController.addClient(invoker, mConnection, ANY_DISPLAY_ID, ANY_CALLER_UID, ANY_CALLER_PID); - assertThat(mController.mClients).containsEntry(invoker.asBinder(), added); + assertThat(mController.getClient(invoker.asBinder())).isSameInstanceAs(added); assertThat(mController.removeClient(mClient)).isTrue(); } diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java index 02e3ef4d5f0b..75febd902dcf 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java @@ -61,6 +61,7 @@ import static org.mockito.Mockito.when; import android.annotation.NonNull; import android.annotation.Nullable; +import android.app.ActivityOptions.LaunchCookie; import android.app.PropertyInvalidatedCache; import android.companion.virtual.IVirtualDevice; import android.companion.virtual.IVirtualDeviceManager; @@ -1557,7 +1558,7 @@ public class DisplayManagerServiceTest { when(mMockProjectionService .setContentRecordingSession(any(ContentRecordingSession.class), eq(projection))) .thenReturn(true); - doReturn(mock(IBinder.class)).when(projection).getLaunchCookie(); + doReturn(new LaunchCookie()).when(projection).getLaunchCookie(); doReturn(true).when(mMockProjectionService).isCurrentProjection(eq(projection)); final VirtualDisplayConfig.Builder builder = new VirtualDisplayConfig.Builder( diff --git a/services/tests/mockingservicestests/Android.bp b/services/tests/mockingservicestests/Android.bp index 321d945e9c15..d9283063b7fc 100644 --- a/services/tests/mockingservicestests/Android.bp +++ b/services/tests/mockingservicestests/Android.bp @@ -46,7 +46,6 @@ android_test { "androidx.test.espresso.core", "androidx.test.espresso.contrib", "androidx.test.ext.truth", - "backup_flags_lib", "flag-junit", "frameworks-base-testutils", "hamcrest-library", diff --git a/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java b/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java index a65ef00f8a21..bf00b75e9f7b 100644 --- a/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java +++ b/services/tests/mockingservicestests/src/com/android/server/pm/PackageArchiverTest.java @@ -552,20 +552,22 @@ public class PackageArchiverTest { when(mComputer.getPackageStateFiltered(eq(PACKAGE), anyInt(), anyInt())).thenReturn( null); - assertThat(mArchiveManager.getArchivedAppIcon(PACKAGE, UserHandle.CURRENT)).isNull(); + assertThat(mArchiveManager.getArchivedAppIcon(PACKAGE, UserHandle.CURRENT, + CALLER_PACKAGE)).isNull(); } @Test public void getArchivedAppIcon_notArchived() { - assertThat(mArchiveManager.getArchivedAppIcon(PACKAGE, UserHandle.CURRENT)).isNull(); + assertThat(mArchiveManager.getArchivedAppIcon(PACKAGE, UserHandle.CURRENT, + CALLER_PACKAGE)).isNull(); } @Test public void getArchivedAppIcon_success() { mUserState.setArchiveState(createArchiveState()).setInstalled(false); - assertThat(mArchiveManager.getArchivedAppIcon(PACKAGE, UserHandle.CURRENT)).isEqualTo( - mIcon); + assertThat(mArchiveManager.getArchivedAppIcon(PACKAGE, UserHandle.CURRENT, + CALLER_PACKAGE)).isEqualTo(mIcon); } diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java index bb70080362b1..92513760fa4a 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/BatteryStatsHistoryTest.java @@ -21,6 +21,9 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; import android.content.Context; @@ -96,18 +99,11 @@ public class BatteryStatsHistoryTest { mClock.realtime = 123; mHistory = new BatteryStatsHistory(mHistoryBuffer, mSystemDir, 32, 1024, - mStepDetailsCalculator, mClock, mMonotonicClock, mTracer) { - @Override - public boolean readFileToParcel(Parcel out, AtomicFile file) { - mReadFiles.add(file.getBaseFile().getName()); - return super.readFileToParcel(out, file); - } - }; + mStepDetailsCalculator, mClock, mMonotonicClock, mTracer); when(mStepDetailsCalculator.getHistoryStepDetails()) .thenReturn(new BatteryStats.HistoryStepDetails()); - mHistoryPrinter = new BatteryStats.HistoryPrinter(); } @@ -276,6 +272,15 @@ public class BatteryStatsHistoryTest { mReadFiles.clear(); + // Make an immutable copy and spy on it + mHistory = spy(mHistory.copy()); + + doAnswer(invocation -> { + AtomicFile file = invocation.getArgument(1); + mReadFiles.add(file.getBaseFile().getName()); + return invocation.callRealMethod(); + }).when(mHistory).readFileToParcel(any(), any()); + // Prepare history for iteration mHistory.iterate(0, MonotonicClock.UNDEFINED); @@ -309,6 +314,15 @@ public class BatteryStatsHistoryTest { mReadFiles.clear(); + // Make an immutable copy and spy on it + mHistory = spy(mHistory.copy()); + + doAnswer(invocation -> { + AtomicFile file = invocation.getArgument(1); + mReadFiles.add(file.getBaseFile().getName()); + return invocation.callRealMethod(); + }).when(mHistory).readFileToParcel(any(), any()); + // Prepare history for iteration mHistory.iterate(1000, 3000); diff --git a/services/tests/powerstatstests/src/com/android/server/power/stats/SystemServicePowerCalculatorTest.java b/services/tests/powerstatstests/src/com/android/server/power/stats/SystemServicePowerCalculatorTest.java index 4dae2d548057..8e53d5285cc4 100644 --- a/services/tests/powerstatstests/src/com/android/server/power/stats/SystemServicePowerCalculatorTest.java +++ b/services/tests/powerstatstests/src/com/android/server/power/stats/SystemServicePowerCalculatorTest.java @@ -28,6 +28,9 @@ import static org.mockito.Mockito.when; import android.os.BatteryConsumer; import android.os.Binder; import android.os.Process; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; @@ -38,6 +41,7 @@ import com.android.internal.os.KernelCpuUidTimeReader; import com.android.internal.os.KernelSingleUidTimeReader; import com.android.internal.os.PowerProfile; import com.android.internal.power.EnergyConsumerStats; +import com.android.server.power.optimization.Flags; import org.junit.Before; import org.junit.Rule; @@ -54,6 +58,8 @@ import java.util.Collection; @RunWith(AndroidJUnit4.class) @SuppressWarnings("GuardedBy") public class SystemServicePowerCalculatorTest { + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); private static final double PRECISION = 0.000001; private static final int APP_UID1 = 100; @@ -108,6 +114,7 @@ public class SystemServicePowerCalculatorTest { } @Test + @RequiresFlagsDisabled(Flags.FLAG_DISABLE_SYSTEM_SERVICE_POWER_ATTR) public void testPowerProfileBasedModel() { prepareBatteryStats(null); @@ -135,6 +142,7 @@ public class SystemServicePowerCalculatorTest { } @Test + @RequiresFlagsDisabled(Flags.FLAG_DISABLE_SYSTEM_SERVICE_POWER_ATTR) public void testMeasuredEnergyBasedModel() { final boolean[] supportedPowerBuckets = new boolean[EnergyConsumerStats.NUMBER_STANDARD_POWER_BUCKETS]; diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp index 8958fac87bb6..e22d99d45521 100644 --- a/services/tests/servicestests/Android.bp +++ b/services/tests/servicestests/Android.bp @@ -36,6 +36,7 @@ android_test { "-Werror", ], static_libs: [ + "cts-input-lib", "frameworks-base-testutils", "services.accessibility", "services.appwidget", diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityInputFilterInputTest.kt b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityInputFilterInputTest.kt new file mode 100644 index 000000000000..52c7d8d2bd2e --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityInputFilterInputTest.kt @@ -0,0 +1,280 @@ +/* + * 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 com.android.server.accessibility + +import android.hardware.display.DisplayManagerGlobal +import android.os.SystemClock +import android.view.Display +import android.view.Display.DEFAULT_DISPLAY +import android.view.DisplayAdjustments +import android.view.DisplayInfo +import android.view.IInputFilterHost +import android.view.InputDevice.SOURCE_TOUCHSCREEN +import android.view.InputEvent +import android.view.MotionEvent +import android.view.MotionEvent.ACTION_DOWN +import android.view.MotionEvent.ACTION_MOVE +import android.view.MotionEvent.ACTION_UP +import android.view.MotionEvent.ACTION_HOVER_ENTER +import android.view.MotionEvent.ACTION_HOVER_EXIT +import android.view.MotionEvent.ACTION_HOVER_MOVE +import android.view.WindowManagerPolicyConstants.FLAG_PASS_TO_USER +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import com.android.cts.input.inputeventmatchers.withDeviceId +import com.android.cts.input.inputeventmatchers.withMotionAction +import com.android.server.LocalServices +import com.android.server.accessibility.magnification.MagnificationProcessor +import com.android.server.wm.WindowManagerInternal +import java.util.concurrent.LinkedBlockingQueue +import org.hamcrest.Matchers.allOf +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.Mockito.doAnswer +import org.mockito.Mockito.`when` +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule +import org.mockito.stubbing.OngoingStubbing + + +/** + * Create a MotionEvent with the provided action, eventTime, and source + */ +fun createMotionEvent(action: Int, downTime: Long, eventTime: Long, source: Int, deviceId: Int): + MotionEvent { + val x = 1f + val y = 2f + val pressure = 3f + val size = 1f + val metaState = 0 + val xPrecision = 0f + val yPrecision = 0f + val edgeFlags = 0 + val displayId = 0 + return MotionEvent.obtain(downTime, eventTime, action, x, y, pressure, size, metaState, + xPrecision, yPrecision, deviceId, edgeFlags, source, displayId) +} + +/** + * Tests for AccessibilityInputFilter, focusing on the input event processing as seen by the callers + * of the InputFilter interface. + * The main interaction with AccessibilityInputFilter in these tests is with the filterInputEvent + * and sendInputEvent APIs of InputFilter. + */ +@RunWith(AndroidJUnit4::class) +class AccessibilityInputFilterInputTest { + private val instrumentation = InstrumentationRegistry.getInstrumentation() + + private companion object{ + const val ALL_A11Y_FEATURES = (AccessibilityInputFilter.FLAG_FEATURE_AUTOCLICK + or AccessibilityInputFilter.FLAG_FEATURE_TOUCH_EXPLORATION + or AccessibilityInputFilter.FLAG_FEATURE_CONTROL_SCREEN_MAGNIFIER + or AccessibilityInputFilter.FLAG_FEATURE_TRIGGERED_SCREEN_MAGNIFIER + or AccessibilityInputFilter.FLAG_FEATURE_INJECT_MOTION_EVENTS + or AccessibilityInputFilter.FLAG_FEATURE_FILTER_KEY_EVENTS) + } + + @Rule + @JvmField + val mocks: MockitoRule = MockitoJUnit.rule() + + @Mock + private lateinit var mockA11yController: WindowManagerInternal.AccessibilityControllerInternal + + @Mock + private lateinit var mockWindowManagerService: WindowManagerInternal + + @Mock + private lateinit var mockMagnificationProcessor: MagnificationProcessor + + private val inputEvents = LinkedBlockingQueue<InputEvent>() + private val verifier = BlockingQueueEventVerifier(inputEvents) + + @Mock + private lateinit var host: IInputFilterHost + private lateinit var ams: AccessibilityManagerService + private lateinit var a11yInputFilter: AccessibilityInputFilter + private val touchDeviceId = 1 + + @Before + fun setUp() { + val context = instrumentation.context + LocalServices.removeServiceForTest(WindowManagerInternal::class.java) + LocalServices.addService(WindowManagerInternal::class.java, mockWindowManagerService) + + whenever(mockA11yController.isAccessibilityTracingEnabled).thenReturn(false) + whenever( + mockWindowManagerService.accessibilityController).thenReturn( + mockA11yController) + + ams = Mockito.spy(AccessibilityManagerService(context)) + val displayList = arrayListOf(createStubDisplay(DEFAULT_DISPLAY, DisplayInfo())) + whenever(ams.validDisplayList).thenReturn(displayList) + whenever(ams.magnificationProcessor).thenReturn(mockMagnificationProcessor) + + doAnswer { + val event = it.getArgument(0) as MotionEvent + inputEvents.add(MotionEvent.obtain(event)) + }.`when`(host).sendInputEvent(any(), anyInt()) + + a11yInputFilter = AccessibilityInputFilter(context, ams) + a11yInputFilter.install(host) + } + + @After + fun tearDown() { + if (this::a11yInputFilter.isInitialized) { + a11yInputFilter.uninstall() + } + } + + /** + * When no features are enabled, the events pass through the filter without getting modified. + */ + @Test + fun testSingleDeviceTouchEventsWithoutA11yFeatures() { + enableFeatures(0) + + val downTime = SystemClock.uptimeMillis() + val downEvent = createMotionEvent( + ACTION_DOWN, downTime, downTime, SOURCE_TOUCHSCREEN, touchDeviceId) + send(downEvent) + verifier.assertReceivedMotion( + allOf(withMotionAction(ACTION_DOWN), withDeviceId(touchDeviceId))) + + val moveEvent = createMotionEvent( + ACTION_MOVE, downTime, SystemClock.uptimeMillis(), SOURCE_TOUCHSCREEN, touchDeviceId) + send(moveEvent) + verifier.assertReceivedMotion( + allOf(withMotionAction(ACTION_MOVE), withDeviceId(touchDeviceId))) + + val upEvent = createMotionEvent( + ACTION_UP, downTime, SystemClock.uptimeMillis(), SOURCE_TOUCHSCREEN, touchDeviceId) + send(upEvent) + verifier.assertReceivedMotion( + allOf(withMotionAction(ACTION_UP), withDeviceId(touchDeviceId))) + + verifier.assertNoEvents() + } + + /** + * Enable all a11y features and send a touchscreen stream of DOWN -> MOVE -> UP events. + * These get converted into HOVER_ENTER -> HOVER_MOVE -> HOVER_EXIT events by the input filter. + */ + @Test + fun testSingleDeviceTouchEventsWithAllA11yFeatures() { + enableFeatures(ALL_A11Y_FEATURES) + + val downTime = SystemClock.uptimeMillis() + val downEvent = createMotionEvent( + ACTION_DOWN, downTime, downTime, SOURCE_TOUCHSCREEN, touchDeviceId) + send(MotionEvent.obtain(downEvent)) + + // DOWN event gets transformed to HOVER_ENTER + verifier.assertReceivedMotion( + allOf(withMotionAction(ACTION_HOVER_ENTER), withDeviceId(touchDeviceId))) + + // MOVE becomes HOVER_MOVE + val moveEvent = createMotionEvent( + ACTION_MOVE, downTime, SystemClock.uptimeMillis(), SOURCE_TOUCHSCREEN, touchDeviceId) + send(moveEvent) + verifier.assertReceivedMotion( + allOf(withMotionAction(ACTION_HOVER_MOVE), withDeviceId(touchDeviceId))) + + // UP becomes HOVER_EXIT + val upEvent = createMotionEvent( + ACTION_UP, downTime, SystemClock.uptimeMillis(), SOURCE_TOUCHSCREEN, touchDeviceId) + send(upEvent) + + verifier.assertReceivedMotion( + allOf(withMotionAction(ACTION_HOVER_EXIT), withDeviceId(touchDeviceId))) + + verifier.assertNoEvents() + } + + /** + * Enable all a11y features and send a touchscreen event stream. In the middle of the gesture, + * disable the a11y features. + * When the a11y features are disabled, the filter generates HOVER_EXIT without further input + * from the dispatcher. + */ + @Test + fun testSingleDeviceTouchEventsDisableFeaturesMidGesture() { + enableFeatures(ALL_A11Y_FEATURES) + + val downTime = SystemClock.uptimeMillis() + val downEvent = createMotionEvent( + ACTION_DOWN, downTime, downTime, SOURCE_TOUCHSCREEN, touchDeviceId) + send(MotionEvent.obtain(downEvent)) + + // DOWN event gets transformed to HOVER_ENTER + verifier.assertReceivedMotion( + allOf(withMotionAction(ACTION_HOVER_ENTER), withDeviceId(touchDeviceId))) + verifier.assertNoEvents() + + enableFeatures(0) + verifier.assertReceivedMotion( + allOf(withMotionAction(ACTION_HOVER_EXIT), withDeviceId(touchDeviceId))) + verifier.assertNoEvents() + + val moveEvent = createMotionEvent( + ACTION_MOVE, downTime, SystemClock.uptimeMillis(), SOURCE_TOUCHSCREEN, touchDeviceId) + send(moveEvent) + val upEvent = createMotionEvent( + ACTION_UP, downTime, SystemClock.uptimeMillis(), SOURCE_TOUCHSCREEN, touchDeviceId) + send(upEvent) + // As the original gesture continues, no additional events should be getting sent by the + // filter because the HOVER_EXIT above already effectively finished the current gesture and + // the DOWN event was never sent to the host. + + // Bug: the down event was swallowed, so the remainder of the gesture should be swallowed + // too. However, the MOVE and UP events are currently passed back to the dispatcher. + // TODO(b/310014874) - ensure a11y sends consistent input streams to the dispatcher + verifier.assertReceivedMotion( + allOf(withMotionAction(ACTION_MOVE), withDeviceId(touchDeviceId))) + verifier.assertReceivedMotion( + allOf(withMotionAction(ACTION_UP), withDeviceId(touchDeviceId))) + + verifier.assertNoEvents() + } + + private fun createStubDisplay(displayId: Int, displayInfo: DisplayInfo): Display { + val display = Display(DisplayManagerGlobal.getInstance(), displayId, + displayInfo, DisplayAdjustments.DEFAULT_DISPLAY_ADJUSTMENTS) + return display + } + + private fun send(event: InputEvent) { + // We need to make a copy of the event before sending it to the filter, because the filter + // will recycle it, but the caller of this function might want to still be able to use + // this event for subsequent checks + val eventCopy = if (event is MotionEvent) MotionEvent.obtain(event) else event + a11yInputFilter.filterInputEvent(eventCopy, FLAG_PASS_TO_USER) + } + + private fun enableFeatures(features: Int) { + instrumentation.runOnMainSync { a11yInputFilter.setUserAndEnabledFeatures(0, features) } + } +} + +private fun <T> whenever(methodCall: T): OngoingStubbing<T> = `when`(methodCall) diff --git a/services/tests/servicestests/src/com/android/server/accessibility/BlockingQueueEventVerifier.kt b/services/tests/servicestests/src/com/android/server/accessibility/BlockingQueueEventVerifier.kt new file mode 100644 index 000000000000..b12f537d1482 --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/accessibility/BlockingQueueEventVerifier.kt @@ -0,0 +1,57 @@ +/* + * 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 com.android.server.accessibility + +import android.os.InputConstants.DEFAULT_DISPATCHING_TIMEOUT_MILLIS +import android.view.InputEvent +import android.view.MotionEvent +import java.time.Duration +import java.util.concurrent.BlockingQueue +import java.util.concurrent.TimeUnit +import org.junit.Assert.fail + +import org.hamcrest.Matcher +import org.hamcrest.MatcherAssert.assertThat +import org.junit.Assert.assertNull + +private fun <T> getEvent(queue: BlockingQueue<T>, timeout: Duration): T? { + return queue.poll(timeout.toMillis(), TimeUnit.MILLISECONDS) +} + +class BlockingQueueEventVerifier(val queue: BlockingQueue<InputEvent>) { + fun assertReceivedMotion(matcher: Matcher<MotionEvent>) { + val event = getMotionEvent() + assertThat("MotionEvent checks", event, matcher) + } + + fun assertNoEvents() { + val event = getEvent(queue, Duration.ofMillis(50)) + assertNull(event) + } + + private fun getMotionEvent(): MotionEvent { + val event = getEvent(queue, Duration.ofMillis(DEFAULT_DISPATCHING_TIMEOUT_MILLIS.toLong())) + if (event == null) { + fail("Did not get an event") + } + if (event is MotionEvent) { + return event + } + fail("Instead of motion, got $event") + throw RuntimeException("should not reach here") + } +} + diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BaseClientMonitorTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BaseClientMonitorTest.java index 3a9c0f0f1790..a1f0dbd4e889 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/BaseClientMonitorTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/BaseClientMonitorTest.java @@ -70,7 +70,7 @@ public class BaseClientMonitorTest { mClientMonitor.binderDied(); assertThat(mClientMonitor.mCanceled).isTrue(); - assertThat(mClientMonitor.getListener()).isNull(); + assertThat(mClientMonitor.getListener()).isNotNull(); } @Test diff --git a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceGenerateChallengeClientTest.java b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceGenerateChallengeClientTest.java index c8bfaa90d863..5b81277250c2 100644 --- a/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceGenerateChallengeClientTest.java +++ b/services/tests/servicestests/src/com/android/server/biometrics/sensors/face/aidl/FaceGenerateChallengeClientTest.java @@ -87,14 +87,6 @@ public class FaceGenerateChallengeClientTest { verify(mCallback).onClientFinished(mClient, true); } - @Test - public void generateChallenge_nullListener() { - createClient(null); - mClient.start(mCallback); - - verify(mCallback).onClientFinished(mClient, false); - } - private void createClient(ClientMonitorCallbackConverter listener) { mClient = new FaceGenerateChallengeClient(mContext, () -> mAidlSession, mToken, listener, USER_ID, TAG, SENSOR_ID, mBiometricLogger, mBiometricContext); diff --git a/services/tests/servicestests/src/com/android/server/credentials/CredentialManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/credentials/CredentialManagerServiceTest.java index d850c73ebc26..57f3cc03980e 100644 --- a/services/tests/servicestests/src/com/android/server/credentials/CredentialManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/credentials/CredentialManagerServiceTest.java @@ -22,6 +22,7 @@ import android.content.Context; import android.os.UserHandle; import android.provider.Settings; +import androidx.annotation.NonNull; import androidx.test.core.app.ApplicationProvider; import androidx.test.filters.SmallTest; import androidx.test.runner.AndroidJUnit4; @@ -40,10 +41,12 @@ import java.util.Set; public final class CredentialManagerServiceTest { Context mContext = null; + MockSettingsWrapper mSettingsWrapper = null; @Before public void setUp() throws CertificateException { mContext = ApplicationProvider.getApplicationContext(); + mSettingsWrapper = new MockSettingsWrapper(mContext); } @Test @@ -81,7 +84,8 @@ public final class CredentialManagerServiceTest { Settings.Secure.CREDENTIAL_SERVICE_PRIMARY, "com.example.test/com.example.test.TestActivity"); - CredentialManagerService.updateProvidersWhenPackageRemoved(mContext, "com.example.test"); + CredentialManagerService.updateProvidersWhenPackageRemoved( + mSettingsWrapper, "com.example.test"); assertThat(getSettingsKey(Settings.Secure.AUTOFILL_SERVICE)).isEqualTo(""); assertThat(getSettingsKey(Settings.Secure.CREDENTIAL_SERVICE)) @@ -101,7 +105,8 @@ public final class CredentialManagerServiceTest { setSettingsKey(Settings.Secure.CREDENTIAL_SERVICE, testCredentialValue); setSettingsKey(Settings.Secure.CREDENTIAL_SERVICE_PRIMARY, testCredentialPrimaryValue); - CredentialManagerService.updateProvidersWhenPackageRemoved(mContext, "com.example.test3"); + CredentialManagerService.updateProvidersWhenPackageRemoved( + mSettingsWrapper, "com.example.test3"); // Since the provider removed was not a primary provider then we should do nothing. assertThat(getSettingsKey(Settings.Secure.AUTOFILL_SERVICE)) @@ -125,7 +130,8 @@ public final class CredentialManagerServiceTest { Settings.Secure.CREDENTIAL_SERVICE_PRIMARY, "com.example.test/com.example.test.TestActivity"); - CredentialManagerService.updateProvidersWhenPackageRemoved(mContext, "com.example.test"); + CredentialManagerService.updateProvidersWhenPackageRemoved( + mSettingsWrapper, "com.example.test"); assertThat(getSettingsKey(Settings.Secure.AUTOFILL_SERVICE)).isEqualTo(""); assertThat(getSettingsKey(Settings.Secure.CREDENTIAL_SERVICE)) @@ -144,7 +150,8 @@ public final class CredentialManagerServiceTest { setSettingsKey(Settings.Secure.CREDENTIAL_SERVICE, testCredentialValue); setSettingsKey(Settings.Secure.CREDENTIAL_SERVICE_PRIMARY, testCredentialPrimaryValue); - CredentialManagerService.updateProvidersWhenPackageRemoved(mContext, "com.example.test3"); + CredentialManagerService.updateProvidersWhenPackageRemoved( + mSettingsWrapper, "com.example.test3"); // Since the provider removed was not a primary provider then we should do nothing. assertCredentialPropertyEquals( @@ -176,12 +183,36 @@ public final class CredentialManagerServiceTest { assertThat(actualValueSet).isEqualTo(newValueSet); } - private void setSettingsKey(String key, String value) { - assertThat(Settings.Secure.putString(mContext.getContentResolver(), key, value)).isTrue(); + private void setSettingsKey(String name, String value) { + assertThat( + mSettingsWrapper.putStringForUser( + name, value, UserHandle.myUserId(), true)) + .isTrue(); } - private String getSettingsKey(String key) { - return Settings.Secure.getStringForUser( - mContext.getContentResolver(), key, UserHandle.myUserId()); + private String getSettingsKey(String name) { + return mSettingsWrapper.getStringForUser(name, UserHandle.myUserId()); + } + + private static final class MockSettingsWrapper + extends CredentialManagerService.SettingsWrapper { + + MockSettingsWrapper(@NonNull Context context) { + super(context); + } + + /** Updates the string value of a system setting */ + @Override + public boolean putStringForUser( + String name, + String value, + int userHandle, + boolean overrideableByRestore) { + // This will ensure that when the settings putStringForUser method is called by + // CredentialManagerService that the overrideableByRestore bit is true. + assertThat(overrideableByRestore).isTrue(); + + return Settings.Secure.putStringForUser(getContentResolver(), name, value, userHandle); + } } } diff --git a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java index eca19c8e8c4d..2da2f50447c7 100644 --- a/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java +++ b/services/tests/servicestests/src/com/android/server/locksettings/SyntheticPasswordTests.java @@ -506,6 +506,14 @@ public class SyntheticPasswordTests extends BaseLockSettingsServiceTests { } @Test + public void testUnlockUserWithTokenWithBadHandleReturnsFalse() { + final long badTokenHandle = 123456789; + final byte[] token = "some-high-entropy-secure-token".getBytes(); + mService.initializeSyntheticPassword(PRIMARY_USER_ID); + assertFalse(mLocalService.unlockUserWithToken(badTokenHandle, token, PRIMARY_USER_ID)); + } + + @Test public void testGetHashFactorPrimaryUser() throws RemoteException { LockscreenCredential password = newPassword("password"); initSpAndSetCredential(PRIMARY_USER_ID, password); diff --git a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java index 097cc5177a83..abd3abee82fb 100644 --- a/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/media/projection/MediaProjectionManagerServiceTest.java @@ -49,6 +49,7 @@ import static org.mockito.Mockito.when; import static org.testng.Assert.assertThrows; import android.app.ActivityManagerInternal; +import android.app.ActivityOptions.LaunchCookie; import android.content.Context; import android.content.ContextWrapper; import android.content.pm.ApplicationInfo; @@ -784,7 +785,7 @@ public class MediaProjectionManagerServiceTest { @RecordContent int recordedContent) throws NameNotFoundException { MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions(); - projection.setLaunchCookie(mock(IBinder.class)); + projection.setLaunchCookie(new LaunchCookie()); projection.start(mIMediaProjectionCallback); projection.notifyVirtualDisplayCreated(10); // Waiting for user to review consent. @@ -825,7 +826,7 @@ public class MediaProjectionManagerServiceTest { public void testSetUserReviewGrantedConsentResult_displayMirroring_noPriorSession() throws NameNotFoundException { MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions(); - projection.setLaunchCookie(mock(IBinder.class)); + projection.setLaunchCookie(new LaunchCookie()); projection.start(mIMediaProjectionCallback); // Skip setting the prior session details. @@ -844,7 +845,7 @@ public class MediaProjectionManagerServiceTest { public void testSetUserReviewGrantedConsentResult_displayMirroring_sessionNotWaiting() throws NameNotFoundException { MediaProjectionManagerService.MediaProjection projection = startProjectionPreconditions(); - projection.setLaunchCookie(mock(IBinder.class)); + projection.setLaunchCookie(new LaunchCookie()); projection.start(mIMediaProjectionCallback); // Session is not waiting for user's consent. doReturn(true).when(mWindowManagerInternal).setContentRecordingSession( diff --git a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java index 080548520b0c..81df597f3f33 100644 --- a/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/pm/BaseShortcutManagerTest.java @@ -157,6 +157,7 @@ public abstract class BaseShortcutManagerTest extends InstrumentationTestCase { return mMockDevicePolicyManager; case Context.APP_SEARCH_SERVICE: case Context.ROLE_SERVICE: + case Context.APP_OPS_SERVICE: // RoleManager is final and cannot be mocked, so we only override the inject // accessor methods in ShortcutService. return getTestContext().getSystemService(name); diff --git a/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingValidationTest.kt b/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingValidationTest.kt index 757abde2041e..e3ee21a450c7 100644 --- a/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingValidationTest.kt +++ b/services/tests/servicestests/src/com/android/server/pm/parsing/AndroidPackageParsingValidationTest.kt @@ -436,6 +436,13 @@ class AndroidPackageParsingValidationTest { validateTagCount("action", 20000, tag) validateTagCount("category", 40000, tag) validateTagCount("data", 40000, tag) + validateTagCount("uri-relative-filter-group", 100, tag) + } + + @Test + fun parseUriRelativeFilterGroupTag() { + val tag = "uri-relative-filter-group" + validateTagCount("data", 100, tag) } @Test @@ -465,6 +472,54 @@ class AndroidPackageParsingValidationTest { R.styleable.AndroidManifestData_pathAdvancedPattern, 4000 ) + validateTagAttr(tag, "query", R.styleable.AndroidManifestData_query, 4000) + validateTagAttr( + tag, + "queryPattern", + R.styleable.AndroidManifestData_queryPattern, + 4000 + ) + validateTagAttr( + tag, + "queryPrefix", + R.styleable.AndroidManifestData_queryPrefix, + 4000 + ) + validateTagAttr(tag, + "querySuffix", + R.styleable.AndroidManifestData_querySuffix, + 4000 + ) + validateTagAttr( + tag, + "queryAdvancedPattern", + R.styleable.AndroidManifestData_queryAdvancedPattern, + 4000 + ) + validateTagAttr(tag, "fragment", R.styleable.AndroidManifestData_query, 4000) + validateTagAttr( + tag, + "fragmentPattern", + R.styleable.AndroidManifestData_fragmentPattern, + 4000 + ) + validateTagAttr( + tag, + "fragmentPrefix", + R.styleable.AndroidManifestData_fragmentPrefix, + 4000 + ) + validateTagAttr(tag, + "fragmentSuffix", + R.styleable.AndroidManifestData_fragmentSuffix, + 4000 + ) + validateTagAttr( + tag, + "fragmentAdvancedPattern", + R.styleable.AndroidManifestData_fragmentAdvancedPattern, + 4000 + ) validateTagAttr(tag, "mimeType", R.styleable.AndroidManifestData_mimeType, 255) validateTagAttr(tag, "mimeGroup", R.styleable.AndroidManifestData_mimeGroup, 1024) } diff --git a/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java b/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java index 0382ca0d9fec..e21388e57a9e 100644 --- a/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java +++ b/services/tests/wmtests/src/com/android/server/policy/ModifierShortcutTests.java @@ -34,6 +34,7 @@ import static android.view.KeyEvent.KEYCODE_SHIFT_LEFT; import static android.view.KeyEvent.KEYCODE_SLASH; import static android.view.KeyEvent.KEYCODE_SPACE; import static android.view.KeyEvent.KEYCODE_TAB; +import static android.view.KeyEvent.KEYCODE_SCREENSHOT; import static android.view.KeyEvent.KEYCODE_U; import static android.view.KeyEvent.KEYCODE_Z; @@ -193,4 +194,26 @@ public class ModifierShortcutTests extends ShortcutKeyTestBase { mPhoneWindowManager.verifyNewBrightness(newBrightness[i]); } } + + /** + * Sends a KEYCODE_SCREENSHOT and validates screenshot is taken if flag is enabled + */ + @Test + public void testTakeScreenshot_flagEnabled() { + mSetFlagsRule.enableFlags(com.android.hardware.input.Flags + .FLAG_EMOJI_AND_SCREENSHOT_KEYCODES_AVAILABLE); + sendKeyCombination(new int[]{KEYCODE_SCREENSHOT}, 0); + mPhoneWindowManager.assertTakeScreenshotCalled(); + } + + /** + * Sends a KEYCODE_SCREENSHOT and validates screenshot is not taken if flag is disabled + */ + @Test + public void testTakeScreenshot_flagDisabled() { + mSetFlagsRule.disableFlags(com.android.hardware.input.Flags + .FLAG_EMOJI_AND_SCREENSHOT_KEYCODES_AVAILABLE); + sendKeyCombination(new int[]{KEYCODE_SCREENSHOT}, 0); + mPhoneWindowManager.assertTakeScreenshotNotCalled(); + } } diff --git a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java index 157d1627d993..fee6582c1ba4 100644 --- a/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java +++ b/services/tests/wmtests/src/com/android/server/policy/ShortcutKeyTestBase.java @@ -46,6 +46,7 @@ import static com.android.server.policy.WindowManagerPolicy.ACTION_PASS_TO_USER; import static java.util.Collections.unmodifiableMap; import android.content.Context; +import android.platform.test.flag.junit.SetFlagsRule; import android.util.ArrayMap; import android.view.InputDevice; import android.view.KeyCharacterMap; @@ -57,11 +58,17 @@ import com.android.internal.util.test.FakeSettingsProviderRule; import org.junit.After; import org.junit.Rule; +import org.junit.rules.RuleChain; import java.util.Map; class ShortcutKeyTestBase { - @Rule public FakeSettingsProviderRule mSettingsProviderRule = FakeSettingsProvider.rule(); + + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + public final FakeSettingsProviderRule mSettingsProviderRule = FakeSettingsProvider.rule(); + + @Rule + public RuleChain rules = RuleChain.outerRule(mSettingsProviderRule).around(mSetFlagsRule); TestPhoneWindowManager mPhoneWindowManager; DispatchedKeyHandler mDispatchedKeyHandler = event -> false; diff --git a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java index c8abd8d7297e..2904c0325d40 100644 --- a/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java +++ b/services/tests/wmtests/src/com/android/server/policy/TestPhoneWindowManager.java @@ -596,6 +596,11 @@ class TestPhoneWindowManager { verify(mDisplayPolicy).takeScreenshot(anyInt(), anyInt()); } + void assertTakeScreenshotNotCalled() { + mTestLooper.dispatchAll(); + verify(mDisplayPolicy, never()).takeScreenshot(anyInt(), anyInt()); + } + void assertShowGlobalActionsCalled() { mTestLooper.dispatchAll(); verify(mPhoneWindowManager).showGlobalActions(); diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java index 9e00f927a568..5d14334aaf69 100644 --- a/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/DisplayPolicyTests.java @@ -417,6 +417,8 @@ public class DisplayPolicyTests extends WindowTestsBase { di.logicalWidth, di.logicalHeight).mConfigInsets.top); } + // Flush the pending change (DecorInsets.Info#mNeedUpdate) for the rotation to be tested. + displayPolicy.getDecorInsetsInfo(Surface.ROTATION_90, di.logicalHeight, di.logicalWidth); // Add a window that provides the same insets in current rotation. But it specifies // different insets in other rotations. final WindowState bar2 = createWindow(null, navbar.mAttrs.type, "bar2"); @@ -446,6 +448,12 @@ public class DisplayPolicyTests extends WindowTestsBase { // The insets in other rotations should be still updated. assertEquals(doubleHeightFor90, displayPolicy.getDecorInsetsInfo(Surface.ROTATION_90, di.logicalHeight, di.logicalWidth).mConfigInsets.bottom); + // Restore to previous height and the insets can still be updated. + bar2.mAttrs.paramsForRotation[Surface.ROTATION_90].providedInsets[0].setInsetsSize( + Insets.of(0, 0, 0, NAV_BAR_HEIGHT)); + assertFalse(displayPolicy.updateDecorInsetsInfo()); + assertEquals(NAV_BAR_HEIGHT, displayPolicy.getDecorInsetsInfo(Surface.ROTATION_90, + di.logicalHeight, di.logicalWidth).mConfigInsets.bottom); navbar.removeIfPossible(); bar2.removeIfPossible(); diff --git a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java index 3b4b220907f7..9930c88b1e48 100644 --- a/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java +++ b/services/tests/wmtests/src/com/android/server/wm/RecentTasksTest.java @@ -19,6 +19,7 @@ package com.android.server.wm; import static android.app.ActivityManager.RECENT_WITH_EXCLUDED; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED; +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; @@ -1207,6 +1208,29 @@ public class RecentTasksTest extends WindowTestsBase { } @Test + public void addTask_tasksAreAddedAccordingToZOrder() { + final Task firstTask = new TaskBuilder(mSupervisor).setTaskId(1) + .setWindowingMode(WINDOWING_MODE_FREEFORM).build(); + final Task secondTask = new TaskBuilder(mSupervisor).setTaskId(2) + .setWindowingMode(WINDOWING_MODE_FREEFORM).build(); + + assertEquals(-1, firstTask.compareTo(secondTask)); + + // initial addition when tasks are created + mRecentTasks.add(firstTask); + mRecentTasks.add(secondTask); + + assertRecentTasksOrder(secondTask, firstTask); + + // Tasks are added in a different order + mRecentTasks.add(secondTask); + mRecentTasks.add(firstTask); + + // order in recents don't change as first task has lower z-order + assertRecentTasksOrder(secondTask, firstTask); + } + + @Test public void removeTask_callsTaskNotificationController() { final Task task = createTaskBuilder(".Task").build(); diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java index 1bf11df7059a..eb7e67dccfd5 100644 --- a/telephony/java/android/telephony/SubscriptionManager.java +++ b/telephony/java/android/telephony/SubscriptionManager.java @@ -1125,7 +1125,7 @@ public class SubscriptionManager { /** * TelephonyProvider column name for satellite attach enabled for carrier. The value of this * column is set based on user settings. - * By default, it's disabled. + * By default, it's enabled. * <P>Type: INTEGER (int)</P> * @hide */ diff --git a/telephony/java/android/telephony/TelephonyManager.java b/telephony/java/android/telephony/TelephonyManager.java index c1ceaef64d5d..a0f033860b03 100644 --- a/telephony/java/android/telephony/TelephonyManager.java +++ b/telephony/java/android/telephony/TelephonyManager.java @@ -18970,11 +18970,11 @@ public class TelephonyManager { @FlaggedApi(Flags.FLAG_ENABLE_MODEM_CIPHER_TRANSPARENCY) @RequiresPermission(Manifest.permission.MODIFY_PHONE_STATE) @SystemApi - public void setEnableNullCipherNotifications(boolean enable) { + public void setNullCipherNotificationsEnabled(boolean enable) { try { ITelephony telephony = getITelephony(); if (telephony != null) { - telephony.setEnableNullCipherNotifications(enable); + telephony.setNullCipherNotificationsEnabled(enable); } else { throw new IllegalStateException("telephony service is null."); } diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl index 43eb111db0ee..a1fc064d0d81 100644 --- a/telephony/java/com/android/internal/telephony/ITelephony.aidl +++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl @@ -3247,7 +3247,7 @@ interface ITelephony { */ @JavaPassthrough(annotation="@android.annotation.RequiresPermission(" + "android.Manifest.permission.MODIFY_PHONE_STATE)") - void setEnableNullCipherNotifications(boolean enable); + void setNullCipherNotificationsEnabled(boolean enable); /** * Get whether notifications are enabled for null cipher or integrity algorithms in use by the diff --git a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt index 566e51a9062a..cbec85efe93a 100644 --- a/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt +++ b/tests/Input/src/com/android/server/input/InputManagerServiceTests.kt @@ -146,6 +146,7 @@ class InputManagerServiceTests { verify(native).setTouchpadPointerSpeed(anyInt()) verify(native).setTouchpadNaturalScrollingEnabled(anyBoolean()) verify(native).setTouchpadTapToClickEnabled(anyBoolean()) + verify(native).setTouchpadTapDraggingEnabled(anyBoolean()) verify(native).setTouchpadRightClickZoneEnabled(anyBoolean()) verify(native).setShowTouches(anyBoolean()) verify(native).setMotionClassifierEnabled(anyBoolean()) diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp index f3f183815d0b..642a5618b6ad 100644 --- a/tools/aapt2/cmd/Link.cpp +++ b/tools/aapt2/cmd/Link.cpp @@ -1988,6 +1988,8 @@ class Linker { context_->SetNameManglerPolicy(NameManglerPolicy{context_->GetCompilationPackage()}); context_->SetSplitNameDependencies(app_info_.split_name_dependencies); + std::unique_ptr<xml::XmlResource> pre_flags_filter_manifest_xml = manifest_xml->Clone(); + FeatureFlagsFilterOptions flags_filter_options; if (context_->GetMinSdkVersion() > SDK_UPSIDE_DOWN_CAKE) { // For API version > U, PackageManager will dynamically read the flag values and disable @@ -2297,7 +2299,12 @@ class Linker { } if (options_.generate_java_class_path) { - if (!WriteManifestJavaFile(manifest_xml.get())) { + // The FeatureFlagsFilter may remove <permission> and <permission-group> elements that + // generate constants in the Manifest Java file. While we want those permissions and + // permission groups removed in the SDK (i.e., if a feature flag is disabled), the + // constants should still remain so that code referencing it (e.g., within a feature + // flag check) will still compile. Therefore we use the manifest XML before the filter. + if (!WriteManifestJavaFile(pre_flags_filter_manifest_xml.get())) { error = true; } } diff --git a/tools/aapt2/cmd/Link_test.cpp b/tools/aapt2/cmd/Link_test.cpp index 9323f3b95eac..6cc42f17c0a1 100644 --- a/tools/aapt2/cmd/Link_test.cpp +++ b/tools/aapt2/cmd/Link_test.cpp @@ -1021,9 +1021,11 @@ TEST_F(LinkTest, FeatureFlagDisabled_SdkAtMostUDC) { .AddContents(manifest_contents) .Build(); + const std::string app_java = GetTestPath("app-java"); auto app_link_args = LinkCommandBuilder(this) .SetManifestFile(app_manifest) .AddParameter("-I", android_apk) + .AddParameter("--java", app_java) .AddParameter("--feature-flags", "flag=false"); const std::string app_apk = GetTestPath("app.apk"); @@ -1038,6 +1040,12 @@ TEST_F(LinkTest, FeatureFlagDisabled_SdkAtMostUDC) { ASSERT_THAT(root, NotNull()); auto maybe_removed = root->FindChild({}, "permission"); ASSERT_THAT(maybe_removed, IsNull()); + + // Code for the permission should be generated even if the element is removed + const std::string manifest_java = app_java + "/com/example/app/Manifest.java"; + std::string manifest_java_contents; + ASSERT_TRUE(android::base::ReadFileToString(manifest_java, &manifest_java_contents)); + EXPECT_THAT(manifest_java_contents, HasSubstr(" public static final String FOO=\"FOO\";")); } TEST_F(LinkTest, FeatureFlagEnabled_SdkAtMostUDC) { diff --git a/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/MessageQueue_host.java b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/MessageQueue_host.java index 2e47d48f4fa0..65da4a144160 100644 --- a/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/MessageQueue_host.java +++ b/tools/hoststubgen/hoststubgen/helper-framework-runtime-src/framework/com/android/hoststubgen/nativesubstitution/MessageQueue_host.java @@ -28,6 +28,7 @@ public class MessageQueue_host { private final Object mPoller = new Object(); private volatile boolean mPolling; + private volatile boolean mPendingWake; private void validate() { if (mDeleted) { @@ -62,7 +63,9 @@ public class MessageQueue_host { synchronized (q.mPoller) { q.mPolling = true; try { - if (timeoutMillis == 0) { + if (q.mPendingWake) { + // Calling with pending wake returns immediately + } else if (timeoutMillis == 0) { // Calling epoll_wait() with 0 returns immediately } else if (timeoutMillis == -1) { q.mPoller.wait(); @@ -72,6 +75,8 @@ public class MessageQueue_host { } catch (InterruptedException e) { Thread.currentThread().interrupt(); } + // Any reason for returning counts as a "wake", so clear pending + q.mPendingWake = false; q.mPolling = false; } } @@ -79,6 +84,7 @@ public class MessageQueue_host { public static void nativeWake(long ptr) { var q = getInstance(ptr); synchronized (q.mPoller) { + q.mPendingWake = true; q.mPoller.notifyAll(); } } diff --git a/tools/hoststubgen/hoststubgen/helper-runtime-src/com/android/hoststubgen/hosthelper/HostTestUtils.java b/tools/hoststubgen/hoststubgen/helper-runtime-src/com/android/hoststubgen/hosthelper/HostTestUtils.java index 7c6aa25bf5ea..60eb47eea7c7 100644 --- a/tools/hoststubgen/hoststubgen/helper-runtime-src/com/android/hoststubgen/hosthelper/HostTestUtils.java +++ b/tools/hoststubgen/hoststubgen/helper-runtime-src/com/android/hoststubgen/hosthelper/HostTestUtils.java @@ -63,7 +63,10 @@ public class HostTestUtils { */ public static void onThrowMethodCalled() { // TODO: Maybe add call tracking? - throw new RuntimeException("This method is not supported on the host side"); + throw new RuntimeException( + "This method is not yet supported under the Ravenwood deviceless testing " + + "environment; consider requesting support from the API owner or " + + "consider using Mockito; more details at go/ravenwood-docs"); } /** diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassTest.java b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassTest.java index fc6b862705f8..ba17c75132f2 100644 --- a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassTest.java +++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassTest.java @@ -68,7 +68,7 @@ public class TinyFrameworkClassTest { TinyFrameworkForTextPolicy tfc = new TinyFrameworkForTextPolicy(); thrown.expect(RuntimeException.class); - thrown.expectMessage("This method is not supported on the host side"); + thrown.expectMessage("not yet supported"); tfc.visibleButUsesUnsupportedMethod(); } @@ -182,7 +182,7 @@ public class TinyFrameworkClassTest { } catch (java.lang.reflect.InvocationTargetException e) { var inner = e.getCause(); - assertThat(inner.getMessage()).contains("not supported on the host side"); + assertThat(inner.getMessage()).contains("not yet supported"); } } diff --git a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassWithAnnotTest.java b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassWithAnnotTest.java index 20cc2ec9d50d..288c7162aa58 100644 --- a/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassWithAnnotTest.java +++ b/tools/hoststubgen/hoststubgen/test-tiny-framework/tiny-test/src/com/android/hoststubgen/test/tinyframework/TinyFrameworkClassWithAnnotTest.java @@ -60,7 +60,7 @@ public class TinyFrameworkClassWithAnnotTest { TinyFrameworkClassAnnotations tfc = new TinyFrameworkClassAnnotations(); thrown.expect(RuntimeException.class); - thrown.expectMessage("This method is not supported on the host side"); + thrown.expectMessage("not yet supported"); tfc.visibleButUsesUnsupportedMethod(); } } |