diff options
244 files changed, 8026 insertions, 1665 deletions
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..920d6e00598d 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); diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 94cde98ca762..708f10e27bcc 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[]); diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 42cf08ff9a66..c546d7a4cfe8 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"; 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/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/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/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/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/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/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/Telephony.java b/core/java/android/provider/Telephony.java index d47ff2e2cd99..af688e92fb36 100644 --- a/core/java/android/provider/Telephony.java +++ b/core/java/android/provider/Telephony.java @@ -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/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/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/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/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/values/config.xml b/core/res/res/values/config.xml index 23c78fdf108e..ebb0d344d7eb 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -6908,4 +6908,7 @@ <!-- Defines the minimum interval (in ms) between two input-based user-activity poke events. --> <integer name="config_minMillisBetweenInputUserActivityEvents">100</integer> + + <!-- Name of the starting activity for DisplayCompat host. specific to automotive.--> + <string name="config_defaultDisplayCompatHostActivity" translatable="false"></string> </resources> 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/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/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..945b8ac59b98 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 @@ -1366,8 +1366,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 +1479,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 +1486,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> 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/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/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/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..28c06a46c516 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 @@ -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() } /** 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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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/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..9504cfcdbe3c 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUICoreStartableModule.kt @@ -24,6 +24,7 @@ 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 @@ -50,7 +51,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 +224,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,6 +322,11 @@ abstract class SystemUICoreStartableModule { @Binds @IntoMap + @ClassKey(CommunalSceneStartable::class) + abstract fun bindCommunalSceneStartable(impl: CommunalSceneStartable): CoreStartable + + @Binds + @IntoMap @ClassKey(CommunalAppWidgetHostStartable::class) abstract fun bindCommunalAppWidgetHostStartable( impl: CommunalAppWidgetHostStartable 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/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/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/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/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/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/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/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..cf76c0d2e696 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 @@ -190,7 +190,7 @@ constructor( } } - tracker.addCallback(callback, backgroundDispatcher.asExecutor()) + tracker.addCallback(callback, mainDispatcher.asExecutor()) send(currentSelectionStatus) awaitClose { tracker.removeCallback(callback) } 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/wmshell/WMShell.java b/packages/SystemUI/src/com/android/systemui/wmshell/WMShell.java index 7c6ad233d853..1e801aeb5a29 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); 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/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/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/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/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/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/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/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/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java index b3dbcde9d324..a797b1d61b2a 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,35 @@ package android.platform.test.ravenwood; +import android.app.Instrumentation; +import android.os.Bundle; import android.os.HandlerThread; import android.os.Looper; +import androidx.test.platform.app.InstrumentationRegistry; + +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 +64,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 +91,19 @@ public class RavenwoodRuleImpl { android.os.Binder.reset$ravenwood(); android.os.Process.reset$ravenwood(); } + + 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..1e7cbf6d802d 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 @@ -245,6 +299,7 @@ public class RavenwoodRule implements TestRule { return new Statement() { @Override public void evaluate() throws Throwable { + Assume.assumeFalse(shouldStillIgnoreInProbeIgnoreMode(description)); RavenwoodRuleImpl.init(RavenwoodRule.this); try { base.evaluate(); 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/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/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/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/MediaSessionService.java b/services/core/java/com/android/server/media/MediaSessionService.java index 1d516e2931d7..7fabdf293b39 100644 --- a/services/core/java/com/android/server/media/MediaSessionService.java +++ b/services/core/java/com/android/server/media/MediaSessionService.java @@ -34,6 +34,8 @@ import android.app.ForegroundServiceDelegationOptions; import android.app.KeyguardManager; import android.app.NotificationManager; import android.app.PendingIntent; +import android.app.usage.UsageStatsManager; +import android.app.usage.UsageStatsManagerInternal; import android.content.ActivityNotFoundException; import android.content.BroadcastReceiver; import android.content.ComponentName; @@ -68,6 +70,7 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.IBinder; import android.os.Message; +import android.os.PersistableBundle; import android.os.PowerExemptionManager; import android.os.PowerManager; import android.os.Process; @@ -100,7 +103,9 @@ import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; +import java.util.Set; /** * System implementation of MediaSessionManager @@ -145,6 +150,8 @@ public class MediaSessionService extends SystemService implements Monitor { private AudioManager mAudioManager; private boolean mHasFeatureLeanback; private ActivityManagerInternal mActivityManagerInternal; + private UsageStatsManagerInternal mUsageStatsManagerInternal; + private final Set<Integer> mUserEngagingSessions = new HashSet<>(); // The FullUserRecord of the current users. (i.e. The foreground user that isn't a profile) // It's always not null after the MediaSessionService is started. @@ -230,6 +237,7 @@ public class MediaSessionService extends SystemService implements Monitor { mContext.registerReceiver(mNotificationListenerEnabledChangedReceiver, filter); mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class); + mUsageStatsManagerInternal = LocalServices.getService(UsageStatsManagerInternal.class); } @Override @@ -582,12 +590,43 @@ public class MediaSessionService extends SystemService implements Monitor { if (allowRunningInForeground) { mActivityManagerInternal.startForegroundServiceDelegate( foregroundServiceDelegationOptions, /* connection= */ null); + reportMediaInteractionEvent(record, /* userEngaged= */ true); } else { mActivityManagerInternal.stopForegroundServiceDelegate( foregroundServiceDelegationOptions); + reportMediaInteractionEvent(record, /* userEngaged= */ false); } } + private void reportMediaInteractionEvent(MediaSessionRecordImpl record, boolean userEngaged) { + if (!android.app.usage.Flags.userInteractionTypeApi()) { + return; + } + + String packageName = record.getPackageName(); + int sessionUid = record.getUid(); + String actionToLog = null; + if (userEngaged) { + if (!mUserEngagingSessions.contains(sessionUid)) { + actionToLog = "start"; + } + mUserEngagingSessions.add(sessionUid); + } else { + if (mUserEngagingSessions.contains(sessionUid)) { + actionToLog = "stop"; + } + mUserEngagingSessions.remove(sessionUid); + } + + if (actionToLog != null) { + PersistableBundle extras = new PersistableBundle(); + extras.putString(UsageStatsManager.EXTRA_EVENT_CATEGORY, "android.media"); + extras.putString(UsageStatsManager.EXTRA_EVENT_ACTION, actionToLog); + mUsageStatsManagerInternal.reportUserInteractionEvent( + packageName, record.getUserId(), extras); + } + } + void tempAllowlistTargetPkgIfPossible(int targetUid, String targetPackage, int callingPid, int callingUid, String callingPackage, String reason) { final long token = Binder.clearCallingIdentity(); 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/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/trust/TrustManagerService.java b/services/core/java/com/android/server/trust/TrustManagerService.java index e5a8a6dd2a3a..dbf777f48e35 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 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/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/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/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/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/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/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(); } } |